File: Logging\HttpClientLoggingExtensionsTest.cs
Web Access
Project: src\test\Libraries\Microsoft.Extensions.Http.Diagnostics.Tests\Microsoft.Extensions.Http.Diagnostics.Tests.csproj (Microsoft.Extensions.Http.Diagnostics.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.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using AutoFixture;
using FluentAssertions;
using Microsoft.Extensions.Compliance.Classification;
using Microsoft.Extensions.Compliance.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.Testing;
using Microsoft.Extensions.Http.Diagnostics;
using Microsoft.Extensions.Http.Logging.Test.Internal;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
 
namespace Microsoft.Extensions.Http.Logging.Test;
 
public class HttpClientLoggingExtensionsTest
{
    private readonly Fixture _fixture;
 
    public HttpClientLoggingExtensionsTest()
    {
        _fixture = new Fixture();
    }
 
    [Fact]
    public void AddHttpClientLogging_AnyArgumentIsNull_Throws()
    {
        var act = () => ((IHttpClientBuilder)null!).AddExtendedHttpClientLogging();
        act.Should().Throw<ArgumentNullException>();
 
        act = () => ((IHttpClientBuilder)null!).AddExtendedHttpClientLogging(_ => { });
        act.Should().Throw<ArgumentNullException>();
 
        act = () => ((IHttpClientBuilder)null!).AddExtendedHttpClientLogging(Mock.Of<IConfigurationSection>());
        act.Should().Throw<ArgumentNullException>();
 
        act = () => Mock.Of<IHttpClientBuilder>().AddExtendedHttpClientLogging((Action<LoggingOptions>)null!);
        act.Should().Throw<ArgumentNullException>();
 
        act = () => Mock.Of<IHttpClientBuilder>().AddExtendedHttpClientLogging((IConfigurationSection)null!);
        act.Should().Throw<ArgumentNullException>();
    }
 
    [Fact]
    public void AddHttpClientLogging_ServiceCollection_AnyArgumentIsNull_Throws()
    {
        var act = () => ((IServiceCollection)null!).AddExtendedHttpClientLogging();
        act.Should().Throw<ArgumentNullException>();
 
        act = () => ((IServiceCollection)null!).AddExtendedHttpClientLogging(_ => { });
        act.Should().Throw<ArgumentNullException>();
 
        act = () => ((IServiceCollection)null!).AddExtendedHttpClientLogging(Mock.Of<IConfigurationSection>());
        act.Should().Throw<ArgumentNullException>();
 
        act = () => Mock.Of<IServiceCollection>().AddExtendedHttpClientLogging((Action<LoggingOptions>)null!);
        act.Should().Throw<ArgumentNullException>();
 
        act = () => Mock.Of<IServiceCollection>().AddExtendedHttpClientLogging((IConfigurationSection)null!);
        act.Should().Throw<ArgumentNullException>();
    }
 
    [Fact]
    public void AddHttpClientLogEnricher_AnyArgumentIsNull_Throws()
    {
        var act = () => ((IServiceCollection)null!).AddHttpClientLogEnricher<EmptyEnricher>();
        act.Should().Throw<ArgumentNullException>();
    }
 
    [Fact]
    public void AddHttpClientLogging_ConfiguredOptionsWithNamedClient_ShouldNotBeSame()
    {
        var services = new ServiceCollection();
 
        using var provider = services
            .AddHttpClient("test1")
            .AddExtendedHttpClientLogging(options => options.BodyReadTimeout = TimeSpan.FromSeconds(1))
            .Services
            .AddHttpClient("test2")
            .AddExtendedHttpClientLogging(options => options.BodyReadTimeout = TimeSpan.FromSeconds(2))
            .Services
            .BuildServiceProvider();
 
        var optionsFirst = provider.GetRequiredService<IOptionsMonitor<LoggingOptions>>().Get("test1");
        var optionsSecond = provider.GetRequiredService<IOptionsMonitor<LoggingOptions>>().Get("test2");
        optionsFirst.Should().NotBeNull();
        optionsSecond.Should().NotBeNull();
        optionsFirst.Should().NotBeEquivalentTo(optionsSecond);
        optionsFirst.BodyReadTimeout.Should().Be(TimeSpan.FromSeconds(1));
        optionsSecond.BodyReadTimeout.Should().Be(TimeSpan.FromSeconds(2));
    }
 
    [Fact]
    public void AddHttpClientLogging_ConfiguredOptionsWithTypedClient_ShouldNotBeSame()
    {
        var services = new ServiceCollection();
 
        using var provider = services
            .AddHttpClient<ITestHttpClient1, TestHttpClient1>()
            .AddExtendedHttpClientLogging(options => options.BodyReadTimeout = TimeSpan.FromSeconds(1))
            .Services
            .AddHttpClient<ITestHttpClient2, TestHttpClient2>()
            .AddExtendedHttpClientLogging(options => options.BodyReadTimeout = TimeSpan.FromSeconds(2))
            .Services
            .BuildServiceProvider();
 
        var optionsFirst = provider.GetRequiredService<IOptionsMonitor<LoggingOptions>>().Get(nameof(ITestHttpClient1));
        var optionsSecond = provider.GetRequiredService<IOptionsMonitor<LoggingOptions>>().Get(nameof(ITestHttpClient2));
        optionsFirst.Should().NotBeNull();
        optionsSecond.Should().NotBeNull();
        optionsFirst.Should().NotBeEquivalentTo(optionsSecond);
        optionsFirst.BodyReadTimeout.Should().Be(TimeSpan.FromSeconds(1));
        optionsSecond.BodyReadTimeout.Should().Be(TimeSpan.FromSeconds(2));
    }
 
    [Fact]
    public void AddHttpClientLogging_DefaultOptions_CreatesOptionsCorrectly()
    {
        var services = new ServiceCollection();
 
        using var provider = services
            .AddHttpClient("")
            .AddExtendedHttpClientLogging(o => o.RequestHeadersDataClasses.Add("test1", FakeTaxonomy.PrivateData))
            .Services
            .AddHttpClient("")
            .AddExtendedHttpClientLogging(o => o.RequestHeadersDataClasses.Add("test2", FakeTaxonomy.PrivateData))
            .Services
            .BuildServiceProvider();
 
        var options = provider.GetRequiredService<IOptions<LoggingOptions>>().Value;
        options.RequestHeadersDataClasses.Should().HaveCount(2);
        options.RequestHeadersDataClasses.Should().ContainKeys(new List<string> { "test1", "test2" });
        options.RequestHeadersDataClasses.Should().ContainValues(new List<DataClassification> { FakeTaxonomy.PrivateData });
    }
 
    [Fact]
    public void AddHttpClientLogging_GivenActionDelegate_RegistersInDi()
    {
        var requestBodyContentType = "application/json";
        var responseBodyContentType = "application/json";
        var requestHeader = _fixture.Create<string>();
        var responseHeader = _fixture.Create<string>();
        var bodyReadTimeout = TimeSpan.FromSeconds(1);
        var bodySizeLimit = 100;
        var formatRequestPath = _fixture.Create<OutgoingPathLoggingMode>();
        var formatRequestPathParameters = _fixture.Create<HttpRouteParameterRedactionMode>();
        var logStart = _fixture.Create<bool>();
        var paramToRedact = new KeyValuePair<string, DataClassification>("userId", FakeTaxonomy.PrivateData);
 
        var services = new ServiceCollection();
 
        services
            .AddHttpClient("test")
            .AddExtendedHttpClientLogging(options =>
            {
                options.RequestBodyContentTypes.Add(requestBodyContentType);
                options.ResponseBodyContentTypes.Add(responseBodyContentType);
                options.BodyReadTimeout = bodyReadTimeout;
                options.BodySizeLimit = bodySizeLimit;
                options.RequestPathLoggingMode = formatRequestPath;
                options.RequestPathParameterRedactionMode = formatRequestPathParameters;
                options.RequestHeadersDataClasses.Add(requestHeader, FakeTaxonomy.PrivateData);
                options.ResponseHeadersDataClasses.Add(responseHeader, FakeTaxonomy.PrivateData);
                options.RouteParameterDataClasses.Add(paramToRedact);
                options.LogRequestStart = logStart;
            });
 
        using var provider = services.BuildServiceProvider();
        var options = provider.GetRequiredService<IOptionsMonitor<LoggingOptions>>().Get("test");
 
        options.Should().NotBeNull();
        options.RequestBodyContentTypes.Should().ContainSingle();
        options.RequestBodyContentTypes.Should().Contain(requestBodyContentType);
        options.ResponseBodyContentTypes.Should().ContainSingle();
        options.ResponseBodyContentTypes.Should().Contain(responseBodyContentType);
        options.BodyReadTimeout.Should().Be(bodyReadTimeout);
        options.BodySizeLimit.Should().Be(bodySizeLimit);
        options.RequestPathLoggingMode.Should().Be(formatRequestPath);
        options.RequestPathParameterRedactionMode.Should().Be(formatRequestPathParameters);
        options.RequestHeadersDataClasses.Should().ContainSingle();
        options.RequestHeadersDataClasses.Should().Contain(requestHeader, FakeTaxonomy.PrivateData);
        options.ResponseHeadersDataClasses.Should().ContainSingle();
        options.ResponseHeadersDataClasses.Should().Contain(responseHeader, FakeTaxonomy.PrivateData);
        options.RouteParameterDataClasses.Should().ContainSingle();
        options.RouteParameterDataClasses.Should().Contain(paramToRedact);
        options.LogRequestStart.Should().Be(logStart);
    }
 
    [Fact]
    public async Task AddHttpClientLogging_GivenInvalidOptions_Throws()
    {
        using var host = FakeHost.CreateBuilder()
            .ConfigureServices(services =>
            {
                services
                    .AddFakeRedaction()
                    .AddHttpClient("test")
                    .AddExtendedHttpClientLogging(options =>
                    {
                        options.BodyReadTimeout = TimeSpan.Zero;
                        options.BodySizeLimit = -1;
                    });
            })
            .Build();
 
        var act = async () => await host.StartAsync().ConfigureAwait(false);
        await act.Should().ThrowAsync<OptionsValidationException>();
    }
 
    [Theory]
    [InlineData(2)]
    [InlineData(5)]
    [InlineData(30)]
    [InlineData(59)]
    [InlineData(17)]
    public void AddHttpClientLogging_GivenConfigurationSection_SetsTimeoutCorrectly(int seconds)
    {
        var timeoutValue = TimeSpan.FromSeconds(seconds);
 
        using var provider = new ServiceCollection()
            .AddHttpClient("test")
            .AddExtendedHttpClientLogging(TestConfiguration.GetHttpClientLoggingConfigurationSection(timeoutValue))
            .Services
            .BuildServiceProvider();
        var options = provider
            .GetRequiredService<IOptionsMonitor<LoggingOptions>>().Get("test");
 
        options.Should().NotBeNull();
        options.BodyReadTimeout.Should().Be(timeoutValue);
    }
 
    [Fact]
    public void AddHttpClientLogEnricher_RegistersEnricherInDI()
    {
        using var provider = new ServiceCollection()
            .AddHttpClientLogEnricher<EmptyEnricher>()
            .BuildServiceProvider();
 
        var enricherRegistered = provider.GetService<IHttpClientLogEnricher>();
 
        enricherRegistered.Should().NotBeNull();
        enricherRegistered.Should().BeOfType<EmptyEnricher>();
    }
 
    [Fact]
    public void AddHttpClientLogging_ServiceCollection_GivenActionDelegate_RegistersInDi()
    {
        var requestBodyContentType = "application/json";
        var responseBodyContentType = "application/json";
        var requestHeader = _fixture.Create<string>();
        var responseHeader = _fixture.Create<string>();
        var bodyReadTimeout = TimeSpan.FromSeconds(1);
        var bodySizeLimit = 100;
        var formatRequestPath = _fixture.Create<OutgoingPathLoggingMode>();
        var formatRequestPathParameters = _fixture.Create<HttpRouteParameterRedactionMode>();
        var logStart = _fixture.Create<bool>();
        var paramToRedact = new KeyValuePair<string, DataClassification>("userId", FakeTaxonomy.PrivateData);
 
        var services = new ServiceCollection();
 
        services
            .AddFakeRedaction()
            .AddHttpClient()
            .AddExtendedHttpClientLogging(options =>
            {
                options.RequestBodyContentTypes.Add(requestBodyContentType);
                options.ResponseBodyContentTypes.Add(responseBodyContentType);
                options.BodyReadTimeout = bodyReadTimeout;
                options.BodySizeLimit = bodySizeLimit;
                options.RequestPathLoggingMode = formatRequestPath;
                options.RequestPathParameterRedactionMode = formatRequestPathParameters;
                options.RequestHeadersDataClasses.Add(requestHeader, FakeTaxonomy.PrivateData);
                options.ResponseHeadersDataClasses.Add(responseHeader, FakeTaxonomy.PrivateData);
                options.RouteParameterDataClasses.Add(paramToRedact);
                options.LogRequestStart = logStart;
            });
 
        using var provider = services.BuildServiceProvider();
        var options = provider.GetRequiredService<IOptions<LoggingOptions>>().Value;
 
        options.Should().NotBeNull();
        options.RequestBodyContentTypes.Should().ContainSingle();
        options.RequestBodyContentTypes.Should().Contain(requestBodyContentType);
        options.ResponseBodyContentTypes.Should().ContainSingle();
        options.ResponseBodyContentTypes.Should().Contain(responseBodyContentType);
        options.BodyReadTimeout.Should().Be(bodyReadTimeout);
        options.BodySizeLimit.Should().Be(bodySizeLimit);
        options.RequestPathLoggingMode.Should().Be(formatRequestPath);
        options.RequestPathParameterRedactionMode.Should().Be(formatRequestPathParameters);
        options.RequestHeadersDataClasses.Should().ContainSingle();
        options.RequestHeadersDataClasses.Should().Contain(requestHeader, FakeTaxonomy.PrivateData);
        options.ResponseHeadersDataClasses.Should().ContainSingle();
        options.ResponseHeadersDataClasses.Should().Contain(responseHeader, FakeTaxonomy.PrivateData);
        options.RouteParameterDataClasses.Should().ContainSingle();
        options.RouteParameterDataClasses.Should().Contain(paramToRedact);
        options.LogRequestStart.Should().Be(logStart);
 
        using var httpClient = provider.GetRequiredService<IHttpClientFactory>().CreateClient();
        Assert.NotNull(httpClient);
    }
 
    [Fact]
    public async Task AddHttpClientLogging_ServiceCollection_GivenInvalidOptions_Throws()
    {
        using var provider = new ServiceCollection()
            .AddFakeRedaction()
            .AddHttpClient()
            .AddExtendedHttpClientLogging(options =>
            {
                options.BodyReadTimeout = TimeSpan.Zero;
                options.BodySizeLimit = -1;
            })
            .BuildServiceProvider();
 
        var act = () =>
            provider
                .GetRequiredService<IHostedService>()
                .StartAsync(CancellationToken.None);
        await act.Should().ThrowAsync<OptionsValidationException>();
    }
 
    [Fact]
    public void AddHttpClientLogging_ServiceCollectionAndHttpClientBuilder_DoesNotDuplicate()
    {
        const string ClientName = "test";
 
        using var provider = new ServiceCollection()
            .AddFakeRedaction()
            .AddHttpClient(ClientName)
            .AddExtendedHttpClientLogging(x =>
            {
                x.BodySizeLimit = 100500;
                x.RequestHeadersDataClasses.Add(ClientName, FakeTaxonomy.PublicData);
            }).Services
            .AddExtendedHttpClientLogging(x =>
            {
                x.BodySizeLimit = 347;
                x.RequestHeadersDataClasses.Add("default", FakeTaxonomy.PrivateData);
            })
            .BuildServiceProvider();
 
        EnsureSingleLogger<HttpClientLogger>(provider, ClientName);
    }
 
    [Fact]
    public void AddHttpClientLogging_HttpClientBuilderAndServiceCollection_DoesNotDuplicate()
    {
        const string ClientName = "test";
 
        using var provider = new ServiceCollection()
            .AddFakeRedaction()
            .AddExtendedHttpClientLogging()
            .AddHttpClient(ClientName)
            .AddExtendedHttpClientLogging()
            .Services.BuildServiceProvider();
 
        EnsureSingleLogger<HttpClientLogger>(provider, ClientName);
    }
 
    [Theory]
    [InlineData(2)]
    [InlineData(5)]
    [InlineData(30)]
    [InlineData(59)]
    [InlineData(17)]
    public void AddHttpClientLogging_ServiceCollection_GivenConfigurationSection_SetsTimeoutCorrectly(int seconds)
    {
        var timeoutValue = TimeSpan.FromSeconds(seconds);
 
        using var provider = new ServiceCollection()
            .AddFakeRedaction()
            .AddHttpClient()
            .AddExtendedHttpClientLogging(TestConfiguration.GetHttpClientLoggingConfigurationSection(timeoutValue))
            .BuildServiceProvider();
        var options = provider
            .GetRequiredService<IOptions<LoggingOptions>>().Value;
 
        options.Should().NotBeNull();
        options.BodyReadTimeout.Should().Be(timeoutValue);
 
        using var httpClient = provider.GetRequiredService<IHttpClientFactory>().CreateClient();
        Assert.NotNull(httpClient);
    }
 
    [Fact]
    public void AddHttpClientLogging_ServiceCollection_CreatesClientSuccessfully()
    {
        using var sp = new ServiceCollection()
            .AddFakeRedaction()
            .AddHttpClient()
            .AddExtendedHttpClientLogging()
            .BuildServiceProvider();
 
        using var httpClient = sp.GetRequiredService<IHttpClientFactory>().CreateClient();
        Assert.NotNull(httpClient);
    }
 
    private static void EnsureSingleLogger<T>(IServiceProvider serviceProvider, string serviceKey)
        where T : IHttpClientLogger
    {
        var loggers = serviceProvider.GetServices<T>();
        loggers.Should().ContainSingle();
 
        var keyedLoggers = serviceProvider.GetKeyedServices<T>(serviceKey);
        keyedLoggers.Should().ContainSingle();
    }
}