File: Logging\AcceptanceTests.Mvc.cs
Web Access
Project: src\test\Libraries\Microsoft.AspNetCore.Diagnostics.Middleware.Tests\Microsoft.AspNetCore.Diagnostics.Middleware.Tests.csproj (Microsoft.AspNetCore.Diagnostics.Middleware.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#if NET8_0_OR_GREATER
 
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Compliance.Classification;
using Microsoft.Extensions.Compliance.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http.Diagnostics;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Shared.Text;
using Xunit;
 
namespace Microsoft.AspNetCore.Diagnostics.Logging.Test;
 
public partial class AcceptanceTests
{
    private const string RedactedFormat = "<redacted:{0}>";
    private const string UserIdParamName = "userId";
    private const string NoDataClassParamName = "noDataClassification";
    private const string QueryParamName = "noRedaction";
 
    internal const string ActionRouteTemplate = "api/users/{userId}/{noDataClassification}";
    internal const int ControllerProcessingTimeMs = 1_000;
 
    [SuppressMessage("Design", "CA1052:Static holder types should be Static or NotInheritable", Justification = "Needed for reflection")]
    private class TestStartupWithControllers
    {
        [SuppressMessage("Major Code Smell", "S1144:Unused private types or members should be removed", Justification = "Used through reflection")]
        public static void ConfigureServices(IServiceCollection services)
            => services
                .AddFakeRedaction(x => x.RedactionFormat = RedactedFormat)
                .AddRouting() // Adds routing middleware.
                .AddControllers(); // Allows to read routes from classes annotated with [ApiController] attribute.
 
        [SuppressMessage("Major Code Smell", "S1144:Unused private types or members should be removed", Justification = "Used through reflection")]
        public static void Configure(IApplicationBuilder app)
            => app
                .UseRouting()
                .UseHttpLogging()
                .UseEndpoints(endpoints => endpoints.MapControllers());
    }
 
    private static Task RunControllerAsync(LogLevel level, Action<IServiceCollection> configure, Func<FakeLogCollector, HttpClient, Task> func)
        => RunAsync<TestStartupWithControllers>(level, configure, (collector, client, _) => func(collector, client));
 
    [Theory]
    [InlineData(HttpRouteParameterRedactionMode.Strict, $"api/users/<redacted:testUserId>/{TelemetryConstants.Redacted}")]
    [InlineData(HttpRouteParameterRedactionMode.Loose, "api/users/<redacted:testUserId>/someTestData")]
    public async Task TestServer_WhenController_RedactPath(HttpRouteParameterRedactionMode mode, string redactedPath)
    {
        await RunControllerAsync(
            LogLevel.Information,
            services => services.AddHttpLoggingRedaction(o => o.RequestPathParameterRedactionMode = mode),
            async (logCollector, client) =>
            {
                const string UserId = "testUserId";
                using var response = await client.GetAsync($"/api/users/{UserId}/someTestData?{QueryParamName}=foo").ConfigureAwait(false);
                Assert.True(response.IsSuccessStatusCode);
 
                await WaitForLogRecordsAsync(logCollector, TimeSpan.FromSeconds(30));
 
                Assert.Equal(1, logCollector.Count);
 
                var logRecord = logCollector.LatestRecord;
                Assert.Null(logRecord.Exception);
                Assert.Equal(LoggingCategory, logRecord.Category);
                Assert.Equal(LogLevel.Information, logRecord.Level);
 
                var responseStatus = ((int)response.StatusCode).ToInvariantString();
                var state = logRecord.StructuredState!;
 
                Assert.DoesNotContain(state, x => x.Key == QueryParamName);
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Host && !string.IsNullOrEmpty(x.Value));
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Path && x.Value == redactedPath);
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.StatusCode && x.Value == responseStatus);
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Method && x.Value == HttpMethod.Get.ToString());
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Duration &&
                    x.Value != null &&
                    int.Parse(x.Value, CultureInfo.InvariantCulture) == ControllerProcessingTimeMs);
            });
    }
 
    [Fact]
    public async Task TestServer_WhenControllerWithPathRoute_RedactParameters()
    {
        await RunControllerAsync(
            LogLevel.Information,
            services => services.AddHttpLoggingRedaction(x => x.RequestPathLoggingMode = IncomingPathLoggingMode.Structured),
            async (logCollector, client) =>
            {
                const string UserId = "testUserId";
                using var response = await client.GetAsync($"/api/users/{UserId}/someTestData?{QueryParamName}=foo").ConfigureAwait(false);
                Assert.True(response.IsSuccessStatusCode);
 
                await WaitForLogRecordsAsync(logCollector, TimeSpan.FromSeconds(30));
 
                Assert.Equal(1, logCollector.Count);
 
                var logRecord = logCollector.LatestRecord;
                Assert.Null(logRecord.Exception);
                Assert.Equal(LoggingCategory, logRecord.Category);
                Assert.Equal(LogLevel.Information, logRecord.Level);
 
                var responseStatus = ((int)response.StatusCode).ToInvariantString();
                var state = logRecord.StructuredState!;
 
                string redactedUserId = string.Format(CultureInfo.InvariantCulture, RedactedFormat, UserId);
                Assert.Single(state, x => x.Key == UserIdParamName && x.Value == redactedUserId);
                Assert.Single(state, x => x.Key == NoDataClassParamName && x.Value == TelemetryConstants.Redacted);
                Assert.DoesNotContain(state, x => x.Key == QueryParamName);
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Host && !string.IsNullOrEmpty(x.Value));
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Path && x.Value == ActionRouteTemplate);
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.StatusCode && x.Value == responseStatus);
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Method && x.Value == HttpMethod.Get.ToString());
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Duration &&
                    x.Value != null &&
                    int.Parse(x.Value, CultureInfo.InvariantCulture) == ControllerProcessingTimeMs);
            });
    }
 
    [Theory]
    [CombinatorialData]
    public async Task TestServer_WhenControllerWithPathRoute_HonorRouteParamDataClassMap(bool routeParameterRedactionModeNone)
    {
        await RunControllerAsync(
            LogLevel.Information,
            services => services.AddHttpLoggingRedaction(x =>
            {
                x.RouteParameterDataClasses.Add(new(NoDataClassParamName, DataClassification.None));
                x.RequestPathLoggingMode = IncomingPathLoggingMode.Structured;
                x.RequestPathParameterRedactionMode = routeParameterRedactionModeNone
                    ? HttpRouteParameterRedactionMode.None : HttpRouteParameterRedactionMode.Strict;
            }),
            async (logCollector, client) =>
            {
                const string UserId = "testUserId";
                const string NoDataClassParamValue = "someTestData";
                using var response = await client.GetAsync($"/api/users/{UserId}/{NoDataClassParamValue}?{QueryParamName}=foo").ConfigureAwait(false);
                Assert.True(response.IsSuccessStatusCode);
 
                await WaitForLogRecordsAsync(logCollector, TimeSpan.FromSeconds(30));
 
                Assert.Equal(1, logCollector.Count);
 
                var logRecord = logCollector.LatestRecord;
                Assert.Null(logRecord.Exception);
                Assert.Equal(LoggingCategory, logRecord.Category);
                Assert.Equal(LogLevel.Information, logRecord.Level);
 
                var responseStatus = ((int)response.StatusCode).ToInvariantString();
                var state = logRecord.StructuredState!;
 
                string redactedUserId = string.Format(CultureInfo.InvariantCulture, RedactedFormat, UserId);
 
                if (!routeParameterRedactionModeNone)
                {
                    Assert.Single(state, x => x.Key == UserIdParamName && x.Value == redactedUserId);
                    Assert.Single(state, x => x.Key == NoDataClassParamName && x.Value == NoDataClassParamValue);
                }
 
                Assert.DoesNotContain(state, x => x.Key == QueryParamName);
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Host && !string.IsNullOrEmpty(x.Value));
                var expectedPath = routeParameterRedactionModeNone ? $"/api/users/{UserId}/{NoDataClassParamValue}" : ActionRouteTemplate;
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Path && x.Value == expectedPath);
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.StatusCode && x.Value == responseStatus);
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Method && x.Value == HttpMethod.Get.ToString());
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Duration &&
                    x.Value != null &&
                    int.Parse(x.Value, CultureInfo.InvariantCulture) == ControllerProcessingTimeMs);
            });
    }
 
    [Fact]
    public async Task TestServer_WhenControllerWithPathRoute_RedactionModeNone()
    {
        await RunControllerAsync(
            LogLevel.Information,
            services => services.AddHttpLoggingRedaction(x => x.RequestPathParameterRedactionMode = HttpRouteParameterRedactionMode.None),
            async (logCollector, client) =>
            {
                const string UserId = "testUserId";
                using var response = await client.GetAsync($"/api/users/{UserId}/someTestData?{QueryParamName}=foo").ConfigureAwait(false);
                Assert.True(response.IsSuccessStatusCode);
 
                await WaitForLogRecordsAsync(logCollector, TimeSpan.FromSeconds(30));
 
                Assert.Equal(1, logCollector.Count);
 
                var logRecord = logCollector.LatestRecord;
                Assert.Null(logRecord.Exception);
                Assert.Equal(LoggingCategory, logRecord.Category);
                Assert.Equal(LogLevel.Information, logRecord.Level);
 
                var responseStatus = ((int)response.StatusCode).ToInvariantString();
                var state = logRecord.StructuredState!;
 
                Assert.DoesNotContain(state, x => x.Key == QueryParamName);
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Host && !string.IsNullOrEmpty(x.Value));
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Path && x.Value == $"/api/users/testUserId/someTestData");
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.StatusCode && x.Value == responseStatus);
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Method && x.Value == HttpMethod.Get.ToString());
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Duration &&
                    x.Value != null &&
                    int.Parse(x.Value, CultureInfo.InvariantCulture) == ControllerProcessingTimeMs);
            });
    }
 
    [Theory]
    [CombinatorialData]
    public async Task TestServer_WhenControllerWithoutPathRoute_LogPath(bool routeParameterRedactionModeNone)
    {
        const string RequestPath = $"/api/test/1/2/3";
 
        await RunControllerAsync(
            LogLevel.Information,
            services => services.AddHttpLoggingRedaction(x =>
            {
                x.RequestPathParameterRedactionMode = routeParameterRedactionModeNone
                    ? HttpRouteParameterRedactionMode.None : HttpRouteParameterRedactionMode.Strict;
            }),
            async (logCollector, client) =>
            {
                using var response = await client.GetAsync(RequestPath).ConfigureAwait(false);
                Assert.False(response.IsSuccessStatusCode);
 
                await WaitForLogRecordsAsync(logCollector, TimeSpan.FromSeconds(30));
 
                Assert.Equal(1, logCollector.Count);
 
                var logRecord = logCollector.LatestRecord;
                Assert.Null(logRecord.Exception);
                Assert.Equal(LoggingCategory, logRecord.Category);
                Assert.Equal(LogLevel.Information, logRecord.Level);
 
                var responseStatus = ((int)response.StatusCode).ToInvariantString();
                var state = logRecord.StructuredState!;
 
                var expectedPath = routeParameterRedactionModeNone ? RequestPath : TelemetryConstants.Unknown;
 
                Assert.DoesNotContain(state, x => x.Key == QueryParamName);
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Host && !string.IsNullOrEmpty(x.Value));
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Path && x.Value == expectedPath);
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.StatusCode && x.Value == responseStatus);
                Assert.Single(state, x => x.Key == HttpLoggingTagNames.Method && x.Value == HttpMethod.Get.ToString());
            });
    }
}
#endif