// ExtractionPluginTests.cs -- Tests for the extraction plugin's schema validation // and external API tool methods. // // ValidateSchema is tested directly (no external APIs needed). // LookupCounterparty, ValidateTrade, and ValidateCurrency are tested with // mocked HttpClients using MockHttpMessageHandler to simulate API responses. using System.Net; using System.Net.Http.Json; using System.Text.Json; using ChatAgent.Api.Plugins; using ChatAgent.Api.Services; using ChatAgent.Shared.Models; using Moq; using Moq.Protected; namespace ChatAgent.Api.Tests; public class ExtractionPluginTests { // Helper to create an ExtractionPlugin with mocked HttpClients private static ExtractionPlugin CreatePluginWithMocks( HttpMessageHandler? counterpartyHandler = null, HttpMessageHandler? tradeHandler = null, HttpMessageHandler? currencyHandler = null) { var counterpartyClient = new CounterpartyApiClient( new HttpClient(counterpartyHandler ?? CreateOkHandler("[]")) { BaseAddress = new Uri("http://test/") }); var tradeClient = new TradeApiClient( new HttpClient(tradeHandler ?? CreateOkHandler("{\"isValid\":true}")) { BaseAddress = new Uri("http://test/") }); var currencyClient = new CurrencyApiClient( new HttpClient(currencyHandler ?? CreateOkHandler("{\"isValid\":true}")) { BaseAddress = new Uri("http://test/") }); return new ExtractionPlugin(counterpartyClient, tradeClient, currencyClient); } // Creates an HttpMessageHandler that returns a 200 OK with the given JSON body private static HttpMessageHandler CreateOkHandler(string jsonBody) { var mock = new Mock(); mock.Protected() .Setup>( "SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(jsonBody, System.Text.Encoding.UTF8, "application/json") }); return mock.Object; } // Creates an HttpMessageHandler that throws HttpRequestException private static HttpMessageHandler CreateErrorHandler() { var mock = new Mock(); mock.Protected() .Setup>( "SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) .ThrowsAsync(new HttpRequestException("Connection refused")); return mock.Object; } // --- ValidateSchema tests --- [Fact] public void ValidateSchema_ValidResult_ReturnsValid() { var plugin = CreatePluginWithMocks(); var extraction = new ExtractionResult { Items = new List { new() { Valuedate = "27/11/2025", Counterparty = "Assured Guaranty UK Limited", TradeId = 79353083, DisplayCcy = "GBP", Pv = 4562456.0, Breakclause = "N" } } }; var resultJson = plugin.ValidateSchema(JsonSerializer.Serialize(extraction)); var result = JsonSerializer.Deserialize(resultJson); Assert.NotNull(result); Assert.True(result.IsValid); Assert.Empty(result.Errors); } [Fact] public void ValidateSchema_MissingFields_ReturnsErrors() { var plugin = CreatePluginWithMocks(); // Missing counterparty and display_ccy var extraction = new ExtractionResult { Items = new List { new() { Valuedate = "27/11/2025", TradeId = 79353083, Pv = 4562456.0, Breakclause = "N" } } }; var resultJson = plugin.ValidateSchema(JsonSerializer.Serialize(extraction)); var result = JsonSerializer.Deserialize(resultJson); Assert.NotNull(result); Assert.False(result.IsValid); Assert.Contains(result.Errors, e => e.Contains("counterparty")); Assert.Contains(result.Errors, e => e.Contains("display_ccy")); } [Fact] public void ValidateSchema_InvalidBreakclause_ReturnsError() { var plugin = CreatePluginWithMocks(); var extraction = new ExtractionResult { Items = new List { new() { Valuedate = "27/11/2025", Counterparty = "Test Corp", TradeId = 12345, DisplayCcy = "GBP", Pv = 100.0, Breakclause = "Maybe" } } }; var resultJson = plugin.ValidateSchema(JsonSerializer.Serialize(extraction)); var result = JsonSerializer.Deserialize(resultJson); Assert.NotNull(result); Assert.False(result.IsValid); Assert.Contains(result.Errors, e => e.Contains("'Y' or 'N'")); } [Fact] public void ValidateSchema_InvalidJson_ReturnsError() { var plugin = CreatePluginWithMocks(); var resultJson = plugin.ValidateSchema("not valid json"); var result = JsonSerializer.Deserialize(resultJson); Assert.NotNull(result); Assert.False(result.IsValid); Assert.Contains(result.Errors, e => e.Contains("Invalid JSON")); } [Fact] public void ValidateSchema_EmptyItems_ReturnsError() { var plugin = CreatePluginWithMocks(); var resultJson = plugin.ValidateSchema("{\"items\":[]}"); var result = JsonSerializer.Deserialize(resultJson); Assert.NotNull(result); Assert.False(result.IsValid); Assert.Contains(result.Errors, e => e.Contains("no items")); } // --- LookupCounterparty tests --- [Fact] public async Task LookupCounterparty_SingleMatch_ReturnsValid() { var candidates = new List { new() { Name = "Assured Guaranty UK Ltd", LegalEntity = "AG-UK-001" } }; var handler = CreateOkHandler(JsonSerializer.Serialize(candidates)); var plugin = CreatePluginWithMocks(counterpartyHandler: handler); var resultJson = await plugin.LookupCounterparty("Assured Guaranty"); var result = JsonSerializer.Deserialize(resultJson); Assert.NotNull(result); Assert.True(result.IsValid); Assert.NotNull(result.Candidates); Assert.Single(result.Candidates); } [Fact] public async Task LookupCounterparty_MultipleMatches_ReturnsInvalidWithCandidates() { var candidates = new List { new() { Name = "Assured Guaranty UK Ltd", LegalEntity = "AG-UK-001" }, new() { Name = "Assured Guaranty EU Ltd", LegalEntity = "AG-EU-002" } }; var handler = CreateOkHandler(JsonSerializer.Serialize(candidates)); var plugin = CreatePluginWithMocks(counterpartyHandler: handler); var resultJson = await plugin.LookupCounterparty("Assured Guaranty"); var result = JsonSerializer.Deserialize(resultJson); Assert.NotNull(result); Assert.False(result.IsValid); Assert.NotNull(result.Candidates); Assert.Equal(2, result.Candidates.Count); } [Fact] public async Task LookupCounterparty_ApiError_ReturnsErrorMessage() { var plugin = CreatePluginWithMocks(counterpartyHandler: CreateErrorHandler()); var resultJson = await plugin.LookupCounterparty("Test"); var result = JsonSerializer.Deserialize(resultJson); Assert.NotNull(result); Assert.False(result.IsValid); Assert.Contains(result.Errors, e => e.Contains("Counterparty API unavailable")); } // --- ValidateTrade tests --- [Fact] public async Task ValidateTrade_ValidId_ReturnsValid() { var handler = CreateOkHandler("{\"isValid\":true}"); var plugin = CreatePluginWithMocks(tradeHandler: handler); var resultJson = await plugin.ValidateTrade(79353083); var result = JsonSerializer.Deserialize(resultJson); Assert.NotNull(result); Assert.True(result.IsValid); } [Fact] public async Task ValidateTrade_InvalidId_ReturnsError() { var handler = CreateOkHandler("{\"isValid\":false,\"message\":\"Trade not found\"}"); var plugin = CreatePluginWithMocks(tradeHandler: handler); var resultJson = await plugin.ValidateTrade(99999); var result = JsonSerializer.Deserialize(resultJson); Assert.NotNull(result); Assert.False(result.IsValid); Assert.Contains(result.Errors, e => e.Contains("Trade not found")); } [Fact] public async Task ValidateTrade_ApiError_ReturnsErrorMessage() { var plugin = CreatePluginWithMocks(tradeHandler: CreateErrorHandler()); var resultJson = await plugin.ValidateTrade(12345); var result = JsonSerializer.Deserialize(resultJson); Assert.NotNull(result); Assert.False(result.IsValid); Assert.Contains(result.Errors, e => e.Contains("Trade validation API unavailable")); } // --- ValidateCurrency tests --- [Fact] public async Task ValidateCurrency_ValidCode_ReturnsValid() { var handler = CreateOkHandler("{\"isValid\":true}"); var plugin = CreatePluginWithMocks(currencyHandler: handler); var resultJson = await plugin.ValidateCurrency("GBP"); var result = JsonSerializer.Deserialize(resultJson); Assert.NotNull(result); Assert.True(result.IsValid); } [Fact] public async Task ValidateCurrency_InvalidCode_ReturnsError() { var handler = CreateOkHandler("{\"isValid\":false,\"message\":\"Unknown currency\",\"suggestions\":[\"GBP\",\"GEL\"]}"); var plugin = CreatePluginWithMocks(currencyHandler: handler); var resultJson = await plugin.ValidateCurrency("GBX"); var result = JsonSerializer.Deserialize(resultJson); Assert.NotNull(result); Assert.False(result.IsValid); Assert.Contains(result.Errors, e => e.Contains("GBP")); } [Fact] public async Task ValidateCurrency_ApiError_ReturnsErrorMessage() { var plugin = CreatePluginWithMocks(currencyHandler: CreateErrorHandler()); var resultJson = await plugin.ValidateCurrency("USD"); var result = JsonSerializer.Deserialize(resultJson); Assert.NotNull(result); Assert.False(result.IsValid); Assert.Contains(result.Errors, e => e.Contains("Currency validation API unavailable")); } }