From 84ce38a586269aad846bb8b8c740a9bafb50df8b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 00:39:10 +0000 Subject: [PATCH] Implement file support with models, interface, and comprehensive tests Co-authored-by: StevanFreeborn <65925598+StevanFreeborn@users.noreply.github.com> --- src/AnthropicClient/AnthropicApiClient.cs | 85 +++++++ src/AnthropicClient/IAnthropicApiClient.cs | 48 ++++ src/AnthropicClient/Models/AnthropicFile.cs | 42 ++++ .../Models/FileDeleteResponse.cs | 24 ++ .../Models/FileDownloadResponse.cs | 42 ++++ src/AnthropicClient/Models/FileRequest.cs | 51 ++++ .../AnthropicApiClientFileTests.cs | 219 ++++++++++++++++++ .../Integration/IntegrationTest.cs | 31 +++ .../Unit/Models/AnthropicFileTests.cs | 67 ++++++ .../Unit/Models/FileDeleteResponseTests.cs | 52 +++++ .../Unit/Models/FileDownloadResponseTests.cs | 20 ++ .../Unit/Models/FileRequestTests.cs | 103 ++++++++ 12 files changed, 784 insertions(+) create mode 100644 src/AnthropicClient/Models/AnthropicFile.cs create mode 100644 src/AnthropicClient/Models/FileDeleteResponse.cs create mode 100644 src/AnthropicClient/Models/FileDownloadResponse.cs create mode 100644 src/AnthropicClient/Models/FileRequest.cs create mode 100644 tests/AnthropicClient.Tests/Integration/AnthropicApiClientFileTests.cs create mode 100644 tests/AnthropicClient.Tests/Unit/Models/AnthropicFileTests.cs create mode 100644 tests/AnthropicClient.Tests/Unit/Models/FileDeleteResponseTests.cs create mode 100644 tests/AnthropicClient.Tests/Unit/Models/FileDownloadResponseTests.cs create mode 100644 tests/AnthropicClient.Tests/Unit/Models/FileRequestTests.cs diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs index 2e78ac2..b490d32 100644 --- a/src/AnthropicClient/AnthropicApiClient.cs +++ b/src/AnthropicClient/AnthropicApiClient.cs @@ -18,6 +18,7 @@ public class AnthropicApiClient : IAnthropicApiClient private string CountTokensEndpoint => $"{MessagesEndpoint}/count_tokens"; private string MessageBatchesEndpoint => $"{MessagesEndpoint}/batches"; private const string ModelsEndpoint = "models"; + private const string FilesEndpoint = "files"; private const string JsonContentType = "application/json"; private const string EventPrefix = "event:"; private const string DataPrefix = "data:"; @@ -380,6 +381,79 @@ public class AnthropicApiClient : IAnthropicApiClient return await CreateResultAsync(response); } + /// + public async Task> CreateFileAsync(FileRequest request, CancellationToken cancellationToken = default) + { + var formData = new MultipartFormDataContent(); + formData.Add(new ByteArrayContent(request.Content), "file", request.Filename); + formData.Add(new StringContent(request.Purpose), "purpose"); + + var httpRequest = new HttpRequestMessage(HttpMethod.Post, FilesEndpoint) + { + Content = formData + }; + + var response = await _httpClient.SendAsync(httpRequest, cancellationToken); + return await CreateResultAsync(response); + } + + /// + public async Task>> ListFilesAsync(PagingRequest? request = null, CancellationToken cancellationToken = default) + { + var pagingRequest = request ?? new PagingRequest(); + var endpoint = $"{FilesEndpoint}?{pagingRequest.ToQueryParameters()}"; + var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken); + return await CreateResultAsync>(response); + } + + /// + public async IAsyncEnumerable>> ListAllFilesAsync(int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (var result in GetAllPagesAsync(FilesEndpoint, limit, cancellationToken)) + { + yield return result; + } + } + + /// + public async Task> GetFileAsync(string fileId, CancellationToken cancellationToken = default) + { + var endpoint = $"{FilesEndpoint}/{fileId}"; + var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken); + return await CreateResultAsync(response); + } + + /// + public async Task> DownloadFileAsync(string fileId, CancellationToken cancellationToken = default) + { + var endpoint = $"{FilesEndpoint}/{fileId}/content"; + var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken); + var anthropicHeaders = new AnthropicHeaders(response.Headers); + + if (response.IsSuccessStatusCode is false) + { + var errorContent = await response.Content.ReadAsStringAsync(); + var error = Deserialize(errorContent) ?? new AnthropicError(); + return AnthropicResult.Failure(error, anthropicHeaders); + } + + var content = await response.Content.ReadAsByteArrayAsync(); + var contentType = response.Content.Headers.ContentType?.MediaType ?? "application/octet-stream"; + var filename = ExtractFilenameFromContentDisposition(response.Content.Headers.ContentDisposition?.FileName) ?? fileId; + var sizeBytes = content.Length; + + var downloadResponse = new FileDownloadResponse(content, filename, contentType, sizeBytes); + return AnthropicResult.Success(downloadResponse, anthropicHeaders); + } + + /// + public async Task> DeleteFileAsync(string fileId, CancellationToken cancellationToken = default) + { + var endpoint = $"{FilesEndpoint}/{fileId}"; + var response = await SendRequestAsync(endpoint, HttpMethod.Delete, cancellationToken); + return await CreateResultAsync(response); + } + private async IAsyncEnumerable>> GetAllPagesAsync(string endpoint, int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var pagingRequest = new PagingRequest(limit: limit); @@ -449,6 +523,17 @@ public class AnthropicApiClient : IAnthropicApiClient return AnthropicResult.Success(model, anthropicHeaders); } + private static string? ExtractFilenameFromContentDisposition(string? contentDisposition) + { + if (string.IsNullOrEmpty(contentDisposition)) + { + return null; + } + + // Remove quotes if present + return contentDisposition!.Trim('"'); + } + private async Task SendRequestAsync(string endpoint, HttpMethod? method = null, CancellationToken cancellationToken = default) { var request = new HttpRequestMessage(method ?? HttpMethod.Get, endpoint); diff --git a/src/AnthropicClient/IAnthropicApiClient.cs b/src/AnthropicClient/IAnthropicApiClient.cs index e4f63b9..dc7f794 100644 --- a/src/AnthropicClient/IAnthropicApiClient.cs +++ b/src/AnthropicClient/IAnthropicApiClient.cs @@ -111,4 +111,52 @@ public interface IAnthropicApiClient /// A token to cancel the asynchronous operation. /// A task that represents the asynchronous operation. The task result contains the response as an where T is . Task> GetModelAsync(string modelId, CancellationToken cancellationToken = default); + + /// + /// Creates a file asynchronously. + /// + /// The file request to create. + /// A token to cancel the asynchronous operation. + /// A task that represents the asynchronous operation. The task result contains the response as an where T is . + Task> CreateFileAsync(FileRequest request, CancellationToken cancellationToken = default); + + /// + /// Lists files asynchronously, returning a single page of results. + /// + /// The paging request to use for listing the files. + /// A token to cancel the asynchronous operation. + /// A task that represents the asynchronous operation. The task result contains the response as an where T is where T is . + Task>> ListFilesAsync(PagingRequest? request = null, CancellationToken cancellationToken = default); + + /// + /// Lists all files asynchronously, returning every page of results. + /// + /// The maximum number of files to return in each page. + /// A token to cancel the asynchronous operation. + /// An asynchronous enumerable that yields the response as an where T is where T is . + IAsyncEnumerable>> ListAllFilesAsync(int limit = 20, CancellationToken cancellationToken = default); + + /// + /// Gets a file by its ID asynchronously. + /// + /// The ID of the file to get. + /// A token to cancel the asynchronous operation. + /// A task that represents the asynchronous operation. The task result contains the response as an where T is . + Task> GetFileAsync(string fileId, CancellationToken cancellationToken = default); + + /// + /// Downloads a file by its ID asynchronously. + /// + /// The ID of the file to download. + /// A token to cancel the asynchronous operation. + /// A task that represents the asynchronous operation. The task result contains the response as an where T is . + Task> DownloadFileAsync(string fileId, CancellationToken cancellationToken = default); + + /// + /// Deletes a file by its ID asynchronously. + /// + /// The ID of the file to delete. + /// A token to cancel the asynchronous operation. + /// A task that represents the asynchronous operation. The task result contains the response as an where T is . + Task> DeleteFileAsync(string fileId, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/AnthropicClient/Models/AnthropicFile.cs b/src/AnthropicClient/Models/AnthropicFile.cs new file mode 100644 index 0000000..93db238 --- /dev/null +++ b/src/AnthropicClient/Models/AnthropicFile.cs @@ -0,0 +1,42 @@ +using System.Text.Json.Serialization; + +namespace AnthropicClient.Models; + +/// +/// Represents a file in the Anthropic API. +/// +public class AnthropicFile +{ + /// + /// The type of the object. + /// + public string Type { get; init; } = "file"; + + /// + /// The unique identifier for the file. + /// + public string Id { get; init; } = string.Empty; + + /// + /// The filename of the file. + /// + public string Filename { get; init; } = string.Empty; + + /// + /// The MIME type of the file. + /// + [JsonPropertyName("content_type")] + public string ContentType { get; init; } = string.Empty; + + /// + /// The size of the file in bytes. + /// + [JsonPropertyName("size_bytes")] + public int SizeBytes { get; init; } + + /// + /// The date and time when the file was created. + /// + [JsonPropertyName("created_at")] + public DateTimeOffset CreatedAt { get; init; } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/FileDeleteResponse.cs b/src/AnthropicClient/Models/FileDeleteResponse.cs new file mode 100644 index 0000000..8964307 --- /dev/null +++ b/src/AnthropicClient/Models/FileDeleteResponse.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace AnthropicClient.Models; + +/// +/// Represents the response from a file deletion operation. +/// +public class FileDeleteResponse +{ + /// + /// The type of the object. + /// + public string Type { get; init; } = "file_deleted"; + + /// + /// The unique identifier for the deleted file. + /// + public string Id { get; init; } = string.Empty; + + /// + /// Indicates whether the file was successfully deleted. + /// + public bool Deleted { get; init; } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/FileDownloadResponse.cs b/src/AnthropicClient/Models/FileDownloadResponse.cs new file mode 100644 index 0000000..05a6a95 --- /dev/null +++ b/src/AnthropicClient/Models/FileDownloadResponse.cs @@ -0,0 +1,42 @@ +namespace AnthropicClient.Models; + +/// +/// Represents the response from downloading a file. +/// +public class FileDownloadResponse +{ + /// + /// The file content as a byte array. + /// + public byte[] Content { get; } + + /// + /// The filename of the file. + /// + public string Filename { get; } + + /// + /// The MIME type of the file. + /// + public string ContentType { get; } + + /// + /// The size of the file in bytes. + /// + public int SizeBytes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The file content as a byte array. + /// The filename of the file. + /// The MIME type of the file. + /// The size of the file in bytes. + public FileDownloadResponse(byte[] content, string filename, string contentType, int sizeBytes) + { + Content = content; + Filename = filename; + ContentType = contentType; + SizeBytes = sizeBytes; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/FileRequest.cs b/src/AnthropicClient/Models/FileRequest.cs new file mode 100644 index 0000000..98f9633 --- /dev/null +++ b/src/AnthropicClient/Models/FileRequest.cs @@ -0,0 +1,51 @@ +using AnthropicClient.Utils; + +namespace AnthropicClient.Models; + +/// +/// Represents a request to create a file. +/// +public class FileRequest +{ + /// + /// The file content as a byte array. + /// + public byte[] Content { get; } + + /// + /// The filename of the file. + /// + public string Filename { get; } + + /// + /// The MIME type of the file. + /// + public string ContentType { get; } + + /// + /// The purpose of the file. + /// + public string Purpose { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The file content as a byte array. + /// The filename of the file. + /// The MIME type of the file. + /// The purpose of the file (default: "user_upload"). + /// Thrown when content, filename, or contentType is null. + /// Thrown when filename or contentType is empty. + public FileRequest(byte[] content, string filename, string contentType, string purpose = "user_upload") + { + ArgumentValidator.ThrowIfNull(content, nameof(content)); + ArgumentValidator.ThrowIfNullOrWhitespace(filename, nameof(filename)); + ArgumentValidator.ThrowIfNullOrWhitespace(contentType, nameof(contentType)); + ArgumentValidator.ThrowIfNullOrWhitespace(purpose, nameof(purpose)); + + Content = content; + Filename = filename; + ContentType = contentType; + Purpose = purpose; + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientFileTests.cs b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientFileTests.cs new file mode 100644 index 0000000..6c25011 --- /dev/null +++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientFileTests.cs @@ -0,0 +1,219 @@ +namespace AnthropicClient.Tests.Integration; + +public class AnthropicApiClientFileTests : IntegrationTest +{ + [Fact] + public async Task CreateFileAsync_WhenCalled_ItShouldReturnFileResponse() + { + var fileResponseJson = @"{ + ""type"": ""file"", + ""id"": ""file_abc123"", + ""filename"": ""example.txt"", + ""content_type"": ""text/plain"", + ""size_bytes"": 1024, + ""created_at"": ""2024-03-15T10:30:00Z"" + }"; + + _mockHttpMessageHandler + .WhenCreateFileRequest() + .Respond(HttpStatusCode.OK, "application/json", fileResponseJson); + + var content = "Hello World"u8.ToArray(); + var request = new FileRequest(content, "example.txt", "text/plain"); + + var result = await Client.CreateFileAsync(request); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + result.Value!.Id.Should().Be("file_abc123"); + result.Value.Filename.Should().Be("example.txt"); + result.Value.ContentType.Should().Be("text/plain"); + result.Value.SizeBytes.Should().Be(1024); + } + + [Fact] + public async Task ListFilesAsync_WhenCalled_ItShouldReturnFilesPage() + { + var filesResponseJson = @"{ + ""data"": [ + { + ""type"": ""file"", + ""id"": ""file_abc123"", + ""filename"": ""example.txt"", + ""content_type"": ""text/plain"", + ""size_bytes"": 1024, + ""created_at"": ""2024-03-15T10:30:00Z"" + } + ], + ""has_more"": false, + ""first_id"": ""file_abc123"", + ""last_id"": ""file_abc123"" + }"; + + _mockHttpMessageHandler + .WhenListFilesRequest() + .Respond(HttpStatusCode.OK, "application/json", filesResponseJson); + + var result = await Client.ListFilesAsync(); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + result.Value!.Data.Should().HaveCount(1); + result.Value.Data[0].Id.Should().Be("file_abc123"); + result.Value.HasMore.Should().BeFalse(); + } + + [Fact] + public async Task GetFileAsync_WhenCalled_ItShouldReturnFileResponse() + { + var fileResponseJson = @"{ + ""type"": ""file"", + ""id"": ""file_abc123"", + ""filename"": ""example.txt"", + ""content_type"": ""text/plain"", + ""size_bytes"": 1024, + ""created_at"": ""2024-03-15T10:30:00Z"" + }"; + + _mockHttpMessageHandler + .WhenGetFileRequest("file_abc123") + .Respond(HttpStatusCode.OK, "application/json", fileResponseJson); + + var result = await Client.GetFileAsync("file_abc123"); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + result.Value!.Id.Should().Be("file_abc123"); + result.Value.Filename.Should().Be("example.txt"); + } + + [Fact] + public async Task DownloadFileAsync_WhenCalled_ItShouldReturnFileContent() + { + var fileContent = "Hello World"u8.ToArray(); + + var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent(fileContent) + }; + httpResponseMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("text/plain"); + httpResponseMessage.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment") + { + FileName = "\"example.txt\"" + }; + + _mockHttpMessageHandler + .WhenDownloadFileRequest("file_abc123") + .Respond(_ => httpResponseMessage); + + var result = await Client.DownloadFileAsync("file_abc123"); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + result.Value!.Content.Should().BeEquivalentTo(fileContent); + result.Value.Filename.Should().Be("example.txt"); + result.Value.ContentType.Should().Be("text/plain"); + result.Value.SizeBytes.Should().Be(fileContent.Length); + } + + [Fact] + public async Task DeleteFileAsync_WhenCalled_ItShouldReturnDeleteResponse() + { + var deleteResponseJson = @"{ + ""type"": ""file_deleted"", + ""id"": ""file_abc123"", + ""deleted"": true + }"; + + _mockHttpMessageHandler + .WhenDeleteFileRequest("file_abc123") + .Respond(HttpStatusCode.OK, "application/json", deleteResponseJson); + + var result = await Client.DeleteFileAsync("file_abc123"); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + result.Value!.Id.Should().Be("file_abc123"); + result.Value.Deleted.Should().BeTrue(); + } + + [Fact] + public async Task CreateFileAsync_WhenCalledAndErrorReturned_ItShouldHandleError() + { + var errorJson = @"{ + ""type"": ""error"", + ""error"": { + ""type"": ""invalid_request_error"", + ""message"": ""File too large"" + } + }"; + + _mockHttpMessageHandler + .WhenCreateFileRequest() + .Respond(HttpStatusCode.BadRequest, "application/json", errorJson); + + var content = "Hello World"u8.ToArray(); + var request = new FileRequest(content, "example.txt", "text/plain"); + + var result = await Client.CreateFileAsync(request); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeOfType(); + result.Error.Error.Should().BeOfType(); + } + + [Fact] + public async Task ListAllFilesAsync_WhenCalled_ItShouldReturnAllPages() + { + var firstPageJson = @"{ + ""data"": [ + { + ""type"": ""file"", + ""id"": ""file_abc123"", + ""filename"": ""example1.txt"", + ""content_type"": ""text/plain"", + ""size_bytes"": 1024, + ""created_at"": ""2024-03-15T10:30:00Z"" + } + ], + ""has_more"": true, + ""first_id"": ""file_abc123"", + ""last_id"": ""file_abc123"" + }"; + + var secondPageJson = @"{ + ""data"": [ + { + ""type"": ""file"", + ""id"": ""file_def456"", + ""filename"": ""example2.txt"", + ""content_type"": ""text/plain"", + ""size_bytes"": 2048, + ""created_at"": ""2024-03-15T11:30:00Z"" + } + ], + ""has_more"": false, + ""first_id"": ""file_def456"", + ""last_id"": ""file_def456"" + }"; + + _mockHttpMessageHandler + .WhenListFilesRequest() + .Respond(HttpStatusCode.OK, "application/json", firstPageJson); + + _mockHttpMessageHandler + .WhenListFilesRequest() + .Respond(HttpStatusCode.OK, "application/json", secondPageJson); + + var pages = new List>>(); + await foreach (var page in Client.ListAllFilesAsync(1)) + { + pages.Add(page); + } + + pages.Should().HaveCount(2); + pages.All(p => p.IsSuccess).Should().BeTrue(); + pages[0].Value!.Data[0].Id.Should().Be("file_abc123"); + pages[1].Value!.Data[0].Id.Should().Be("file_def456"); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs index 223107b..b4ed52e 100644 --- a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs +++ b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs @@ -20,6 +20,7 @@ public static class MockHttpMessageHandlerExtensions private static readonly string CountTokensEndpoint = $"{BaseUrl}/messages/count_tokens"; private static readonly string MessageBatchesEndpoint = $"{BaseUrl}/messages/batches"; private static readonly string ModelsEndpoint = $"{BaseUrl}/models"; + private static readonly string FilesEndpoint = $"{BaseUrl}/files"; private static MockedRequest SetupBaseRequest( this MockHttpMessageHandler mockHttpMessageHandler, @@ -103,4 +104,34 @@ public static class MockHttpMessageHandlerExtensions return mockHttpMessageHandler .SetupBaseRequest(HttpMethod.Delete, $"{MessageBatchesEndpoint}/{batchId}"); } + + public static MockedRequest WhenCreateFileRequest(this MockHttpMessageHandler mockHttpMessageHandler) + { + return mockHttpMessageHandler + .SetupBaseRequest(HttpMethod.Post, FilesEndpoint); + } + + public static MockedRequest WhenListFilesRequest(this MockHttpMessageHandler mockHttpMessageHandler) + { + return mockHttpMessageHandler + .SetupBaseRequest(HttpMethod.Get, FilesEndpoint); + } + + public static MockedRequest WhenGetFileRequest(this MockHttpMessageHandler mockHttpMessageHandler, string fileId) + { + return mockHttpMessageHandler + .SetupBaseRequest(HttpMethod.Get, $"{FilesEndpoint}/{fileId}"); + } + + public static MockedRequest WhenDownloadFileRequest(this MockHttpMessageHandler mockHttpMessageHandler, string fileId) + { + return mockHttpMessageHandler + .SetupBaseRequest(HttpMethod.Get, $"{FilesEndpoint}/{fileId}/content"); + } + + public static MockedRequest WhenDeleteFileRequest(this MockHttpMessageHandler mockHttpMessageHandler, string fileId) + { + return mockHttpMessageHandler + .SetupBaseRequest(HttpMethod.Delete, $"{FilesEndpoint}/{fileId}"); + } } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/AnthropicFileTests.cs b/tests/AnthropicClient.Tests/Unit/Models/AnthropicFileTests.cs new file mode 100644 index 0000000..e847657 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/AnthropicFileTests.cs @@ -0,0 +1,67 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class AnthropicFileTests : SerializationTest +{ + private const string SampleJson = @"{ + ""type"": ""file"", + ""id"": ""file_abc123"", + ""filename"": ""example.txt"", + ""content_type"": ""text/plain"", + ""size_bytes"": 1024, + ""created_at"": ""2024-03-15T10:30:00Z"" + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstanceWithPropertiesSet() + { + var file = new AnthropicFile(); + + file.Type.Should().Be("file"); + file.Id.Should().BeEmpty(); + file.Filename.Should().BeEmpty(); + file.ContentType.Should().BeEmpty(); + file.SizeBytes.Should().Be(0); + file.CreatedAt.Should().Be(default); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues() + { + var result = Deserialize(SampleJson); + + result.Should().NotBeNull(); + result!.Type.Should().Be("file"); + result.Id.Should().Be("file_abc123"); + result.Filename.Should().Be("example.txt"); + result.ContentType.Should().Be("text/plain"); + result.SizeBytes.Should().Be(1024); + result.CreatedAt.Should().Be(new DateTimeOffset(2024, 3, 15, 10, 30, 0, TimeSpan.Zero)); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldReturnExpectedShape() + { + var file = new AnthropicFile + { + Type = "file", + Id = "file_abc123", + Filename = "example.txt", + ContentType = "text/plain", + SizeBytes = 1024, + CreatedAt = new DateTimeOffset(2024, 3, 15, 10, 30, 0, TimeSpan.Zero) + }; + + var result = Serialize(file); + + var expectedJson = @"{ + ""type"": ""file"", + ""id"": ""file_abc123"", + ""filename"": ""example.txt"", + ""content_type"": ""text/plain"", + ""size_bytes"": 1024, + ""created_at"": ""2024-03-15T10:30:00+00:00"" + }"; + + JsonAssert.Equal(expectedJson, result); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/FileDeleteResponseTests.cs b/tests/AnthropicClient.Tests/Unit/Models/FileDeleteResponseTests.cs new file mode 100644 index 0000000..be14fe6 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/FileDeleteResponseTests.cs @@ -0,0 +1,52 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class FileDeleteResponseTests : SerializationTest +{ + private const string SampleJson = @"{ + ""type"": ""file_deleted"", + ""id"": ""file_abc123"", + ""deleted"": true + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstanceWithPropertiesSet() + { + var response = new FileDeleteResponse(); + + response.Type.Should().Be("file_deleted"); + response.Id.Should().BeEmpty(); + response.Deleted.Should().BeFalse(); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues() + { + var result = Deserialize(SampleJson); + + result.Should().NotBeNull(); + result!.Type.Should().Be("file_deleted"); + result.Id.Should().Be("file_abc123"); + result.Deleted.Should().BeTrue(); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldReturnExpectedShape() + { + var response = new FileDeleteResponse + { + Type = "file_deleted", + Id = "file_abc123", + Deleted = true + }; + + var result = Serialize(response); + + var expectedJson = @"{ + ""type"": ""file_deleted"", + ""id"": ""file_abc123"", + ""deleted"": true + }"; + + JsonAssert.Equal(expectedJson, result); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/FileDownloadResponseTests.cs b/tests/AnthropicClient.Tests/Unit/Models/FileDownloadResponseTests.cs new file mode 100644 index 0000000..8b08d53 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/FileDownloadResponseTests.cs @@ -0,0 +1,20 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class FileDownloadResponseTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstanceWithPropertiesSet() + { + var content = "Hello World"u8.ToArray(); + var filename = "example.txt"; + var contentType = "text/plain"; + var sizeBytes = 1024; + + var response = new FileDownloadResponse(content, filename, contentType, sizeBytes); + + response.Content.Should().BeEquivalentTo(content); + response.Filename.Should().Be(filename); + response.ContentType.Should().Be(contentType); + response.SizeBytes.Should().Be(sizeBytes); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/FileRequestTests.cs b/tests/AnthropicClient.Tests/Unit/Models/FileRequestTests.cs new file mode 100644 index 0000000..4173c59 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/FileRequestTests.cs @@ -0,0 +1,103 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class FileRequestTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstanceWithPropertiesSet() + { + var content = "Hello World"u8.ToArray(); + var filename = "example.txt"; + var contentType = "text/plain"; + var purpose = "user_upload"; + + var request = new FileRequest(content, filename, contentType, purpose); + + request.Content.Should().BeEquivalentTo(content); + request.Filename.Should().Be(filename); + request.ContentType.Should().Be(contentType); + request.Purpose.Should().Be(purpose); + } + + [Fact] + public void Constructor_WhenCalledWithDefaultPurpose_ItShouldSetPurposeToUserUpload() + { + var content = "Hello World"u8.ToArray(); + var filename = "example.txt"; + var contentType = "text/plain"; + + var request = new FileRequest(content, filename, contentType); + + request.Content.Should().BeEquivalentTo(content); + request.Filename.Should().Be(filename); + request.ContentType.Should().Be(contentType); + request.Purpose.Should().Be("user_upload"); + } + + [Fact] + public void Constructor_WhenContentIsNull_ItShouldThrowArgumentNullException() + { + var act = () => new FileRequest(null!, "example.txt", "text/plain"); + + act.Should().Throw().WithParameterName("content"); + } + + [Fact] + public void Constructor_WhenFilenameIsNull_ItShouldThrowArgumentException() + { + var content = "Hello World"u8.ToArray(); + + var act = () => new FileRequest(content, null!, "text/plain"); + + act.Should().Throw().WithParameterName("filename"); + } + + [Fact] + public void Constructor_WhenFilenameIsEmpty_ItShouldThrowArgumentException() + { + var content = "Hello World"u8.ToArray(); + + var act = () => new FileRequest(content, "", "text/plain"); + + act.Should().Throw().WithParameterName("filename"); + } + + [Fact] + public void Constructor_WhenContentTypeIsNull_ItShouldThrowArgumentException() + { + var content = "Hello World"u8.ToArray(); + + var act = () => new FileRequest(content, "example.txt", null!); + + act.Should().Throw().WithParameterName("contentType"); + } + + [Fact] + public void Constructor_WhenContentTypeIsEmpty_ItShouldThrowArgumentException() + { + var content = "Hello World"u8.ToArray(); + + var act = () => new FileRequest(content, "example.txt", ""); + + act.Should().Throw().WithParameterName("contentType"); + } + + [Fact] + public void Constructor_WhenPurposeIsNull_ItShouldThrowArgumentException() + { + var content = "Hello World"u8.ToArray(); + + var act = () => new FileRequest(content, "example.txt", "text/plain", null!); + + act.Should().Throw().WithParameterName("purpose"); + } + + [Fact] + public void Constructor_WhenPurposeIsEmpty_ItShouldThrowArgumentException() + { + var content = "Hello World"u8.ToArray(); + + var act = () => new FileRequest(content, "example.txt", "text/plain", ""); + + act.Should().Throw().WithParameterName("purpose"); + } +} \ No newline at end of file