From 876c6f571c99082c401259d35fa0e30bac94a328 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 12 Jan 2025 13:26:31 -0600 Subject: [PATCH] tests: cover null responses --- src/AnthropicClient/AnthropicApiClient.cs | 1 - .../Json/ContentDeltaConverter.cs | 1 - .../Json/EventDataConverter.cs | 1 - .../Integration/AnthropicApiClientTests.cs | 239 +++++++++++++++++- .../Models/CanceledMessageBatchResultTests.cs | 34 +++ .../Unit/Models/ErroredMessageBatchResult.cs | 48 ++++ .../Models/ExpiredMessageBatchResultTests.cs | 34 +++ .../Unit/Models/MessageBatchResultTests.cs | 25 ++ .../Unit/Models/ToolCallTests.cs | 2 - .../Unit/SerializationTest.cs | 2 - 10 files changed, 370 insertions(+), 17 deletions(-) create mode 100644 tests/AnthropicClient.Tests/Unit/Models/CanceledMessageBatchResultTests.cs create mode 100644 tests/AnthropicClient.Tests/Unit/Models/ErroredMessageBatchResult.cs create mode 100644 tests/AnthropicClient.Tests/Unit/Models/ExpiredMessageBatchResultTests.cs create mode 100644 tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultTests.cs diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs index fbe11c6..add0174 100644 --- a/src/AnthropicClient/AnthropicApiClient.cs +++ b/src/AnthropicClient/AnthropicApiClient.cs @@ -1,7 +1,6 @@ using System.Net.Http.Headers; using System.Text; using System.Text.Json; -using System.Threading.Tasks; using AnthropicClient.Json; using AnthropicClient.Models; diff --git a/src/AnthropicClient/Json/ContentDeltaConverter.cs b/src/AnthropicClient/Json/ContentDeltaConverter.cs index e384b8a..af2dbb8 100644 --- a/src/AnthropicClient/Json/ContentDeltaConverter.cs +++ b/src/AnthropicClient/Json/ContentDeltaConverter.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/src/AnthropicClient/Json/EventDataConverter.cs b/src/AnthropicClient/Json/EventDataConverter.cs index d11855b..31bb6fe 100644 --- a/src/AnthropicClient/Json/EventDataConverter.cs +++ b/src/AnthropicClient/Json/EventDataConverter.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs index f3fa91e..e5a5a33 100644 --- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs +++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs @@ -1,5 +1,4 @@ using AnthropicClient.Tests.Files; -using AnthropicClient.Tests.Unit; namespace AnthropicClient.Tests.Integration; @@ -35,6 +34,51 @@ public class AnthropicApiClientTests : IntegrationTest actualErrorType.Should().Be(errorType); } + [Fact] + public async Task CreateMessageAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError() + { + _mockHttpMessageHandler + .WhenCreateMessageRequest() + .Respond( + HttpStatusCode.BadRequest, + "application/json", + @"null" + ); + + var request = new MessageRequest( + model: AnthropicModels.Claude3Haiku, + messages: [new(MessageRole.User, [new TextContent("Hello!")])] + ); + + var result = await Client.CreateMessageAsync(request); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeOfType(); + result.Error.Error.Should().BeOfType(); + } + + [Fact] + public async Task CreateMessageAsync_WhenCalledRequestSucceedsAndCanNotDeserializeResponse_ItShouldReturnEmptyResponse() + { + _mockHttpMessageHandler + .WhenCreateMessageRequest() + .Respond( + HttpStatusCode.OK, + "application/json", + @"null" + ); + + var request = new MessageRequest( + model: AnthropicModels.Claude3Haiku, + messages: [new(MessageRole.User, [new TextContent("Hello!")])] + ); + + var result = await Client.CreateMessageAsync(request); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeEquivalentTo(new MessageResponse()); + } + [Fact] public async Task CreateMessageAsync_WhenCalledAndMessageCreatedWithTextContent_ItShouldReturnMessage() { @@ -363,6 +407,34 @@ public class AnthropicApiClientTests : IntegrationTest )); } + [Fact] + public async Task CreateMessageAsync_WhenCalledMessageIsStreamedRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownErrorEvent() + { + _mockHttpMessageHandler + .WhenCreateStreamMessageRequest() + .Respond( + HttpStatusCode.BadRequest, + "application/json", + @"null" + ); + + var request = new StreamMessageRequest( + model: AnthropicModels.Claude35Sonnet, + messages: [ + new(MessageRole.User, [new TextContent("Hello!")]), + new(MessageRole.User, [new TextContent("Hello!")]) + ] + ); + + var result = Client.CreateMessageAsync(request); + var events = await result.ToListAsync(); + + events.Should().HaveCount(1); + events[0].Type.Should().Be(EventType.Error); + events[0].Data.Should().BeOfType(); + events[0].Data.Should().BeEquivalentTo(new ErrorEventData(new ApiError())); + } + [Fact] public async Task CreateMessageAsync_WhenCalledAndMessageCreatedWithDocumentContent_ItShouldReturnMessage() { @@ -489,7 +561,7 @@ public class AnthropicApiClientTests : IntegrationTest .Respond( HttpStatusCode.BadRequest, "application/json", - @"{}" + @"null" ); var request = new CountMessageTokensRequest( @@ -507,6 +579,31 @@ public class AnthropicApiClientTests : IntegrationTest result.Error.Error.Should().BeOfType(); } + [Fact] + public async Task CountMessageTokensAsync_WhenCalledAndResponseCanNotBeDeserialized_ItShouldReturnEmptyResponse() + { + _mockHttpMessageHandler + .WhenCountMessageTokensRequest() + .Respond( + HttpStatusCode.OK, + "application/json", + @"null" + ); + + var request = new CountMessageTokensRequest( + model: AnthropicModels.Claude35Sonnet, + messages: [ + new(MessageRole.User, [new TextContent("Hello!")]), + ] + ); + + var result = await Client.CountMessageTokensAsync(request); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType(); + result.Value.InputTokens.Should().Be(0); + } + [Fact] public async Task ListModelsAsync_WhenCalledWithPagingRequestUsingDefaultValues_ItShouldReturnListOfModels() { @@ -656,7 +753,7 @@ public class AnthropicApiClientTests : IntegrationTest .Respond( HttpStatusCode.BadRequest, "application/json", - @"{}" + @"null" ); var result = await Client.ListModelsAsync(); @@ -666,6 +763,27 @@ public class AnthropicApiClientTests : IntegrationTest result.Error.Error.Should().BeOfType(); } + [Fact] + public async Task ListModelAsync_WhenCalledRequestSucceedsAndCanNotDeserializeResponse_ItShouldReturnEmptyPage() + { + _mockHttpMessageHandler + .WhenListModelsRequest() + .Respond( + HttpStatusCode.OK, + "application/json", + @"null" + ); + + var result = await Client.ListModelsAsync(); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType>(); + result.Value.HasMore.Should().BeFalse(); + result.Value.FirstId.Should().BeEmpty(); + result.Value.LastId.Should().BeEmpty(); + result.Value.Data.Should().BeEmpty(); + } + [Fact] public async Task ListAllModelsAsync_WhenCalled_ItShouldReturnAllModels() { @@ -803,7 +921,7 @@ public class AnthropicApiClientTests : IntegrationTest .Respond( HttpStatusCode.BadRequest, "application/json", - @"{}" + @"null" ); var responses = Client.ListAllModelsAsync(); @@ -889,6 +1007,35 @@ public class AnthropicApiClientTests : IntegrationTest count.Should().Be(2); } + [Fact] + public async Task ListAllModelsAsync_WhenFirstPageSucceedsButResponseCanNotBeDeserialized_ItShouldReturnEmptyPage() + { + _mockHttpMessageHandler + .WhenListModelsRequest() + .WithExactQueryString(new Dictionary + { + { "limit", "20" }, + }) + .Respond( + HttpStatusCode.OK, + "application/json", + @"null" + ); + + var responses = Client.ListAllModelsAsync(); + var count = 0; + + await foreach (var page in responses) + { + count++; + page.IsSuccess.Should().BeTrue(); + page.Value.Should().BeOfType>(); + page.Value.Data.Should().BeEmpty(); + } + + count.Should().Be(1); + } + [Fact] public async Task GetModelAsync_WhenCalled_ItShouldReturnModel() { @@ -953,7 +1100,7 @@ public class AnthropicApiClientTests : IntegrationTest .Respond( HttpStatusCode.BadRequest, "application/json", - @"{}" + @"null" ); var result = await Client.GetModelAsync(modelId); @@ -973,7 +1120,7 @@ public class AnthropicApiClientTests : IntegrationTest .Respond( HttpStatusCode.OK, "application/json", - @"{}" + @"null" ); var result = await Client.GetModelAsync(modelId); @@ -1070,7 +1217,7 @@ public class AnthropicApiClientTests : IntegrationTest .Respond( HttpStatusCode.BadRequest, "application/json", - @"{}" + @"null" ); var request = new MessageBatchRequest([new("custom_id", new())]); @@ -1090,7 +1237,7 @@ public class AnthropicApiClientTests : IntegrationTest .Respond( HttpStatusCode.OK, "application/json", - @"{}" + @"null" ); var request = new MessageBatchRequest([new("custom_id", new())]); @@ -1193,7 +1340,7 @@ public class AnthropicApiClientTests : IntegrationTest .Respond( HttpStatusCode.BadRequest, "application/json", - @"{}" + @"null" ); var result = await Client.GetMessageBatchAsync(batchId); @@ -1213,7 +1360,7 @@ public class AnthropicApiClientTests : IntegrationTest .Respond( HttpStatusCode.OK, "application/json", - @"{}" + @"null" ); var result = await Client.GetMessageBatchAsync(batchId); @@ -1247,4 +1394,76 @@ public class AnthropicApiClientTests : IntegrationTest actualResults.Should().BeEquivalentTo(expectedResults); } + + [Fact] + public async Task GetMessageBatchResultsAsync_WhenCalledSuccessfulAndResultCanNotBeDeserialized_ItShouldReturnEmptyResults() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + var expectedResults = new List() + { + new(), + }; + + _mockHttpMessageHandler + .WhenGetMessageBatchResultsRequest(batchId) + .Respond( + HttpStatusCode.OK, + "application/x-jsonl", + "null" + ); + + var result = await Client.GetMessageBatchResultsAsync(batchId); + + result.IsSuccess.Should().BeTrue(); + + var actualResults = await result.Value.ToListAsync(); + + actualResults.Should().BeEquivalentTo(expectedResults); + } + + [Fact] + public async Task GetMessageBatchResultsAsync_WhenCalledAndRequestFails_ItShouldReturnError() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + + _mockHttpMessageHandler + .WhenGetMessageBatchResultsRequest(batchId) + .Respond( + HttpStatusCode.BadRequest, + "application/json", + @"{ + ""type"": ""error"", + ""error"": { + ""type"": ""invalid_request_error"", + ""message"": ""messages: roles must alternate between user and assistant, but found multiple user roles in a row"" + } + }" + ); + + var result = await Client.GetMessageBatchResultsAsync(batchId); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeOfType(); + result.Error.Error.Should().BeOfType(); + } + + [Fact] + public async Task GetMessageBatchResultsAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + + _mockHttpMessageHandler + .WhenGetMessageBatchResultsRequest(batchId) + .Respond( + HttpStatusCode.BadRequest, + "application/json", + @"null" + ); + + var result = await Client.GetMessageBatchResultsAsync(batchId); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeOfType(); + result.Error.Error.Should().BeOfType(); + } } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/CanceledMessageBatchResultTests.cs b/tests/AnthropicClient.Tests/Unit/Models/CanceledMessageBatchResultTests.cs new file mode 100644 index 0000000..ac68fec --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/CanceledMessageBatchResultTests.cs @@ -0,0 +1,34 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class CanceledMessageBatchResultTests : SerializationTest +{ + private const string SampleJson = @"{ + ""type"": ""canceled"" + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstanceWithPropertiesSet() + { + var result = new CanceledMessageBatchResult(); + + result.Type.Should().Be(MessageBatchResultType.Canceled); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var result = new CanceledMessageBatchResult(); + + var json = Serialize(result); + + JsonAssert.Equal(SampleJson, json); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedProperties() + { + var result = Deserialize(SampleJson); + + result!.Type.Should().Be(MessageBatchResultType.Canceled); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/ErroredMessageBatchResult.cs b/tests/AnthropicClient.Tests/Unit/Models/ErroredMessageBatchResult.cs new file mode 100644 index 0000000..0fd7f69 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/ErroredMessageBatchResult.cs @@ -0,0 +1,48 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class ErroredMessageBatchResultTests : SerializationTest +{ + private const string SampleJson = @"{ + ""type"": ""errored"", + ""error"": { + ""type"": ""error"", + ""error"": { + ""type"": ""api_error"", + ""message"": ""An error occurred."" + } + } + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstanceWithPropertiesSet() + { + var result = new ErroredMessageBatchResult(); + + result.Type.Should().Be(MessageBatchResultType.Errored); + result.Error.Error.Should().BeOfType(); + result.Error.Error.Message.Should().BeEmpty(); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var result = new ErroredMessageBatchResult + { + Error = new(new ApiError("An error occurred.")) + }; + + var json = Serialize(result); + + JsonAssert.Equal(SampleJson, json); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedProperties() + { + var result = Deserialize(SampleJson); + + result!.Type.Should().Be(MessageBatchResultType.Errored); + result.Error.Error.Should().BeOfType(); + result.Error.Error.Message.Should().Be("An error occurred."); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/ExpiredMessageBatchResultTests.cs b/tests/AnthropicClient.Tests/Unit/Models/ExpiredMessageBatchResultTests.cs new file mode 100644 index 0000000..d8e4156 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/ExpiredMessageBatchResultTests.cs @@ -0,0 +1,34 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class ExpiredMessageBatchResultTests : SerializationTest +{ + private const string SampleJson = @"{ + ""type"": ""expired"" + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstanceWithPropertiesSet() + { + var result = new ExpiredMessageBatchResult(); + + result.Type.Should().Be(MessageBatchResultType.Expired); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var result = new ExpiredMessageBatchResult(); + + var json = Serialize(result); + + JsonAssert.Equal(SampleJson, json); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedProperties() + { + var result = Deserialize(SampleJson); + + result!.Type.Should().Be(MessageBatchResultType.Expired); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultTests.cs new file mode 100644 index 0000000..dd873d6 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultTests.cs @@ -0,0 +1,25 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class MessageBatchResultTests : SerializationTest +{ + [Fact] + public void JsonDeserialization_WhenHasUnknownType_ItShouldThrowException() + { + var json = @"{""type"":""unknown""}"; + + var action = () => Deserialize(json); + + action.Should().Throw(); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var expectedJson = @"{""type"":""expired""}"; + var messageBatchResult = new ExpiredMessageBatchResult(); + + var json = Serialize(messageBatchResult); + + JsonAssert.Equal(expectedJson, json); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/ToolCallTests.cs b/tests/AnthropicClient.Tests/Unit/Models/ToolCallTests.cs index 97240a4..a338a05 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/ToolCallTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/ToolCallTests.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Nodes; - namespace AnthropicClient.Tests.Unit.Models; public class ToolCallTests : SerializationTest diff --git a/tests/AnthropicClient.Tests/Unit/SerializationTest.cs b/tests/AnthropicClient.Tests/Unit/SerializationTest.cs index 06dd487..8fdd0df 100644 --- a/tests/AnthropicClient.Tests/Unit/SerializationTest.cs +++ b/tests/AnthropicClient.Tests/Unit/SerializationTest.cs @@ -1,5 +1,3 @@ -using AnthropicClient.Json; - namespace AnthropicClient.Tests.Unit; public class SerializationTest