File: VerbatimHttpHandler.cs
Web Access
Project: src\test\Libraries\Microsoft.Extensions.AI.Integration.Tests\Microsoft.Extensions.AI.Integration.Tests.csproj (Microsoft.Extensions.AI.Integration.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
 
#pragma warning disable CA2000 // Dispose objects before losing scope
#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods
#pragma warning disable CA1031 // Do not catch general exception types
#pragma warning disable S108 // Nested blocks of code should not be left empty
 
namespace Microsoft.Extensions.AI;
 
/// <summary>
/// An <see cref="HttpMessageHandler"/> that checks the request body against an expected one
/// and sends back an expected response.
/// </summary>
public sealed class VerbatimHttpHandler(string expectedInput, string expectedOutput, bool validateExpectedResponse = false) :
    DelegatingHandler(new HttpClientHandler())
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Assert.NotNull(request.Content);
 
        string? actualInput = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
 
        Assert.NotNull(actualInput);
        AssertEqualNormalized(expectedInput, actualInput);
 
        if (validateExpectedResponse)
        {
            ByteArrayContent newContent = new(Encoding.UTF8.GetBytes(actualInput));
            foreach (var header in request.Content.Headers)
            {
                newContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
            }
 
            request.Content = newContent;
 
            using var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
            string? actualOutput = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
 
            Assert.NotNull(actualOutput);
            AssertEqualNormalized(expectedOutput, actualOutput);
        }
 
        return new() { Content = new StringContent(expectedOutput) };
    }
 
    public static string? RemoveWhiteSpace(string? text) =>
        text is null ? null :
        Regex.Replace(text, @"\s*", string.Empty);
 
    private static void AssertEqualNormalized(string expected, string actual)
    {
        // First try to compare as JSON.
        JsonNode? expectedNode = null;
        JsonNode? actualNode = null;
        try
        {
            expectedNode = JsonNode.Parse(expected);
            actualNode = JsonNode.Parse(actual);
        }
        catch
        {
        }
 
        if (expectedNode is not null && actualNode is not null)
        {
            if (!JsonNode.DeepEquals(expectedNode, actualNode))
            {
                FailNotEqual(expected, actual);
            }
 
            return;
        }
 
        // Legitimately may not have been JSON. Fall back to whitespace normalization.
        if (RemoveWhiteSpace(expected) != RemoveWhiteSpace(actual))
        {
            FailNotEqual(expected, actual);
        }
    }
 
    private static void FailNotEqual(string expected, string actual) =>
        Assert.Fail(
            $"Expected:{Environment.NewLine}" +
            $"{expected}{Environment.NewLine}" +
            $"Actual:{Environment.NewLine}" +
            $"{actual}");
}