22 Commits
Author SHA1 Message Date
Stevan Freeborn 071c9c133b chore(release): 1.1.0 [skip ci] 2025-07-15 04:30:23 +00:00
Stevan Freeborn b859859548 feat: add missing model constants 2025-07-14 23:30:02 -05:00
Stevan Freeborn 21ce029f09 chore: run dotnet format 2025-07-14 23:30:02 -05:00
Stevan Freeborn 723476b18f tests: add test for file source constructor with id 2025-07-14 23:30:02 -05:00
Stevan Freeborn 1f83899eb8 feat: add support for file source and url source 2025-07-14 23:30:02 -05:00
Stevan Freeborn 267972ca94 tests: remove unnecessary reason string 2025-07-14 23:30:02 -05:00
Stevan Freeborn 7657e661d0 docs: update files api section with examples for each method 2025-07-14 23:30:02 -05:00
Stevan Freeborn b48baffb60 fix: use sync copy to method 2025-07-14 23:30:02 -05:00
Stevan Freeborn ad3b990138 chore: run dotnet format 2025-07-14 23:30:02 -05:00
Stevan Freeborn ea4c8230bc refactor: remove unnecessary if checks and add test to handle getting null file 2025-07-14 23:30:02 -05:00
Stevan Freeborn 0aaf6e0995 tests: add tests for sad path in new files methods 2025-07-14 23:30:02 -05:00
Stevan Freeborn d1e88a52ac feat: implement GetFileInfoAsync, GetFileAsync, and DeleteFileAsync 2025-07-14 23:30:02 -05:00
Stevan Freeborn 9cb8443f94 feat: implement list all files method 2025-07-14 23:30:02 -05:00
Stevan Freeborn 9d5c620167 feat: implement listing a page of files 2025-07-14 23:30:02 -05:00
Stevan Freeborn c212ebffcd tests: add tests for anthropic file model 2025-07-14 23:30:02 -05:00
Stevan Freeborn 35c4147379 docs: add note about files beta status 2025-07-14 23:30:02 -05:00
Stevan Freeborn e52540eec3 tests: add end to end test 2025-07-14 23:30:02 -05:00
Stevan Freeborn b26e663960 tests: add integration test for creating file 2025-07-14 23:30:02 -05:00
Stevan Freeborn f4ffcf5fbc docs: fix xml comments 2025-07-14 23:30:02 -05:00
Stevan Freeborn 914495ab97 tests: add create file request tests 2025-07-14 23:30:02 -05:00
Stevan Freeborn 1cad19d9c6 fix: remove unnecessary usings 2025-07-14 23:30:02 -05:00
Stevan Freeborn e674b9afe6 feat: initial implementation of creating a file via the Files API 2025-07-14 23:30:02 -05:00
36 changed files with 1676 additions and 686 deletions
+144
View File
@@ -201,6 +201,150 @@ if (response.IsFailure)
Console.WriteLine("Model Id: {0}", response.Value.Id); Console.WriteLine("Model Id: {0}", response.Value.Id);
``` ```
### Files API
The `AnthropicApiClient` provides support for the Anthropic Files API, which allows you to upload and manage files for use with the Anthropic API.
> [!NOTE]
> The Files API is currently in beta. To use the Files API, youll need to include the beta feature header: `anthropic-beta: files-api-2025-04-14`
#### Create a File
You can create a file using the Files API in several ways:
##### From a Byte Array
```csharp
var fileBytes = await File.ReadAllBytesAsync("path/to/file.txt");
var request = new CreateFileRequest(fileBytes, "file.txt", "text/plain");
var result = await client.CreateFileAsync(request);
if (result.IsSuccess)
{
var file = result.Value;
Console.WriteLine($"Created file: {file.Name} (ID: {file.Id})");
}
```
##### From a Stream
```csharp
using var fileStream = File.OpenRead("path/to/file.txt");
var request = new CreateFileRequest(fileStream, "file.txt", "text/plain");
var result = await client.CreateFileAsync(request);
if (result.IsSuccess)
{
var file = result.Value;
Console.WriteLine($"Created file: {file.Name} (ID: {file.Id})");
}
```
#### List Files
You can list files in your account using pagination:
##### Single Page
```csharp
var result = await client.ListFilesAsync();
if (result.IsSuccess)
{
var page = result.Value;
Console.WriteLine($"Found {page.Data.Count} files");
foreach (var file in page.Data)
{
Console.WriteLine($"- {file.Name} (ID: {file.Id}, Size: {file.Size} bytes)");
}
if (page.HasMore)
{
Console.WriteLine("More files available...");
}
}
```
##### With Pagination Options
```csharp
var pagingRequest = new PagingRequest(afterId: "file_12345", limit: 10);
var result = await client.ListFilesAsync(pagingRequest);
```
##### All Files (Multiple Pages)
```csharp
await foreach (var pageResult in client.ListAllFilesAsync(limit: 20))
{
if (pageResult.IsSuccess)
{
var page = pageResult.Value;
foreach (var file in page.Data)
{
Console.WriteLine($"- {file.Name} (ID: {file.Id})");
}
}
}
```
#### Get File Information
Retrieve metadata about a specific file:
```csharp
var result = await client.GetFileInfoAsync("file_12345");
if (result.IsSuccess)
{
var file = result.Value;
Console.WriteLine($"File: {file.Name}");
Console.WriteLine($"ID: {file.Id}");
Console.WriteLine($"MIME Type: {file.MimeType}");
Console.WriteLine($"Size: {file.Size} bytes");
Console.WriteLine($"Created: {file.CreatedAt}");
Console.WriteLine($"Downloadable: {file.Downloadable}");
}
```
#### Get File Content
Download the content of a file as a stream:
```csharp
var result = await client.GetFileAsync("file_12345");
if (result.IsSuccess)
{
using var contentStream = result.Value;
using var reader = new StreamReader(contentStream);
var content = await reader.ReadToEndAsync();
Console.WriteLine("File content:");
Console.WriteLine(content);
}
```
#### Delete a File
Remove a file from your account:
```csharp
var result = await client.DeleteFileAsync("file_12345");
if (result.IsSuccess)
{
var deleteResponse = result.Value;
Console.WriteLine($"Deleted file: {deleteResponse.Id}");
Console.WriteLine($"Type: {deleteResponse.Type}");
}
```
> [!NOTE]
> The Files API has certain limitations on file size, supported file types, and usage quotas. Please refer to the [Anthropic API Documentation](https://docs.anthropic.com/en/docs/build-with-claude/files) for the most up-to-date information on these limitations.
### Create a message ### Create a message
The `AnthropicApiClient` exposes a method named `CreateMessageAsync` that can be used to create a message. The method requires a `MessageRequest` or a `StreamMessageRequest` instance as a parameter. The `MessageRequest` class is used to create a message whose response is not streamed and the `StreamMessageRequest` class is used to create a message whose response is streamed. The `MessageRequest` instance's properties can be set to configure how the message is created. The `AnthropicApiClient` exposes a method named `CreateMessageAsync` that can be used to create a message. The method requires a `MessageRequest` or a `StreamMessageRequest` instance as a parameter. The `MessageRequest` class is used to create a message whose response is not streamed and the `StreamMessageRequest` class is used to create a message whose response is streamed. The `MessageRequest` instance's properties can be set to configure how the message is created.
-89
View File
@@ -1,89 +0,0 @@
using AnthropicClient;
using AnthropicClient.Models;
namespace AnthropicClient.Examples;
/// <summary>
/// Example demonstrating file operations with the Anthropic API.
/// </summary>
public class FileOperationsExample
{
public static async Task RunExample()
{
// This is a demonstration of the file API methods
// Note: You would need a real API key to run this example
var client = new AnthropicApiClient("your-api-key", new HttpClient());
// Create a file
var fileContent = "Hello, this is a sample file content!"u8.ToArray();
var fileRequest = new FileRequest(fileContent, "sample.txt", "text/plain");
Console.WriteLine("Creating file...");
var createResult = await client.CreateFileAsync(fileRequest);
if (createResult.IsFailure)
{
Console.WriteLine($"Failed to create file: {createResult.Error.Error.Message}");
return;
}
var fileId = createResult.Value.Id;
Console.WriteLine($"File created with ID: {fileId}");
// List files
Console.WriteLine("\nListing files...");
var listResult = await client.ListFilesAsync();
if (listResult.IsSuccess)
{
Console.WriteLine($"Found {listResult.Value.Data.Length} files");
foreach (var file in listResult.Value.Data)
{
Console.WriteLine($"- {file.Filename} ({file.Id})");
}
}
// Get file metadata
Console.WriteLine($"\nGetting file metadata for {fileId}...");
var getResult = await client.GetFileAsync(fileId);
if (getResult.IsSuccess)
{
var file = getResult.Value;
Console.WriteLine($"File: {file.Filename}");
Console.WriteLine($"Size: {file.SizeBytes} bytes");
Console.WriteLine($"Content Type: {file.ContentType}");
Console.WriteLine($"Created: {file.CreatedAt}");
}
// Download file
Console.WriteLine($"\nDownloading file {fileId}...");
var downloadResult = await client.DownloadFileAsync(fileId);
if (downloadResult.IsSuccess)
{
var download = downloadResult.Value;
var contentText = System.Text.Encoding.UTF8.GetString(download.Content);
Console.WriteLine($"Downloaded content: {contentText}");
}
// List all files with pagination
Console.WriteLine("\nListing all files (with pagination)...");
await foreach (var pageResult in client.ListAllFilesAsync(limit: 10))
{
if (pageResult.IsSuccess)
{
Console.WriteLine($"Page with {pageResult.Value.Data.Length} files");
}
}
// Delete file
Console.WriteLine($"\nDeleting file {fileId}...");
var deleteResult = await client.DeleteFileAsync(fileId);
if (deleteResult.IsSuccess)
{
Console.WriteLine($"File deleted: {deleteResult.Value.Deleted}");
}
}
}
+22 -37
View File
@@ -382,18 +382,9 @@ public class AnthropicApiClient : IAnthropicApiClient
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<AnthropicResult<AnthropicFile>> CreateFileAsync(FileRequest request, CancellationToken cancellationToken = default) public async Task<AnthropicResult<AnthropicFile>> CreateFileAsync(CreateFileRequest request, CancellationToken cancellationToken = default)
{ {
var formData = new MultipartFormDataContent(); var response = await SendFileRequestAsync(FilesEndpoint, request, cancellationToken);
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<AnthropicFile>(response); return await CreateResultAsync<AnthropicFile>(response);
} }
@@ -416,7 +407,7 @@ public class AnthropicApiClient : IAnthropicApiClient
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<AnthropicResult<AnthropicFile>> GetFileAsync(string fileId, CancellationToken cancellationToken = default) public async Task<AnthropicResult<AnthropicFile>> GetFileInfoAsync(string fileId, CancellationToken cancellationToken = default)
{ {
var endpoint = $"{FilesEndpoint}/{fileId}"; var endpoint = $"{FilesEndpoint}/{fileId}";
var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken); var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken);
@@ -424,34 +415,28 @@ public class AnthropicApiClient : IAnthropicApiClient
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<AnthropicResult<FileDownloadResponse>> DownloadFileAsync(string fileId, CancellationToken cancellationToken = default) public async Task<AnthropicResult<Stream>> GetFileAsync(string fileId, CancellationToken cancellationToken = default)
{ {
var endpoint = $"{FilesEndpoint}/{fileId}/content"; var endpoint = $"{FilesEndpoint}/{fileId}/content";
var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken); var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken);
var anthropicHeaders = new AnthropicHeaders(response.Headers);
if (response.IsSuccessStatusCode is false) if (response.IsSuccessStatusCode is false)
{ {
var errorContent = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
var error = Deserialize<AnthropicError>(errorContent) ?? new AnthropicError(); var error = Deserialize<AnthropicError>(content) ?? new AnthropicError();
return AnthropicResult<FileDownloadResponse>.Failure(error, anthropicHeaders); return AnthropicResult<Stream>.Failure(error, new AnthropicHeaders(response.Headers));
} }
var content = await response.Content.ReadAsByteArrayAsync(); var stream = await response.Content.ReadAsStreamAsync();
var contentType = response.Content.Headers.ContentType?.MediaType ?? "application/octet-stream"; return AnthropicResult<Stream>.Success(stream, new AnthropicHeaders(response.Headers));
var filename = ExtractFilenameFromContentDisposition(response.Content.Headers.ContentDisposition?.FileName) ?? fileId;
var sizeBytes = content.Length;
var downloadResponse = new FileDownloadResponse(content, filename, contentType, sizeBytes);
return AnthropicResult<FileDownloadResponse>.Success(downloadResponse, anthropicHeaders);
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<AnthropicResult<FileDeleteResponse>> DeleteFileAsync(string fileId, CancellationToken cancellationToken = default) public async Task<AnthropicResult<AnthropicFileDeleteResponse>> DeleteFileAsync(string fileId, CancellationToken cancellationToken = default)
{ {
var endpoint = $"{FilesEndpoint}/{fileId}"; var endpoint = $"{FilesEndpoint}/{fileId}";
var response = await SendRequestAsync(endpoint, HttpMethod.Delete, cancellationToken); var response = await SendRequestAsync(endpoint, HttpMethod.Delete, cancellationToken);
return await CreateResultAsync<FileDeleteResponse>(response); return await CreateResultAsync<AnthropicFileDeleteResponse>(response);
} }
private async IAsyncEnumerable<AnthropicResult<Page<T>>> GetAllPagesAsync<T>(string endpoint, int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default) private async IAsyncEnumerable<AnthropicResult<Page<T>>> GetAllPagesAsync<T>(string endpoint, int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default)
@@ -523,17 +508,6 @@ public class AnthropicApiClient : IAnthropicApiClient
return AnthropicResult<T>.Success(model, anthropicHeaders); return AnthropicResult<T>.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<HttpResponseMessage> SendRequestAsync(string endpoint, HttpMethod? method = null, CancellationToken cancellationToken = default) private async Task<HttpResponseMessage> SendRequestAsync(string endpoint, HttpMethod? method = null, CancellationToken cancellationToken = default)
{ {
var request = new HttpRequestMessage(method ?? HttpMethod.Get, endpoint); var request = new HttpRequestMessage(method ?? HttpMethod.Get, endpoint);
@@ -547,6 +521,17 @@ public class AnthropicApiClient : IAnthropicApiClient
return await _httpClient.PostAsync(endpoint, requestContent, cancellationToken); return await _httpClient.PostAsync(endpoint, requestContent, cancellationToken);
} }
private async Task<HttpResponseMessage> SendFileRequestAsync(string endpoint, CreateFileRequest request, CancellationToken cancellationToken = default)
{
using var multipartContent = new MultipartFormDataContent();
using var fileContent = new ByteArrayContent(request.File);
fileContent.Headers.ContentType = new MediaTypeHeaderValue(request.FileType);
multipartContent.Add(fileContent, "file", request.FileName);
return await _httpClient.PostAsync(endpoint, multipartContent, cancellationToken);
}
private string Serialize<T>(T obj) => JsonSerializer.Serialize(obj, JsonSerializationOptions.DefaultOptions); private string Serialize<T>(T obj) => JsonSerializer.Serialize(obj, JsonSerializationOptions.DefaultOptions);
private T? Deserialize<T>(string json) => JsonSerializer.Deserialize<T>(json, JsonSerializationOptions.DefaultOptions); private T? Deserialize<T>(string json) => JsonSerializer.Deserialize<T>(json, JsonSerializationOptions.DefaultOptions);
} }
+1 -1
View File
@@ -4,7 +4,7 @@
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<PackageId>AnthropicClient</PackageId> <PackageId>AnthropicClient</PackageId>
<Version>1.0.0</Version> <Version>1.1.0</Version>
<Authors>Stevan Freeborn</Authors> <Authors>Stevan Freeborn</Authors>
<Description>Anthropic Client Library</Description> <Description>Anthropic Client Library</Description>
<PackageProjectUrl>https://anthropicclient.stevanfreeborn.com/</PackageProjectUrl> <PackageProjectUrl>https://anthropicclient.stevanfreeborn.com/</PackageProjectUrl>
+17
View File
@@ -2,6 +2,23 @@
All notable changes to this project will be documented in this file. See [versionize](https://github.com/versionize/versionize) for commit guidelines. All notable changes to this project will be documented in this file. See [versionize](https://github.com/versionize/versionize) for commit guidelines.
<a name="1.1.0"></a>
## [1.1.0](https://www.github.com/StevanFreeborn/anthropic-client/releases/tag/v1.1.0) (2025-07-15)
### Features
* add missing model constants ([b859859](https://www.github.com/StevanFreeborn/anthropic-client/commit/b8598595484739027bdd83cd8cdfe122ad3cb9cf))
* add support for file source and url source ([1f83899](https://www.github.com/StevanFreeborn/anthropic-client/commit/1f83899eb88b9ee9d87d246158defd7cbde9b01e))
* implement `GetFileInfoAsync`, `GetFileAsync`, and `DeleteFileAsync` ([d1e88a5](https://www.github.com/StevanFreeborn/anthropic-client/commit/d1e88a52ace603a5c017cca3bf48953846d7b271))
* implement list all files method ([9cb8443](https://www.github.com/StevanFreeborn/anthropic-client/commit/9cb8443f94d2086192700aeb1f2dc977a86cb752))
* implement listing a page of files ([9d5c620](https://www.github.com/StevanFreeborn/anthropic-client/commit/9d5c6201674bfb0923a0b0d70530286980ebdf62))
* initial implementation of creating a file via the Files API ([e674b9a](https://www.github.com/StevanFreeborn/anthropic-client/commit/e674b9afe693143841a168c8356ef93079445db0))
### Bug Fixes
* remove unnecessary usings ([1cad19d](https://www.github.com/StevanFreeborn/anthropic-client/commit/1cad19d9c6c4418fef3c206f700ca14b7d250e26))
* use sync copy to method ([b48baff](https://www.github.com/StevanFreeborn/anthropic-client/commit/b48baffb60a8acc8b89a60e53589bc04362a9505))
<a name="1.0.0"></a> <a name="1.0.0"></a>
## [1.0.0](https://www.github.com/StevanFreeborn/anthropic-client/releases/tag/v1.0.0) (2025-07-09) ## [1.0.0](https://www.github.com/StevanFreeborn/anthropic-client/releases/tag/v1.0.0) (2025-07-09)
+12 -11
View File
@@ -113,12 +113,12 @@ public interface IAnthropicApiClient
Task<AnthropicResult<AnthropicModel>> GetModelAsync(string modelId, CancellationToken cancellationToken = default); Task<AnthropicResult<AnthropicModel>> GetModelAsync(string modelId, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Creates a file asynchronously. /// Creates a file asynchronously using the Files API.
/// </summary> /// </summary>
/// <param name="request">The file request to create.</param> /// <param name="request">The file creation request.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="AnthropicFile"/>.</returns> /// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="AnthropicFile"/>.</returns>
Task<AnthropicResult<AnthropicFile>> CreateFileAsync(FileRequest request, CancellationToken cancellationToken = default); Task<AnthropicResult<AnthropicFile>> CreateFileAsync(CreateFileRequest request, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Lists files asynchronously, returning a single page of results. /// Lists files asynchronously, returning a single page of results.
@@ -137,26 +137,27 @@ public interface IAnthropicApiClient
IAsyncEnumerable<AnthropicResult<Page<AnthropicFile>>> ListAllFilesAsync(int limit = 20, CancellationToken cancellationToken = default); IAsyncEnumerable<AnthropicResult<Page<AnthropicFile>>> ListAllFilesAsync(int limit = 20, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Gets a file by its ID asynchronously. /// Gets a file's metadata by its ID asynchronously.
/// </summary> /// </summary>
/// <param name="fileId">The ID of the file to get.</param> /// <param name="fileId">The ID of the file to get.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="AnthropicFile"/>.</returns> /// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="AnthropicFile"/>.</returns>
Task<AnthropicResult<AnthropicFile>> GetFileAsync(string fileId, CancellationToken cancellationToken = default); Task<AnthropicResult<AnthropicFile>> GetFileInfoAsync(string fileId, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Downloads a file by its ID asynchronously. /// Gets a file's content by its ID asynchronously.
/// </summary> /// </summary>
/// <param name="fileId">The ID of the file to download.</param> /// <param name="fileId">The ID of the file to get the content for.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="FileDownloadResponse"/>.</returns> /// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is a stream containing the file content.</returns>
Task<AnthropicResult<FileDownloadResponse>> DownloadFileAsync(string fileId, CancellationToken cancellationToken = default); Task<AnthropicResult<Stream>> GetFileAsync(string fileId, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Deletes a file by its ID asynchronously. /// Deletes a file by its ID asynchronously.
/// </summary> /// </summary>
/// <param name="fileId">The ID of the file to delete.</param> /// <param name="fileId">The ID of the file to delete.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="FileDeleteResponse"/>.</returns> /// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="AnthropicFileDeleteResponse"/>.</returns>
Task<AnthropicResult<FileDeleteResponse>> DeleteFileAsync(string fileId, CancellationToken cancellationToken = default); Task<AnthropicResult<AnthropicFileDeleteResponse>> DeleteFileAsync(string fileId, CancellationToken cancellationToken = default);
} }
@@ -16,6 +16,8 @@ class SourceConverter : JsonConverter<Source>
{ {
SourceType.Text => JsonSerializer.Deserialize<TextSource>(root.GetRawText(), options)!, SourceType.Text => JsonSerializer.Deserialize<TextSource>(root.GetRawText(), options)!,
SourceType.Content => JsonSerializer.Deserialize<CustomSource>(root.GetRawText(), options)!, SourceType.Content => JsonSerializer.Deserialize<CustomSource>(root.GetRawText(), options)!,
SourceType.File => JsonSerializer.Deserialize<FileSource>(root.GetRawText(), options)!,
SourceType.Url => JsonSerializer.Deserialize<UrlSource>(root.GetRawText(), options)!,
SourceType.Base64 => DeserializeBase64Source(root, options), SourceType.Base64 => DeserializeBase64Source(root, options),
_ => throw new JsonException($"Unknown source type: {type}") _ => throw new JsonException($"Unknown source type: {type}")
}; };
@@ -54,6 +56,18 @@ class SourceConverter : JsonConverter<Source>
return; return;
} }
if (value is FileSource fileSource)
{
JsonSerializer.Serialize(writer, fileSource, options);
return;
}
if (value is UrlSource urlSource)
{
JsonSerializer.Serialize(writer, urlSource, options);
return;
}
JsonSerializer.Serialize(writer, value, value.GetType(), options); JsonSerializer.Serialize(writer, value, value.GetType(), options);
} }
} }
+28 -19
View File
@@ -3,40 +3,49 @@ using System.Text.Json.Serialization;
namespace AnthropicClient.Models; namespace AnthropicClient.Models;
/// <summary> /// <summary>
/// Represents a file in the Anthropic API. /// Represents a file object from the Anthropic Files API.
/// </summary> /// </summary>
public class AnthropicFile public class AnthropicFile
{ {
/// <summary> /// <summary>
/// The type of the object. /// Unique object identifier.
/// </summary>
public string Type { get; init; } = "file";
/// <summary>
/// The unique identifier for the file.
/// </summary> /// </summary>
[JsonPropertyName("id")]
public string Id { get; init; } = string.Empty; public string Id { get; init; } = string.Empty;
/// <summary> /// <summary>
/// The filename of the file. /// Object type.
/// </summary> /// </summary>
public string Filename { get; init; } = string.Empty; [JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
/// <summary> /// <summary>
/// The MIME type of the file. /// Original filename of the uploaded file.
/// </summary> /// </summary>
[JsonPropertyName("content_type")] [JsonPropertyName("filename")]
public string ContentType { get; init; } = string.Empty; public string Name { get; init; } = string.Empty;
/// <summary> /// <summary>
/// The size of the file in bytes. /// Date file was created.
/// </summary>
[JsonPropertyName("size_bytes")]
public int SizeBytes { get; init; }
/// <summary>
/// The date and time when the file was created.
/// </summary> /// </summary>
[JsonPropertyName("created_at")] [JsonPropertyName("created_at")]
public DateTimeOffset CreatedAt { get; init; } public DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// Size of the file in bytes.
/// </summary>
[JsonPropertyName("size_bytes")]
public long Size { get; init; }
/// <summary>
/// MIME type of the file.
/// </summary>
[JsonPropertyName("mime_type")]
public string MimeType { get; init; } = string.Empty;
/// <summary>
/// Whether the file can be downloaded.
/// </summary>
[JsonPropertyName("downloadable")]
public bool Downloadable { get; init; }
} }
@@ -0,0 +1,17 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents the response from deleting a file in the Anthropic API.
/// </summary>
public class AnthropicFileDeleteResponse
{
/// <summary>
/// Gets or sets the ID of the file that was deleted.
/// </summary>
public string Id { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the response type
/// </summary>
public string Type { get; init; } = string.Empty;
}
@@ -20,6 +20,16 @@ public static class AnthropicModels
/// </summary> /// </summary>
public const string Claude3OpusLatest = "claude-3-opus-latest"; public const string Claude3OpusLatest = "claude-3-opus-latest";
/// <summary>
/// The Claude 4 Opus model.
/// </summary>
public const string ClaudeOpus420250514 = "claude-opus-4-20250514";
/// <summary>
/// The Claude 4 Opus model.
/// </summary>
public const string ClaudeOpus40 = "claude-opus-4-0";
/// <summary> /// <summary>
/// The Claude 3 Sonnet model. /// The Claude 3 Sonnet model.
/// </summary> /// </summary>
@@ -50,6 +60,26 @@ public static class AnthropicModels
/// </summary> /// </summary>
public const string Claude35SonnetLatest = "claude-3-5-sonnet-latest"; public const string Claude35SonnetLatest = "claude-3-5-sonnet-latest";
/// <summary>
/// The Claude 3 Sonnet model
/// </summary>
public const string Claude37Sonnet20250219 = "claude-3-7-sonnet-20250219";
/// <summary>
/// The Claude 3 Sonnet model
/// </summary>
public const string Claude37SonnetLatest = "claude-3-7-sonnet-latest";
/// <summary>
/// The Claude 4 Sonnet model.
/// </summary>
public const string ClaudeSonnet420250514 = "claude-sonnet-4-20250514";
/// <summary>
/// The Claude 4 Sonnet model.
/// </summary>
public const string ClaudeSonnet40 = "claude-sonnet-4-0";
/// <summary> /// <summary>
/// The Claude 3 Haiku model. /// The Claude 3 Haiku model.
/// </summary> /// </summary>
@@ -0,0 +1,66 @@
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a request to create a file via the Anthropic Files API.
/// </summary>
public class CreateFileRequest
{
/// <summary>
/// The file content as a byte array.
/// </summary>
public byte[] File { get; init; }
/// <summary>
/// The original filename of the file being uploaded.
/// </summary>
public string FileName { get; init; }
/// <summary>
/// The MIME type of the file.
/// </summary>
public string FileType { get; init; }
/// <summary>
/// Initializes a new instance of the <see cref="CreateFileRequest"/> class.
/// </summary>
/// <param name="file">The file content as a byte array.</param>
/// <param name="fileName">The original filename of the file being uploaded.</param>
/// <param name="fileType">The MIME type of the file.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="file"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="fileName"/> or <paramref name="fileType"/> is null or whitespace.</exception>
public CreateFileRequest(byte[] file, string fileName, string fileType)
{
ArgumentValidator.ThrowIfNull(file, nameof(file));
ArgumentValidator.ThrowIfNullOrWhitespace(fileName, nameof(fileName));
ArgumentValidator.ThrowIfNullOrWhitespace(fileType, nameof(fileType));
File = file;
FileName = fileName;
FileType = fileType;
}
/// <summary>
/// Initializes a new instance of the <see cref="CreateFileRequest"/> class from a stream.
/// </summary>
/// <param name="stream">The stream containing the file content.</param>
/// <param name="fileName">The original filename of the file being uploaded.</param>
/// <param name="fileType">The MIME type of the file.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="fileName"/> or <paramref name="fileType"/> is null or whitespace.</exception>
public CreateFileRequest(Stream stream, string fileName, string fileType)
{
ArgumentValidator.ThrowIfNull(stream, nameof(stream));
ArgumentValidator.ThrowIfNullOrWhitespace(fileName, nameof(fileName));
ArgumentValidator.ThrowIfNullOrWhitespace(fileType, nameof(fileType));
using var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
var fileContent = memoryStream.ToArray();
File = fileContent;
FileName = fileName;
FileType = fileType;
}
}
@@ -1,7 +1,5 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using AnthropicClient.Utils;
namespace AnthropicClient.Models; namespace AnthropicClient.Models;
/// <summary> /// <summary>
@@ -1,24 +0,0 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents the response from a file deletion operation.
/// </summary>
public class FileDeleteResponse
{
/// <summary>
/// The type of the object.
/// </summary>
public string Type { get; init; } = "file_deleted";
/// <summary>
/// The unique identifier for the deleted file.
/// </summary>
public string Id { get; init; } = string.Empty;
/// <summary>
/// Indicates whether the file was successfully deleted.
/// </summary>
public bool Deleted { get; init; }
}
@@ -1,42 +0,0 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents the response from downloading a file.
/// </summary>
public class FileDownloadResponse
{
/// <summary>
/// The file content as a byte array.
/// </summary>
public byte[] Content { get; }
/// <summary>
/// The filename of the file.
/// </summary>
public string Filename { get; }
/// <summary>
/// The MIME type of the file.
/// </summary>
public string ContentType { get; }
/// <summary>
/// The size of the file in bytes.
/// </summary>
public int SizeBytes { get; }
/// <summary>
/// Initializes a new instance of the <see cref="FileDownloadResponse"/> class.
/// </summary>
/// <param name="content">The file content as a byte array.</param>
/// <param name="filename">The filename of the file.</param>
/// <param name="contentType">The MIME type of the file.</param>
/// <param name="sizeBytes">The size of the file in bytes.</param>
public FileDownloadResponse(byte[] content, string filename, string contentType, int sizeBytes)
{
Content = content;
Filename = filename;
ContentType = contentType;
SizeBytes = sizeBytes;
}
}
-51
View File
@@ -1,51 +0,0 @@
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a request to create a file.
/// </summary>
public class FileRequest
{
/// <summary>
/// The file content as a byte array.
/// </summary>
public byte[] Content { get; }
/// <summary>
/// The filename of the file.
/// </summary>
public string Filename { get; }
/// <summary>
/// The MIME type of the file.
/// </summary>
public string ContentType { get; }
/// <summary>
/// The purpose of the file.
/// </summary>
public string Purpose { get; }
/// <summary>
/// Initializes a new instance of the <see cref="FileRequest"/> class.
/// </summary>
/// <param name="content">The file content as a byte array.</param>
/// <param name="filename">The filename of the file.</param>
/// <param name="contentType">The MIME type of the file.</param>
/// <param name="purpose">The purpose of the file (default: "user_upload").</param>
/// <exception cref="ArgumentNullException">Thrown when content, filename, or contentType is null.</exception>
/// <exception cref="ArgumentException">Thrown when filename or contentType is empty.</exception>
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;
}
}
+33
View File
@@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a file source in the Anthropic API.
/// </summary>
public class FileSource : Source
{
/// <summary>
/// Gets or sets the unique identifier for the file source.
/// </summary>
[JsonPropertyName("file_id")]
public string Id { get; init; } = string.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="FileSource"/> class.
/// </summary>
/// <returns>A new instance of <see cref="FileSource"/> with the type set to "file".</returns>
public FileSource() : base(SourceType.File)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="FileSource"/> class with a specified file ID.
/// </summary>
/// <param name="id">The unique identifier for the file source.</param>
/// <returns>A new instance of <see cref="FileSource"/>.</returns>
public FileSource(string id) : base(SourceType.File)
{
Id = id;
}
}
@@ -53,4 +53,32 @@ public class ImageContent : Content
Source = new ImageSource(mediaType, data); Source = new ImageSource(mediaType, data);
} }
/// <summary>
/// Initializes a new instance of the <see cref="ImageContent"/> class.
/// </summary>
/// <param name="source">The source of the image.</param>
/// <returns>A new instance of the <see cref="ImageContent"/> class.</returns>
/// <exception cref="ArgumentNullException">Thrown when the source is null.</exception>
public ImageContent(Source source) : base(ContentType.Image)
{
ArgumentValidator.ThrowIfNull(source, nameof(source));
Source = source;
}
/// <summary>
/// Initializes a new instance of the <see cref="ImageContent"/> class.
/// </summary>
/// <param name="source">The source of the image.</param>
/// <param name="cacheControl">The cache control to be used for the content.</param>
/// <returns>A new instance of the <see cref="ImageContent"/> class.</returns>
/// <exception cref="ArgumentNullException">Thrown when the source or cache control is null.</exception>
public ImageContent(Source source, CacheControl cacheControl) : base(ContentType.Image, cacheControl)
{
ArgumentValidator.ThrowIfNull(source, nameof(source));
ArgumentValidator.ThrowIfNull(cacheControl, nameof(cacheControl));
Source = source;
}
} }
@@ -1,7 +1,5 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using AnthropicClient.Utils;
namespace AnthropicClient.Models; namespace AnthropicClient.Models;
/// <summary> /// <summary>
+10
View File
@@ -19,4 +19,14 @@ public static class SourceType
/// The text document source type. /// The text document source type.
/// </summary> /// </summary>
public const string Text = "text"; public const string Text = "text";
/// <summary>
/// The file document source type.
/// </summary>
public const string File = "file";
/// <summary>
/// The URL document source type.
/// </summary>
public const string Url = "url";
} }
+30
View File
@@ -0,0 +1,30 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents a URL source in the Anthropic API.
/// </summary>
public class UrlSource : Source
{
/// <summary>
/// Gets or sets the URL of the source document.
/// </summary>
public string Url { get; init; } = string.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="UrlSource"/> class.
/// </summary>
/// <returns>A new instance of <see cref="UrlSource"/> with the type set to "url".</returns>
public UrlSource() : base(SourceType.Url)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="UrlSource"/> class with a specified URL.
/// </summary>
/// <param name="url">The URL of the source document.</param>
/// <returns>A new instance of <see cref="UrlSource"/>.</returns>
public UrlSource(string url) : base(SourceType.Url)
{
Url = url;
}
}
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
@@ -2,8 +2,15 @@ using AnthropicClient.Tests.Files;
namespace AnthropicClient.Tests.EndToEnd; namespace AnthropicClient.Tests.EndToEnd;
public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture) public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture), IAsyncLifetime
{ {
private readonly List<string> _filesToDelete = [];
public Task InitializeAsync()
{
return Task.CompletedTask;
}
[Fact] [Fact]
public async Task CreateMessageAsync_WhenCalled_ItShouldReturnResponse() public async Task CreateMessageAsync_WhenCalled_ItShouldReturnResponse()
{ {
@@ -96,11 +103,41 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
text.Should().Contain("elephant"); text.Should().Contain("elephant");
} }
[Fact]
public async Task CreateMessageAsync_WhenImageIsSentAsUrl_ItShouldReturnResponse()
{
var request = new MessageRequest(
model: AnthropicModels.Claude3Haiku,
messages: [
new(MessageRole.User, [
new ImageContent(new UrlSource("https://ftp.stevanfreeborn.com/share/anthropic-client/ant.jpg")),
new TextContent("What is in this image?")
]),
]
);
var result = await _client.CreateMessageAsync(request);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType<MessageResponse>();
result.Value.Content.Should().NotBeNullOrEmpty();
var text = result.Value.Content.Aggregate("", static (acc, content) =>
{
if (content is TextContent textContent)
{
acc += textContent.Text;
}
return acc;
});
text.Should().Contain("ant");
}
[Fact] [Fact]
public async Task CreateMessageAsync_WhenSystemMessagesContainCacheControl_ItShouldUseCache() public async Task CreateMessageAsync_WhenSystemMessagesContainCacheControl_ItShouldUseCache()
{ {
var client = CreateClient(new HttpClient());
var storyPath = TestFileHelper.GetTestFilePath("story.txt"); var storyPath = TestFileHelper.GetTestFilePath("story.txt");
var storyText = await File.ReadAllTextAsync(storyPath); var storyText = await File.ReadAllTextAsync(storyPath);
@@ -127,7 +164,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content)); request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this story?")])); request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this story?")]));
var resultTwo = await client.CreateMessageAsync(request); var resultTwo = await _client.CreateMessageAsync(request);
resultTwo.IsSuccess.Should().BeTrue(); resultTwo.IsSuccess.Should().BeTrue();
resultTwo.Value.Should().BeOfType<MessageResponse>(); resultTwo.Value.Should().BeOfType<MessageResponse>();
@@ -138,8 +175,6 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
[Fact] [Fact]
public async Task CreateMessageAsync_WhenMessagesContainCacheControl_ItShouldUseCache() public async Task CreateMessageAsync_WhenMessagesContainCacheControl_ItShouldUseCache()
{ {
var client = CreateClient(new HttpClient());
var storyPath = TestFileHelper.GetTestFilePath("story.txt"); var storyPath = TestFileHelper.GetTestFilePath("story.txt");
var storyText = await File.ReadAllTextAsync(storyPath); var storyText = await File.ReadAllTextAsync(storyPath);
@@ -153,7 +188,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
] ]
); );
var resultOne = await client.CreateMessageAsync(request); var resultOne = await _client.CreateMessageAsync(request);
resultOne.IsSuccess.Should().BeTrue(); resultOne.IsSuccess.Should().BeTrue();
resultOne.Value.Should().BeOfType<MessageResponse>(); resultOne.Value.Should().BeOfType<MessageResponse>();
@@ -163,7 +198,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content)); request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this story?")])); request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this story?")]));
var resultTwo = await client.CreateMessageAsync(request); var resultTwo = await _client.CreateMessageAsync(request);
resultTwo.IsSuccess.Should().BeTrue(); resultTwo.IsSuccess.Should().BeTrue();
resultTwo.Value.Should().BeOfType<MessageResponse>(); resultTwo.Value.Should().BeOfType<MessageResponse>();
@@ -174,8 +209,6 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
[Fact] [Fact]
public async Task CreateMessageAsync_WhenToolsContainCacheControl_ItShouldUseCache() public async Task CreateMessageAsync_WhenToolsContainCacheControl_ItShouldUseCache()
{ {
var client = CreateClient(new HttpClient());
var func = (string ticker) => ticker; var func = (string ticker) => ticker;
var tools = Enumerable var tools = Enumerable
@@ -195,7 +228,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
tools: tools tools: tools
); );
var resultOne = await client.CreateMessageAsync(request); var resultOne = await _client.CreateMessageAsync(request);
resultOne.IsSuccess.Should().BeTrue(); resultOne.IsSuccess.Should().BeTrue();
resultOne.Value.Should().BeOfType<MessageResponse>(); resultOne.Value.Should().BeOfType<MessageResponse>();
@@ -205,7 +238,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content)); request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
request.Messages.Add(new(MessageRole.User, [new TextContent("Could you tell me the stock price for AAPL?")])); request.Messages.Add(new(MessageRole.User, [new TextContent("Could you tell me the stock price for AAPL?")]));
var resultTwo = await client.CreateMessageAsync(request); var resultTwo = await _client.CreateMessageAsync(request);
resultTwo.IsSuccess.Should().BeTrue(); resultTwo.IsSuccess.Should().BeTrue();
resultTwo.Value.Should().BeOfType<MessageResponse>(); resultTwo.Value.Should().BeOfType<MessageResponse>();
@@ -228,9 +261,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
] ]
); );
var client = CreateClient(new HttpClient()); var result = await _client.CreateMessageAsync(request);
var result = await client.CreateMessageAsync(request);
result.IsSuccess.Should().BeTrue(); result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType<MessageResponse>(); result.Value.Should().BeOfType<MessageResponse>();
@@ -256,8 +287,6 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
var bytes = await File.ReadAllBytesAsync(pdfPath); var bytes = await File.ReadAllBytesAsync(pdfPath);
var base64Data = Convert.ToBase64String(bytes); var base64Data = Convert.ToBase64String(bytes);
var client = CreateClient(new HttpClient());
var request = new MessageRequest( var request = new MessageRequest(
model: AnthropicModels.Claude35Sonnet, model: AnthropicModels.Claude35Sonnet,
messages: [ messages: [
@@ -268,7 +297,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
] ]
); );
var resultOne = await client.CreateMessageAsync(request); var resultOne = await _client.CreateMessageAsync(request);
resultOne.IsSuccess.Should().BeTrue(); resultOne.IsSuccess.Should().BeTrue();
resultOne.Value.Should().BeOfType<MessageResponse>(); resultOne.Value.Should().BeOfType<MessageResponse>();
@@ -278,7 +307,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content)); request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this paper?")])); request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this paper?")]));
var resultTwo = await client.CreateMessageAsync(request); var resultTwo = await _client.CreateMessageAsync(request);
resultTwo.IsSuccess.Should().BeTrue(); resultTwo.IsSuccess.Should().BeTrue();
resultTwo.Value.Should().BeOfType<MessageResponse>(); resultTwo.Value.Should().BeOfType<MessageResponse>();
@@ -395,6 +424,58 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
citations.OfType<ContentBlockLocationCitation>().Should().NotBeEmpty(); citations.OfType<ContentBlockLocationCitation>().Should().NotBeEmpty();
} }
[Fact]
public async Task CreateMessageAsync_WhenCitationsAreEnabledForFileSource_ItShouldReturnCitationsInResponse()
{
var fileName = "story.txt";
var fileType = "text/plain";
var filePath = TestFileHelper.GetTestFilePath("story.txt");
var fileContent = await File.ReadAllBytesAsync(filePath);
var createFileRequest = new CreateFileRequest(fileContent, fileName, fileType);
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
var client = CreateClient(httpClient);
var createdFile = await client.CreateFileAsync(createFileRequest);
var request = new MessageRequest(
model: AnthropicModels.Claude35HaikuLatest,
messages: [
new(
MessageRole.User,
[
new DocumentContent(new FileSource(createdFile.Value.Id))
{
Title = "A Story",
Context = "This is a trustworthy document.",
Citations = new() { Enabled = true }
},
new TextContent("Can you tell me what the title of this story is?"),
]
)
]
);
var result = await client.CreateMessageAsync(request);
result.IsSuccess.Should().BeTrue();
var textContents = result.Value.Content.OfType<TextContent>();
var messageContent = textContents.Aggregate(new StringBuilder(), (sb, content) =>
{
sb.Append(content.Text);
return sb;
});
messageContent.ToString().Should().MatchRegex("The Forgotten Lighthouse");
var citations = textContents.SelectMany(static c => c.Citations is null ? [] : c.Citations);
citations.OfType<CharacterLocationCitation>().Should().NotBeEmpty();
_filesToDelete.Add(createdFile.Value.Id);
}
[Fact] [Fact]
public async Task CreateMessageAsync_WhenStreamingAndCitationsAreEnabledForTextDocumentSource_ItShouldReturnCitationsInResponse() public async Task CreateMessageAsync_WhenStreamingAndCitationsAreEnabledForTextDocumentSource_ItShouldReturnCitationsInResponse()
{ {
@@ -516,6 +597,64 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
citations.OfType<ContentBlockLocationCitation>().Should().NotBeEmpty(); citations.OfType<ContentBlockLocationCitation>().Should().NotBeEmpty();
} }
[Fact]
public async Task CreateMessageAsync_WhenStreamingAndCitationsAreEnabledForFileSource_ItShouldReturnCitationsInResponse()
{
var fileName = "story.txt";
var fileType = "text/plain";
var filePath = TestFileHelper.GetTestFilePath("story.txt");
var fileContent = await File.ReadAllBytesAsync(filePath);
var createFileRequest = new CreateFileRequest(fileContent, fileName, fileType);
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
var client = CreateClient(httpClient);
var createdFile = await client.CreateFileAsync(createFileRequest);
var request = new StreamMessageRequest(
model: AnthropicModels.Claude35HaikuLatest,
messages: [
new(
MessageRole.User,
[
new DocumentContent(new FileSource(createdFile.Value.Id))
{
Title = "A Story",
Context = "This is a trustworthy document.",
Citations = new() { Enabled = true }
},
new TextContent("Can you tell me what the title of this story is?"),
]
)
]
);
var result = client.CreateMessageAsync(request);
var messageCompleteEvent = await result
.Where(e => e.Type is EventType.MessageComplete)
.FirstAsync();
var textContents = messageCompleteEvent.Data
.As<MessageCompleteEventData>()
.Message
.Content
.OfType<TextContent>();
var messageContent = textContents.Aggregate(new StringBuilder(), (sb, content) =>
{
sb.Append(content.Text);
return sb;
});
messageContent.ToString().Should().MatchRegex("The Forgotten Lighthouse");
var citations = textContents.SelectMany(static c => c.Citations is null ? [] : c.Citations);
citations.OfType<CharacterLocationCitation>().Should().NotBeEmpty();
_filesToDelete.Add(createdFile.Value.Id);
}
[Fact] [Fact]
public async Task CountMessageTokensAsync_WhenCalled_ItShouldReturnResponse() public async Task CountMessageTokensAsync_WhenCalled_ItShouldReturnResponse()
{ {
@@ -687,4 +826,120 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
result.Value.Id.Should().Be(createResult.Value.Id); result.Value.Id.Should().Be(createResult.Value.Id);
result.Value.ProcessingStatus.Should().Be(MessageBatchStatus.Canceling); result.Value.ProcessingStatus.Should().Be(MessageBatchStatus.Canceling);
} }
[Fact]
public async Task CreateFileAsync_WhenCalled_ItShouldReturnResponse()
{
var fileName = "story.txt";
var fileType = "text/plain";
var filePath = TestFileHelper.GetTestFilePath("story.txt");
var fileContent = await File.ReadAllBytesAsync(filePath);
var request = new CreateFileRequest(fileContent, fileName, fileType);
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
var client = CreateClient(httpClient);
var result = await client.CreateFileAsync(request);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType<AnthropicFile>();
result.Value.Name.Should().Be(fileName);
result.Value.MimeType.Should().Be(fileType);
_filesToDelete.Add(result.Value.Id);
}
[Fact]
public async Task ListFilesAsync_WhenCalled_ItShouldReturnPageOfFiles()
{
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
var client = CreateClient(httpClient);
var fileBytes = await File.ReadAllBytesAsync(TestFileHelper.GetTestFilePath("story.txt"));
var createFileRequest = new CreateFileRequest(fileBytes, "story.txt", "text/plain");
var createdFile = await client.CreateFileAsync(createFileRequest);
var result = await client.ListFilesAsync();
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType<Page<AnthropicFile>>();
result.Value.Data.Should().ContainSingle(f => f.Id == createdFile.Value.Id);
_filesToDelete.Add(createdFile.Value.Id);
}
[Fact]
public async Task ListAllFilesAsync_WhenCalled_ItShouldReturnAllFiles()
{
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
var client = CreateClient(httpClient);
var fileBytes = await File.ReadAllBytesAsync(TestFileHelper.GetTestFilePath("story.txt"));
var createFileRequest = new CreateFileRequest(fileBytes, "story.txt", "text/plain");
var createdFile = await client.CreateFileAsync(createFileRequest);
var responses = await client.ListAllFilesAsync(limit: 1).ToListAsync();
responses.Should().HaveCountGreaterThan(0);
responses.Select(r => r.Value)
.SelectMany(p => p.Data)
.Should()
.ContainSingle(f => f.Id == createdFile.Value.Id);
_filesToDelete.Add(createdFile.Value.Id);
}
[Fact]
public async Task GetFileInfoAsync_WhenCalled_ItShouldReturnFile()
{
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
var client = CreateClient(httpClient);
var fileBytes = await File.ReadAllBytesAsync(TestFileHelper.GetTestFilePath("story.txt"));
var createFileRequest = new CreateFileRequest(fileBytes, "story.txt", "text/plain");
var createdFile = await client.CreateFileAsync(createFileRequest);
var result = await client.GetFileInfoAsync(createdFile.Value.Id);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType<AnthropicFile>();
result.Value.Id.Should().Be(createdFile.Value.Id);
_filesToDelete.Add(createdFile.Value.Id);
}
[Fact]
public async Task DeleteFileAsync_WhenCalled_ItShouldReturnDeleteResponse()
{
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
var client = CreateClient(httpClient);
var fileBytes = await File.ReadAllBytesAsync(TestFileHelper.GetTestFilePath("story.txt"));
var createFileRequest = new CreateFileRequest(fileBytes, "story.txt", "text/plain");
var createdFile = await client.CreateFileAsync(createFileRequest);
var result = await client.DeleteFileAsync(createdFile.Value.Id);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType<AnthropicFileDeleteResponse>();
result.Value.Id.Should().Be(createdFile.Value.Id);
}
public async Task DisposeAsync()
{
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
var client = CreateClient(httpClient);
foreach (var file in _filesToDelete)
{
var result = await client.DeleteFileAsync(file);
result.IsSuccess.Should().BeTrue();
}
}
} }
@@ -1,164 +0,0 @@
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<AnthropicError>();
result.Error.Error.Should().BeOfType<InvalidRequestError>();
}
}
@@ -1928,4 +1928,476 @@ public class AnthropicApiClientTests : IntegrationTest
result.Value.Id.Should().Be(batchId); result.Value.Id.Should().Be(batchId);
result.Value.Type.Should().Be("message_batch_deleted"); result.Value.Type.Should().Be("message_batch_deleted");
} }
[Fact]
public async Task CreateFileAsync_WhenCalled_ItShouldReturnFile()
{
_mockHttpMessageHandler
.WhenCreateFileRequest()
.Respond(
HttpStatusCode.OK,
"application/json",
@"{
""created_at"": ""2023-11-07T05:31:56Z"",
""downloadable"": false,
""filename"": ""example.txt"",
""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"",
""mime_type"": ""text/plain"",
""size_bytes"": 1234,
""type"": ""file""
}"
);
var fileContent = new MemoryStream(Encoding.UTF8.GetBytes("Example file content"));
var request = new CreateFileRequest(fileContent, "example.txt", "text/plain");
var result = await Client.CreateFileAsync(request);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeEquivalentTo(new AnthropicFile()
{
CreatedAt = DateTimeOffset.Parse("2023-11-07T05:31:56Z"),
Downloadable = false,
Name = "example.txt",
Id = "file_013Zva2CMHLNnXjNJJKqJ2EF",
MimeType = "text/plain",
Size = 1234,
Type = "file"
});
}
[Fact]
public async Task CreateFileAsync_WhenCalledAndRequestFails_ItShouldReturnError()
{
_mockHttpMessageHandler
.WhenCreateFileRequest()
.Respond(
HttpStatusCode.BadRequest,
"application/json",
@"{
""type"": ""error"",
""error"": {
""type"": ""invalid_request_error"",
""message"": ""file: file size exceeds limit""
}
}"
);
var fileContent = new MemoryStream(Encoding.UTF8.GetBytes("Example file content"));
var request = new CreateFileRequest(fileContent, "example.txt", "text/plain");
var result = await Client.CreateFileAsync(request);
result.IsSuccess.Should().BeFalse();
result.Error.Should().BeOfType<AnthropicError>();
result.Error.Error.Should().BeOfType<InvalidRequestError>();
}
[Fact]
public async Task ListFilesAsync_WhenCalled_ItShouldReturnPageOfFiles()
{
_mockHttpMessageHandler
.WhenListFilesRequest()
.Respond(
HttpStatusCode.OK,
"application/json",
@"{
""data"": [
{
""created_at"": ""2023-11-07T05:31:56Z"",
""downloadable"": false,
""filename"": ""example.txt"",
""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"",
""mime_type"": ""text/plain"",
""size_bytes"": 1234,
""type"": ""file""
}
],
""has_more"": true,
""first_id"": ""1"",
""last_id"": ""1""
}"
);
var result = await Client.ListFilesAsync();
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType<Page<AnthropicFile>>();
result.Value.HasMore.Should().BeTrue();
result.Value.FirstId.Should().Be("1");
result.Value.LastId.Should().Be("1");
result.Value.Data.Should().BeEquivalentTo(new AnthropicFile[]
{
new()
{
CreatedAt = DateTimeOffset.Parse("2023-11-07T05:31:56Z"),
Downloadable = false,
Name = "example.txt",
Id = "file_013Zva2CMHLNnXjNJJKqJ2EF",
MimeType = "text/plain",
Size = 1234,
Type = "file"
}
});
}
[Fact]
public async Task ListFilesAsync_WhenCalledAndRequestFails_ItShouldReturnError()
{
_mockHttpMessageHandler
.WhenListFilesRequest()
.Respond(
HttpStatusCode.BadRequest,
"application/json",
@"{
""type"": ""error"",
""error"": {
""type"": ""invalid_request_error"",
""message"": ""files: file not found""
}
}"
);
var result = await Client.ListFilesAsync();
result.IsSuccess.Should().BeFalse();
result.Error.Should().BeOfType<AnthropicError>();
result.Error.Error.Should().BeOfType<InvalidRequestError>();
}
[Fact]
public async Task ListFilesAsync_WhenCalledWithPagingRequest_ItShouldReturnPageOfFiles()
{
var pagingRequest = new PagingRequest(afterId: "next_id", limit: 10);
_mockHttpMessageHandler
.WhenListFilesRequest()
.WithQueryString(new Dictionary<string, string>
{
{ "after_id", pagingRequest.AfterId },
{ "limit", pagingRequest.Limit.ToString() },
})
.Respond(
HttpStatusCode.OK,
"application/json",
@"{
""data"": [
{
""created_at"": ""2023-11-07T05:31:56Z"",
""downloadable"": false,
""filename"": ""example.txt"",
""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"",
""mime_type"": ""text/plain"",
""size_bytes"": 1234,
""type"": ""file""
}
],
""has_more"": true,
""first_id"": ""1"",
""last_id"": ""1""
}"
);
var result = await Client.ListFilesAsync(pagingRequest);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType<Page<AnthropicFile>>();
result.Value.HasMore.Should().BeTrue();
result.Value.FirstId.Should().Be("1");
result.Value.LastId.Should().Be("1");
result.Value.Data.Should().BeEquivalentTo(new AnthropicFile[]
{
new()
{
CreatedAt = DateTimeOffset.Parse("2023-11-07T05:31:56Z"),
Downloadable = false,
Name = "example.txt",
Id = "file_013Zva2CMHLNnXjNJJKqJ2EF",
MimeType = "text/plain",
Size = 1234,
Type = "file"
}
});
}
[Fact]
public async Task ListAllFilesAsync_WhenCalled_ItShouldReturnAllFiles()
{
_mockHttpMessageHandler
.WhenListFilesRequest()
.WithExactQueryString(new Dictionary<string, string>()
{
{ "limit", "20" },
})
.Respond(
HttpStatusCode.OK,
"application/json",
@"{
""data"": [
{
""created_at"": ""2023-11-07T05:31:56Z"",
""downloadable"": false,
""filename"": ""example.txt"",
""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"",
""mime_type"": ""text/plain"",
""size_bytes"": 1234,
""type"": ""file""
}
],
""has_more"": true,
""first_id"": ""1"",
""last_id"": ""1""
}"
);
_mockHttpMessageHandler
.WhenListFilesRequest()
.WithExactQueryString(new Dictionary<string, string>()
{
{ "after_id", "1" },
{ "limit", "20" },
})
.Respond(
HttpStatusCode.OK,
"application/json",
@"{
""data"": [
{
""created_at"": ""2023-11-07T05:31:56Z"",
""downloadable"": false,
""filename"": ""example.txt"",
""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"",
""mime_type"": ""text/plain"",
""size_bytes"": 1234,
""type"": ""file""
}
],
""has_more"": false,
""first_id"": ""2"",
""last_id"": ""2""
}"
);
var pageResponses = Client.ListAllFilesAsync();
var collectedPages = new List<Page<AnthropicFile>>();
await foreach (var response in pageResponses)
{
response.IsSuccess.Should().BeTrue();
response.Value.Should().BeOfType<Page<AnthropicFile>>();
collectedPages.Add(response.Value);
}
var expectedFile = new AnthropicFile()
{
CreatedAt = DateTimeOffset.Parse("2023-11-07T05:31:56Z"),
Downloadable = false,
Name = "example.txt",
Id = "file_013Zva2CMHLNnXjNJJKqJ2EF",
MimeType = "text/plain",
Size = 1234,
Type = "file"
};
collectedPages.Should().HaveCount(2);
collectedPages.Should().BeEquivalentTo(new List<Page<AnthropicFile>>()
{
new()
{
Data = [expectedFile],
FirstId = "1",
LastId = "1",
HasMore = true
},
new()
{
Data = [expectedFile],
FirstId = "2",
LastId = "2",
HasMore = false
}
});
}
[Fact]
public async Task GetFileInfoAsync_WhenCalled_ItShouldReturnFile()
{
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
_mockHttpMessageHandler
.WhenGetFileRequest(fileId)
.Respond(
HttpStatusCode.OK,
"application/json",
@"{
""created_at"": ""2023-11-07T05:31:56Z"",
""downloadable"": false,
""filename"": ""example.txt"",
""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"",
""mime_type"": ""text/plain"",
""size_bytes"": 1234,
""type"": ""file""
}"
);
var result = await Client.GetFileInfoAsync(fileId);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeEquivalentTo(new AnthropicFile()
{
CreatedAt = DateTimeOffset.Parse("2023-11-07T05:31:56Z"),
Downloadable = false,
Name = "example.txt",
Id = "file_013Zva2CMHLNnXjNJJKqJ2EF",
MimeType = "text/plain",
Size = 1234,
Type = "file"
});
}
[Fact]
public async Task GetFileInfoAsync_WhenCalledAndRequestFails_ItShouldReturnError()
{
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
_mockHttpMessageHandler
.WhenGetFileRequest(fileId)
.Respond(
HttpStatusCode.BadRequest,
"application/json",
@"{
""type"": ""error"",
""error"": {
""type"": ""invalid_request_error"",
""message"": ""file: file not found""
}
}"
);
var result = await Client.GetFileInfoAsync(fileId);
result.IsSuccess.Should().BeFalse();
result.Error.Should().BeOfType<AnthropicError>();
result.Error.Error.Should().BeOfType<InvalidRequestError>();
}
[Fact]
public async Task GetFileAsync_WhenCalled_ItShouldReturnFileContent()
{
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
var fileContent = new MemoryStream(Encoding.UTF8.GetBytes("Example file content"));
_mockHttpMessageHandler
.WhenGetFileContentRequest(fileId)
.Respond(
HttpStatusCode.OK,
"application/octet-stream",
fileContent
);
var result = await Client.GetFileAsync(fileId);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeAssignableTo<Stream>();
using var streamReader = new StreamReader(result.Value);
var content = await streamReader.ReadToEndAsync();
content.Should().Be("Example file content");
}
[Fact]
public async Task GetFileAsync_WhenCalledAndRequestFails_ItShouldReturnError()
{
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
_mockHttpMessageHandler
.WhenGetFileContentRequest(fileId)
.Respond(
HttpStatusCode.BadRequest,
"application/json",
@"{
""type"": ""error"",
""error"": {
""type"": ""invalid_request_error"",
""message"": ""file: file not found""
}
}"
);
var result = await Client.GetFileAsync(fileId);
result.IsSuccess.Should().BeFalse();
result.Error.Should().BeOfType<AnthropicError>();
result.Error.Error.Should().BeOfType<InvalidRequestError>();
}
[Fact]
public async Task GetFileAsync_WhenCalledAndCanNotDeserializeResponse_ItShouldReturnError()
{
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
_mockHttpMessageHandler
.WhenGetFileContentRequest(fileId)
.Respond(
HttpStatusCode.BadRequest,
"application/json",
@"null"
);
var result = await Client.GetFileAsync(fileId);
result.IsSuccess.Should().BeFalse();
result.Error.Should().BeOfType<AnthropicError>();
result.Error.Error.Should().BeOfType<ApiError>();
}
[Fact]
public async Task DeleteFileAsync_WhenCalled_ItShouldReturnDeletionResponse()
{
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
_mockHttpMessageHandler
.WhenDeleteFileRequest(fileId)
.Respond(
HttpStatusCode.OK,
"application/json",
@"{
""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"",
""type"": ""file_deleted""
}"
);
var result = await Client.DeleteFileAsync(fileId);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType<AnthropicFileDeleteResponse>();
result.Value.Id.Should().Be(fileId);
result.Value.Type.Should().Be("file_deleted");
}
[Fact]
public async Task DeleteFileAsync_WhenCalledAndRequestFails_ItShouldReturnError()
{
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
_mockHttpMessageHandler
.WhenDeleteFileRequest(fileId)
.Respond(
HttpStatusCode.BadRequest,
"application/json",
@"{
""type"": ""error"",
""error"": {
""type"": ""invalid_request_error"",
""message"": ""file: file not found""
}
}"
);
var result = await Client.DeleteFileAsync(fileId);
result.IsSuccess.Should().BeFalse();
result.Error.Should().BeOfType<AnthropicError>();
result.Error.Error.Should().BeOfType<InvalidRequestError>();
}
} }
@@ -123,7 +123,7 @@ public static class MockHttpMessageHandlerExtensions
.SetupBaseRequest(HttpMethod.Get, $"{FilesEndpoint}/{fileId}"); .SetupBaseRequest(HttpMethod.Get, $"{FilesEndpoint}/{fileId}");
} }
public static MockedRequest WhenDownloadFileRequest(this MockHttpMessageHandler mockHttpMessageHandler, string fileId) public static MockedRequest WhenGetFileContentRequest(this MockHttpMessageHandler mockHttpMessageHandler, string fileId)
{ {
return mockHttpMessageHandler return mockHttpMessageHandler
.SetupBaseRequest(HttpMethod.Get, $"{FilesEndpoint}/{fileId}/content"); .SetupBaseRequest(HttpMethod.Get, $"{FilesEndpoint}/{fileId}/content");
@@ -0,0 +1,58 @@
namespace AnthropicClient.Tests.Unit.Models;
public class AnthropicFileDeleteResponseTests : SerializationTest
{
private readonly string _testJson = @"{
""id"": ""file-12345"",
""type"": ""file_deleted""
}";
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var response = new AnthropicFileDeleteResponse();
response.Id.Should().BeEmpty();
response.Type.Should().BeEmpty();
}
[Fact]
public void Constructor_WhenCalledWithValues_ItShouldInitializePropertiesWithValues()
{
var id = "file-12345";
var type = "file_deleted";
var response = new AnthropicFileDeleteResponse
{
Id = id,
Type = type
};
response.Id.Should().Be(id);
response.Type.Should().Be(type);
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldMatchExpectedJson()
{
var response = new AnthropicFileDeleteResponse
{
Id = "file-12345",
Type = "file_deleted"
};
var json = JsonSerializer.Serialize(response, JsonSerializationOptions.DefaultOptions);
JsonAssert.Equal(_testJson, json);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldMatchExpectedObject()
{
var response = JsonSerializer.Deserialize<AnthropicFileDeleteResponse>(_testJson, JsonSerializationOptions.DefaultOptions);
response.Should().NotBeNull();
response.Id.Should().Be("file-12345");
response.Type.Should().Be("file_deleted");
}
}
@@ -2,66 +2,94 @@ namespace AnthropicClient.Tests.Unit.Models;
public class AnthropicFileTests : SerializationTest public class AnthropicFileTests : SerializationTest
{ {
private const string SampleJson = @"{ private readonly string _testJson = @"{
""id"": ""file-123"",
""type"": ""file"", ""type"": ""file"",
""id"": ""file_abc123"", ""filename"": ""test.txt"",
""filename"": ""example.txt"", ""created_at"": ""2023-10-01T00:00:00Z"",
""content_type"": ""text/plain"",
""size_bytes"": 1024, ""size_bytes"": 1024,
""created_at"": ""2024-03-15T10:30:00Z"" ""mime_type"": ""text/plain"",
""downloadable"": true
}"; }";
[Fact] [Fact]
public void Constructor_WhenCalled_ItShouldReturnAnInstanceWithPropertiesSet() public void Constructor_WhenCalled_ItShouldInitializeProperties()
{ {
var file = new AnthropicFile(); var result = new AnthropicFile();
result.Id.Should().BeEmpty();
result.Type.Should().BeEmpty();
result.Name.Should().BeEmpty();
result.CreatedAt.Should().Be(DateTimeOffset.MinValue);
result.Size.Should().Be(0);
result.MimeType.Should().BeEmpty();
result.Downloadable.Should().BeFalse();
}
[Fact]
public void Constructor_WhenCalledWithValues_ItShouldInitializeProperties()
{
var file = new AnthropicFile
{
Id = "file-123",
Type = "file",
Name = "test.txt",
CreatedAt = new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero),
Size = 1024,
MimeType = "text/plain",
Downloadable = true
};
file.Id.Should().Be("file-123");
file.Type.Should().Be("file"); file.Type.Should().Be("file");
file.Id.Should().BeEmpty(); file.Name.Should().Be("test.txt");
file.Filename.Should().BeEmpty(); file.CreatedAt.Should().Be(new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero));
file.ContentType.Should().BeEmpty(); file.Size.Should().Be(1024);
file.SizeBytes.Should().Be(0); file.MimeType.Should().Be("text/plain");
file.CreatedAt.Should().Be(default); file.Downloadable.Should().BeTrue();
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var file = new AnthropicFile
{
Id = "file-123",
Type = "file",
Name = "test.txt",
CreatedAt = new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero),
Size = 1024,
MimeType = "text/plain",
Downloadable = true
};
var json = Serialize(file);
var expectedJson = @"{
""id"": ""file-123"",
""type"": ""file"",
""filename"": ""test.txt"",
""created_at"": ""2023-10-01T00:00:00+00:00"",
""size_bytes"": 1024,
""mime_type"": ""text/plain"",
""downloadable"": true
}";
JsonAssert.Equal(expectedJson, json, true);
} }
[Fact] [Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues() public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues()
{ {
var result = Deserialize<AnthropicFile>(SampleJson); var file = Deserialize<AnthropicFile>(_testJson);
result.Should().NotBeNull(); file.Should().NotBeNull();
result!.Type.Should().Be("file"); file!.Id.Should().Be("file-123");
result.Id.Should().Be("file_abc123"); file.Type.Should().Be("file");
result.Filename.Should().Be("example.txt"); file.Name.Should().Be("test.txt");
result.ContentType.Should().Be("text/plain"); file.CreatedAt.Should().Be(new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero));
result.SizeBytes.Should().Be(1024); file.Size.Should().Be(1024);
result.CreatedAt.Should().Be(new DateTimeOffset(2024, 3, 15, 10, 30, 0, TimeSpan.Zero)); file.MimeType.Should().Be("text/plain");
} file.Downloadable.Should().BeTrue();
[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);
} }
} }
@@ -131,4 +131,64 @@ public class AnthropicModelsTests
actual.Should().Be(expected); actual.Should().Be(expected);
} }
[Fact]
public void Claude37Sonnet20250219_WhenCalled_ItShouldReturnExpectedValue()
{
var expected = "claude-3-7-sonnet-20250219";
var actual = AnthropicModels.Claude37Sonnet20250219;
actual.Should().Be(expected);
}
[Fact]
public void Claude37SonnetLatest_WhenCalled_ItShouldReturnExpectedValue()
{
var expected = "claude-3-7-sonnet-latest";
var actual = AnthropicModels.Claude37SonnetLatest;
actual.Should().Be(expected);
}
[Fact]
public void ClaudeSonnet420250514_WhenCalled_ItShouldReturnExpectedValue()
{
var expected = "claude-sonnet-4-20250514";
var actual = AnthropicModels.ClaudeSonnet420250514;
actual.Should().Be(expected);
}
[Fact]
public void ClaudeSonnet40_WhenCalled_ItShouldReturnExpectedValue()
{
var expected = "claude-sonnet-4-0";
var actual = AnthropicModels.ClaudeSonnet40;
actual.Should().Be(expected);
}
[Fact]
public void ClaudeOpus420250514_WhenCalled_ItShouldReturnExpectedValue()
{
var expected = "claude-opus-4-20250514";
var actual = AnthropicModels.ClaudeOpus420250514;
actual.Should().Be(expected);
}
[Fact]
public void ClaudeOpus40_WhenCalled_ItShouldReturnExpectedValue()
{
var expected = "claude-opus-4-0";
var actual = AnthropicModels.ClaudeOpus40;
actual.Should().Be(expected);
}
} }
@@ -0,0 +1,106 @@
namespace AnthropicClient.Tests.Unit.Models;
public class CreateFileRequestTests
{
[Fact]
public void Constructor_WhenCalledWithBytes_ItShouldInitializeProperties()
{
var fileContent = new byte[] { 1, 2, 3 };
var fileName = "test.txt";
var fileType = "text/plain";
var request = new CreateFileRequest(fileContent, fileName, fileType);
request.File.Should().BeSameAs(fileContent);
request.FileName.Should().Be(fileName);
request.FileType.Should().Be(fileType);
}
[Fact]
public void Constructor_WhenCalledWithStream_ItShouldInitializeProperties()
{
using var stream = new MemoryStream([1, 2, 3]);
var fileName = "test.txt";
var fileType = "text/plain";
var request = new CreateFileRequest(stream, fileName, fileType);
request.File.Should().BeEquivalentTo(new byte[] { 1, 2, 3 });
request.FileName.Should().Be(fileName);
request.FileType.Should().Be(fileType);
}
[Fact]
public void Constructor_WhenCalledWithNullBytes_ItShouldThrowArgumentNullException()
{
var act = () => new CreateFileRequest((byte[])null!, "test.txt", "text/plain");
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenCalledWithBytesAndNullFileName_ItShouldThrowArgumentException()
{
var act = () => new CreateFileRequest([1, 2, 3], null!, "text/plain");
act.Should().Throw<ArgumentException>();
}
[Fact]
public void Constructor_WhenCalledWithBytesAndFileNameIsEmpty_ItShouldThrowArgumentException()
{
var act = () => new CreateFileRequest([1, 2, 3], string.Empty, "text/plain");
act.Should().Throw<ArgumentException>();
}
[Fact]
public void Constructor_WhenCalledWithBytesAndNullFileType_ItShouldThrowArgumentException()
{
var act = () => new CreateFileRequest([1, 2, 3], "test.txt", null!);
act.Should().Throw<ArgumentException>();
}
[Fact]
public void Constructor_WhenCalledWithBytesAndFileTypeIsEmpty_ItShouldThrowArgumentException()
{
var act = () => new CreateFileRequest([1, 2, 3], "test.txt", string.Empty);
act.Should().Throw<ArgumentException>();
}
[Fact]
public void Constructor_WhenCalledWithNullStream_ItShouldThrowArgumentNullException()
{
var act = () => new CreateFileRequest((Stream)null!, "test.txt", "text/plain");
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenCalledWithStreamAndNullFileName_ItShouldThrowArgumentException()
{
using var stream = new MemoryStream([1, 2, 3]);
var act = () => new CreateFileRequest(stream, null!, "text/plain");
act.Should().Throw<ArgumentException>();
}
[Fact]
public void Constructor_WhenCalledWithStreamAndFileNameIsEmpty_ItShouldThrowArgumentException()
{
using var stream = new MemoryStream([1, 2, 3]);
var act = () => new CreateFileRequest(stream, string.Empty, "text/plain");
act.Should().Throw<ArgumentException>();
}
[Fact]
public void Constructor_WhenCalledWithStreamAndNullFileType_ItShouldThrowArgumentException()
{
using var stream = new MemoryStream([1, 2, 3]);
var act = () => new CreateFileRequest(stream, "test.txt", null!);
act.Should().Throw<ArgumentException>();
}
[Fact]
public void Constructor_WhenCalledWithStreamAndFileTypeIsEmpty_ItShouldThrowArgumentException()
{
using var stream = new MemoryStream([1, 2, 3]);
var act = () => new CreateFileRequest(stream, "test.txt", string.Empty);
act.Should().Throw<ArgumentException>();
}
}
@@ -1,52 +0,0 @@
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<FileDeleteResponse>(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);
}
}
@@ -1,20 +0,0 @@
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);
}
}
@@ -1,103 +0,0 @@
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<ArgumentNullException>().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<ArgumentException>().WithParameterName("filename");
}
[Fact]
public void Constructor_WhenFilenameIsEmpty_ItShouldThrowArgumentException()
{
var content = "Hello World"u8.ToArray();
var act = () => new FileRequest(content, "", "text/plain");
act.Should().Throw<ArgumentException>().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<ArgumentException>().WithParameterName("contentType");
}
[Fact]
public void Constructor_WhenContentTypeIsEmpty_ItShouldThrowArgumentException()
{
var content = "Hello World"u8.ToArray();
var act = () => new FileRequest(content, "example.txt", "");
act.Should().Throw<ArgumentException>().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<ArgumentException>().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<ArgumentException>().WithParameterName("purpose");
}
}
@@ -0,0 +1,63 @@
namespace AnthropicClient.Tests.Unit.Models;
public class FileSourceTests : SerializationTest
{
private readonly string _testJson = @"{
""file_id"": ""id"",
""type"": ""file""
}";
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var result = new FileSource();
result.Type.Should().Be("file");
result.Id.Should().BeEmpty();
}
[Fact]
public void Constructor_WhenCalledWithValues_ItShouldInitializeProperties()
{
var id = "id";
var type = "type";
var result = new FileSource()
{
Id = id,
Type = type,
};
result.Id.Should().Be(id);
result.Type.Should().Be(type);
}
[Fact]
public void Constructor_WhenCalledWithId_ItShouldInitializeProperties()
{
var id = "id";
var result = new FileSource(id);
result.Id.Should().Be(id);
result.Type.Should().Be("file");
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var source = new FileSource() { Id = "id" };
var result = Serialize<Source>(source);
JsonAssert.Equal(_testJson, result);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues()
{
var result = Deserialize<Source>(_testJson);
result.Should().BeEquivalentTo(new FileSource() { Id = "id" });
}
}
@@ -77,7 +77,7 @@ public class ImageContentTests : SerializationTest
} }
[Fact] [Fact]
public void Constructor_WhenCalledWithCacheControlAndMediatTypeIsNull_ItShouldThrowArgumentNullException() public void Constructor_WhenCalledWithCacheControlAndMediaTypeIsNull_ItShouldThrowArgumentNullException()
{ {
var expectedData = "data"; var expectedData = "data";
var cacheControl = new EphemeralCacheControl(); var cacheControl = new EphemeralCacheControl();
@@ -98,6 +98,49 @@ public class ImageContentTests : SerializationTest
action.Should().Throw<ArgumentNullException>(); action.Should().Throw<ArgumentNullException>();
} }
[Fact]
public void Constructor_WhenCalledWithSource_ItShouldInitializeSource()
{
var source = new ImageSource("image/png", "data");
var result = new ImageContent(source);
result.Source.Should().BeSameAs(source);
}
[Fact]
public void Constructor_WhenCalledWithSourceAndCacheControl_ItShouldInitializeSourceAndCacheControl()
{
var source = new ImageSource("image/png", "data");
var cacheControl = new EphemeralCacheControl();
var result = new ImageContent(source, cacheControl);
result.Source.Should().BeSameAs(source);
result.CacheControl.Should().BeSameAs(cacheControl);
}
[Fact]
public void Constructor_WhenSourceIsNull_ItShouldThrowArgumentNullException()
{
Source? source = null;
var action = () => new ImageContent(source!);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenCacheControlIsNull_ItShouldThrowNullException()
{
var source = new ImageSource("image/png", "data");
CacheControl? cacheControl = null;
var action = () => new ImageContent(source, cacheControl!);
action.Should().Throw<ArgumentNullException>();
}
[Fact] [Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{ {
@@ -19,4 +19,10 @@ public class SourceTypeTests
{ {
SourceType.Text.Should().Be("text"); SourceType.Text.Should().Be("text");
} }
[Fact]
public void File_WhenCalled_ItShouldReturnCorrectValue()
{
SourceType.File.Should().Be("file");
}
} }
@@ -0,0 +1,64 @@
namespace AnthropicClient.Tests.Unit.Models;
public class UrlSourceTests : SerializationTest
{
private readonly string _testJson = @"{
""type"": ""url"",
""url"": ""https://example.com/document.pdf""
}";
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var result = new UrlSource();
result.Url.Should().BeEmpty();
result.Type.Should().Be("url");
}
[Fact]
public void Constructor_WhenCalledWithValues_ItShouldInitializeProperties()
{
var url = "https://example.com/document.pdf";
var type = "type";
var result = new UrlSource()
{
Url = url,
Type = type,
};
result.Url.Should().Be(url);
result.Type.Should().Be(type);
}
[Fact]
public void Constructor_WhenCalledWithUrl_ItShouldInitializeProperties()
{
var url = "https://example.com/document.pdf";
var result = new UrlSource(url);
result.Url.Should().Be(url);
result.Type.Should().Be("url");
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var url = "https://example.com/document.pdf";
var source = new UrlSource(url);
var result = Serialize<Source>(source);
JsonAssert.Equal(_testJson, result);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldMatchExpectedObject()
{
var result = Deserialize<Source>(_testJson);
result.Should().BeEquivalentTo(new UrlSource("https://example.com/document.pdf"));
}
}