From 1f83899eb88b9ee9d87d246158defd7cbde9b01e Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 14 Jul 2025 23:01:23 -0500 Subject: [PATCH] feat: add support for file source and url source --- src/AnthropicClient/Json/SourceConverter.cs | 14 ++ src/AnthropicClient/Models/FileSource.cs | 33 +++ src/AnthropicClient/Models/ImageContent.cs | 28 +++ src/AnthropicClient/Models/SourceType.cs | 10 + src/AnthropicClient/Models/UrlSource.cs | 30 +++ .../EndToEnd/AnthropicApiClientTests.cs | 214 +++++++++++++++--- .../Unit/Models/FileSourceTests.cs | 52 +++++ .../Unit/Models/ImageContentTests.cs | 45 +++- .../Unit/Models/SourceTypeTests.cs | 6 + .../Unit/Models/UrlSourceTests.cs | 64 ++++++ 10 files changed, 469 insertions(+), 27 deletions(-) create mode 100644 src/AnthropicClient/Models/FileSource.cs create mode 100644 src/AnthropicClient/Models/UrlSource.cs create mode 100644 tests/AnthropicClient.Tests/Unit/Models/FileSourceTests.cs create mode 100644 tests/AnthropicClient.Tests/Unit/Models/UrlSourceTests.cs diff --git a/src/AnthropicClient/Json/SourceConverter.cs b/src/AnthropicClient/Json/SourceConverter.cs index a68fd70..190d93a 100644 --- a/src/AnthropicClient/Json/SourceConverter.cs +++ b/src/AnthropicClient/Json/SourceConverter.cs @@ -16,6 +16,8 @@ class SourceConverter : JsonConverter { SourceType.Text => JsonSerializer.Deserialize(root.GetRawText(), options)!, SourceType.Content => JsonSerializer.Deserialize(root.GetRawText(), options)!, + SourceType.File => JsonSerializer.Deserialize(root.GetRawText(), options)!, + SourceType.Url => JsonSerializer.Deserialize(root.GetRawText(), options)!, SourceType.Base64 => DeserializeBase64Source(root, options), _ => throw new JsonException($"Unknown source type: {type}") }; @@ -54,6 +56,18 @@ class SourceConverter : JsonConverter 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); } } \ No newline at end of file diff --git a/src/AnthropicClient/Models/FileSource.cs b/src/AnthropicClient/Models/FileSource.cs new file mode 100644 index 0000000..106bb2f --- /dev/null +++ b/src/AnthropicClient/Models/FileSource.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +namespace AnthropicClient.Models; + +/// +/// Represents a file source in the Anthropic API. +/// +public class FileSource : Source +{ + /// + /// Gets or sets the unique identifier for the file source. + /// + [JsonPropertyName("file_id")] + public string Id { get; init; } = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + /// A new instance of with the type set to "file". + public FileSource() : base(SourceType.File) + { + } + + /// + /// Initializes a new instance of the class with a specified file ID. + /// + /// The unique identifier for the file source. + /// A new instance of . + public FileSource(string id) : base(SourceType.File) + { + Id = id; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/ImageContent.cs b/src/AnthropicClient/Models/ImageContent.cs index 1d4a949..6393db1 100644 --- a/src/AnthropicClient/Models/ImageContent.cs +++ b/src/AnthropicClient/Models/ImageContent.cs @@ -53,4 +53,32 @@ public class ImageContent : Content Source = new ImageSource(mediaType, data); } + + /// + /// Initializes a new instance of the class. + /// + /// The source of the image. + /// A new instance of the class. + /// Thrown when the source is null. + public ImageContent(Source source) : base(ContentType.Image) + { + ArgumentValidator.ThrowIfNull(source, nameof(source)); + + Source = source; + } + + /// + /// Initializes a new instance of the class. + /// + /// The source of the image. + /// The cache control to be used for the content. + /// A new instance of the class. + /// Thrown when the source or cache control is null. + public ImageContent(Source source, CacheControl cacheControl) : base(ContentType.Image, cacheControl) + { + ArgumentValidator.ThrowIfNull(source, nameof(source)); + ArgumentValidator.ThrowIfNull(cacheControl, nameof(cacheControl)); + + Source = source; + } } \ No newline at end of file diff --git a/src/AnthropicClient/Models/SourceType.cs b/src/AnthropicClient/Models/SourceType.cs index 7079bbe..0eb9e3a 100644 --- a/src/AnthropicClient/Models/SourceType.cs +++ b/src/AnthropicClient/Models/SourceType.cs @@ -19,4 +19,14 @@ public static class SourceType /// The text document source type. /// public const string Text = "text"; + + /// + /// The file document source type. + /// + public const string File = "file"; + + /// + /// The URL document source type. + /// + public const string Url = "url"; } \ No newline at end of file diff --git a/src/AnthropicClient/Models/UrlSource.cs b/src/AnthropicClient/Models/UrlSource.cs new file mode 100644 index 0000000..f9e343d --- /dev/null +++ b/src/AnthropicClient/Models/UrlSource.cs @@ -0,0 +1,30 @@ +namespace AnthropicClient.Models; + +/// +/// Represents a URL source in the Anthropic API. +/// +public class UrlSource : Source +{ + /// + /// Gets or sets the URL of the source document. + /// + public string Url { get; init; } = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + /// A new instance of with the type set to "url". + public UrlSource() : base(SourceType.Url) + { + } + + /// + /// Initializes a new instance of the class with a specified URL. + /// + /// The URL of the source document. + /// A new instance of . + public UrlSource(string url) : base(SourceType.Url) + { + Url = url; + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs index da6a61f..e7b289c 100644 --- a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs +++ b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs @@ -2,8 +2,15 @@ using AnthropicClient.Tests.Files; namespace AnthropicClient.Tests.EndToEnd; -public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture) +public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture), IAsyncLifetime { + private readonly List _filesToDelete = []; + + public Task InitializeAsync() + { + return Task.CompletedTask; + } + [Fact] public async Task CreateMessageAsync_WhenCalled_ItShouldReturnResponse() { @@ -96,11 +103,41 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo 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(); + 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] public async Task CreateMessageAsync_WhenSystemMessagesContainCacheControl_ItShouldUseCache() { - var client = CreateClient(new HttpClient()); - var storyPath = TestFileHelper.GetTestFilePath("story.txt"); 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.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.Value.Should().BeOfType(); @@ -138,8 +175,6 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo [Fact] public async Task CreateMessageAsync_WhenMessagesContainCacheControl_ItShouldUseCache() { - var client = CreateClient(new HttpClient()); - var storyPath = TestFileHelper.GetTestFilePath("story.txt"); 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.Value.Should().BeOfType(); @@ -163,7 +198,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo 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?")])); - var resultTwo = await client.CreateMessageAsync(request); + var resultTwo = await _client.CreateMessageAsync(request); resultTwo.IsSuccess.Should().BeTrue(); resultTwo.Value.Should().BeOfType(); @@ -174,8 +209,6 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo [Fact] public async Task CreateMessageAsync_WhenToolsContainCacheControl_ItShouldUseCache() { - var client = CreateClient(new HttpClient()); - var func = (string ticker) => ticker; var tools = Enumerable @@ -195,7 +228,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo tools: tools ); - var resultOne = await client.CreateMessageAsync(request); + var resultOne = await _client.CreateMessageAsync(request); resultOne.IsSuccess.Should().BeTrue(); resultOne.Value.Should().BeOfType(); @@ -205,7 +238,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo 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?")])); - var resultTwo = await client.CreateMessageAsync(request); + var resultTwo = await _client.CreateMessageAsync(request); resultTwo.IsSuccess.Should().BeTrue(); resultTwo.Value.Should().BeOfType(); @@ -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.Value.Should().BeOfType(); @@ -256,8 +287,6 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo var bytes = await File.ReadAllBytesAsync(pdfPath); var base64Data = Convert.ToBase64String(bytes); - var client = CreateClient(new HttpClient()); - var request = new MessageRequest( model: AnthropicModels.Claude35Sonnet, 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.Value.Should().BeOfType(); @@ -278,7 +307,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo 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?")])); - var resultTwo = await client.CreateMessageAsync(request); + var resultTwo = await _client.CreateMessageAsync(request); resultTwo.IsSuccess.Should().BeTrue(); resultTwo.Value.Should().BeOfType(); @@ -395,6 +424,58 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo citations.OfType().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(); + + 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().Should().NotBeEmpty(); + + _filesToDelete.Add(createdFile.Value.Id); + } + [Fact] public async Task CreateMessageAsync_WhenStreamingAndCitationsAreEnabledForTextDocumentSource_ItShouldReturnCitationsInResponse() { @@ -516,6 +597,64 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo citations.OfType().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() + .Message + .Content + .OfType(); + + 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().Should().NotBeEmpty(); + + _filesToDelete.Add(createdFile.Value.Id); + } + [Fact] public async Task CountMessageTokensAsync_WhenCalled_ItShouldReturnResponse() { @@ -697,7 +836,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo var fileContent = await File.ReadAllBytesAsync(filePath); var request = new CreateFileRequest(fileContent, fileName, fileType); - var httpClient = new HttpClient(); + using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14"); var client = CreateClient(httpClient); @@ -707,12 +846,14 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo result.Value.Should().BeOfType(); result.Value.Name.Should().Be(fileName); result.Value.MimeType.Should().Be(fileType); + + _filesToDelete.Add(result.Value.Id); } [Fact] public async Task ListFilesAsync_WhenCalled_ItShouldReturnPageOfFiles() { - var httpClient = new HttpClient(); + using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14"); var client = CreateClient(httpClient); @@ -725,12 +866,14 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType>(); result.Value.Data.Should().ContainSingle(f => f.Id == createdFile.Value.Id); + + _filesToDelete.Add(createdFile.Value.Id); } [Fact] public async Task ListAllFilesAsync_WhenCalled_ItShouldReturnAllFiles() { - var httpClient = new HttpClient(); + using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14"); var client = CreateClient(httpClient); @@ -741,14 +884,18 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo 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); + 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() { - var httpClient = new HttpClient(); + using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14"); var client = CreateClient(httpClient); @@ -761,12 +908,14 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType(); result.Value.Id.Should().Be(createdFile.Value.Id); + + _filesToDelete.Add(createdFile.Value.Id); } [Fact] public async Task DeleteFileAsync_WhenCalled_ItShouldReturnDeleteResponse() { - var httpClient = new HttpClient(); + using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14"); var client = CreateClient(httpClient); @@ -780,4 +929,17 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo result.Value.Should().BeOfType(); 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(); + } + } } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/FileSourceTests.cs b/tests/AnthropicClient.Tests/Unit/Models/FileSourceTests.cs new file mode 100644 index 0000000..6a99d28 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/FileSourceTests.cs @@ -0,0 +1,52 @@ +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 JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var source = new FileSource() { Id = "id" }; + + var result = Serialize(source); + + JsonAssert.Equal(_testJson, result); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues() + { + var result = Deserialize(_testJson); + + result.Should().BeEquivalentTo(new FileSource() { Id = "id" }); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/ImageContentTests.cs b/tests/AnthropicClient.Tests/Unit/Models/ImageContentTests.cs index bcc4e50..c75db27 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/ImageContentTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/ImageContentTests.cs @@ -77,7 +77,7 @@ public class ImageContentTests : SerializationTest } [Fact] - public void Constructor_WhenCalledWithCacheControlAndMediatTypeIsNull_ItShouldThrowArgumentNullException() + public void Constructor_WhenCalledWithCacheControlAndMediaTypeIsNull_ItShouldThrowArgumentNullException() { var expectedData = "data"; var cacheControl = new EphemeralCacheControl(); @@ -98,6 +98,49 @@ public class ImageContentTests : SerializationTest action.Should().Throw(); } + [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(); + } + + [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(); + } + [Fact] public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() { diff --git a/tests/AnthropicClient.Tests/Unit/Models/SourceTypeTests.cs b/tests/AnthropicClient.Tests/Unit/Models/SourceTypeTests.cs index 1e091ab..561dbf8 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/SourceTypeTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/SourceTypeTests.cs @@ -19,4 +19,10 @@ public class SourceTypeTests { SourceType.Text.Should().Be("text"); } + + [Fact] + public void File_WhenCalled_ItShouldReturnCorrectValue() + { + SourceType.File.Should().Be("file"); + } } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/UrlSourceTests.cs b/tests/AnthropicClient.Tests/Unit/Models/UrlSourceTests.cs new file mode 100644 index 0000000..d1ccbc8 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/UrlSourceTests.cs @@ -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); + + JsonAssert.Equal(_testJson, result); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldMatchExpectedObject() + { + var result = Deserialize(_testJson); + + result.Should().BeEquivalentTo(new UrlSource("https://example.com/document.pdf")); + } +}