File: Http\HttpRouteFormatterTests.cs
Web Access
Project: src\test\Libraries\Microsoft.Extensions.Telemetry.Tests\Microsoft.Extensions.Telemetry.Tests.csproj (Microsoft.Extensions.Telemetry.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 Microsoft.Extensions.Compliance.Classification;
using Microsoft.Extensions.Compliance.Testing;
using Microsoft.Extensions.Http.Diagnostics;
using Xunit;
 
namespace Microsoft.Extensions.Http.Diagnostics.Test;
 
public class HttpRouteFormatterTests
{
    [Theory]
    [CombinatorialData]
    public void Format_WithEmptyParametersToRedact_ReturnsOriginalPath(HttpRouteParameterRedactionMode redactionMode)
    {
        HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter();
        var parametersToRedact = new Dictionary<string, DataClassification>();
 
        string httpPath = "/api/routes/routeId123/chats/chatId123";
        string httpRoute = "/api/routes/{routeId}/chats/{chatId}";
        string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
 
        if (redactionMode == HttpRouteParameterRedactionMode.Strict)
        {
            Assert.Equal($"api/routes/{TelemetryConstants.Redacted}/chats/{TelemetryConstants.Redacted}", formattedPath);
        }
        else
        {
            Assert.Equal($"api/routes/routeId123/chats/chatId123", formattedPath);
        }
 
        httpPath = "api/routes/routeId123/chats/chatId123";
        httpRoute = "api/routes/{routeId}/chats/{chatId}";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
 
        if (redactionMode == HttpRouteParameterRedactionMode.Strict)
        {
            Assert.Equal($"api/routes/{TelemetryConstants.Redacted}/chats/{TelemetryConstants.Redacted}", formattedPath);
        }
        else
        {
            Assert.Equal($"api/routes/routeId123/chats/chatId123", formattedPath);
        }
 
        httpPath = "api/routes/routeId123/chats/chatId123";
        httpRoute = "api/routes/{routeId}/chats/{chatId}";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
 
        if (redactionMode == HttpRouteParameterRedactionMode.Strict)
        {
            Assert.Equal($"api/routes/{TelemetryConstants.Redacted}/chats/{TelemetryConstants.Redacted}", formattedPath);
        }
        else
        {
            Assert.Equal($"api/routes/routeId123/chats/chatId123", formattedPath);
        }
 
        httpPath = "/api/chats:chatId123@routeId123";
        httpRoute = "/api/chats:{chatId}@{routeId}";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
 
        if (redactionMode == HttpRouteParameterRedactionMode.Strict)
        {
            Assert.Equal($"api/chats:{TelemetryConstants.Redacted}@{TelemetryConstants.Redacted}", formattedPath);
        }
        else
        {
            Assert.Equal($"api/chats:chatId123@routeId123", formattedPath);
        }
 
        httpPath = "/api/chats:chatId123@routeId123/messages";
        httpRoute = "/api/chats:{chatId}@{routeId}/messages";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
 
        if (redactionMode == HttpRouteParameterRedactionMode.Strict)
        {
            Assert.Equal($"api/chats:{TelemetryConstants.Redacted}@{TelemetryConstants.Redacted}/messages", formattedPath);
        }
        else
        {
            Assert.Equal($"api/chats:chatId123@routeId123/messages", formattedPath);
        }
    }
 
    [Theory]
    [CombinatorialData]
    public void Format_NoParameterRoute_WithParametersToRedact_ReturnsOriginalpath(HttpRouteParameterRedactionMode redactionMode)
    {
        HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter();
        Dictionary<string, DataClassification> parametersToRedact = new()
        {
            { "userId", FakeTaxonomy.PrivateData },
            { "v1", FakeTaxonomy.PrivateData }
        };
 
        string httpPath = "/api/v1/chats";
        string httpRoute = "/api/v1/chats";
        string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal(httpPath.TrimStart('/'), formattedPath);
 
        // http path doesn't begin with / while route begins with /
        httpPath = "api/v1/chats";
        httpRoute = "/api/v1/chats";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal(httpPath.TrimStart('/'), formattedPath);
 
        // route doesn't begin with / while http path begins with /
        httpPath = "/api/v1/chats";
        httpRoute = "api/v1/chats";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal(httpPath.TrimStart('/'), formattedPath);
 
        // empty route
        httpPath = "/";
        httpRoute = "/";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal(httpPath.TrimStart('/'), formattedPath);
 
        // empty route
        httpPath = "";
        httpRoute = "/";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal(httpPath.TrimStart('/'), formattedPath);
    }
 
    [Theory]
    [InlineData(-1)]
    [InlineData(3)]
    [InlineData(4)]
    public void Format_WithParametersToRedact_GivenInvalidHttpRouteParameterRedactionMode_Throws(int redactionMode)
    {
        HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter();
        Dictionary<string, DataClassification> parametersToRedact = new()
        {
            { "userId", FakeTaxonomy.PrivateData },
            { "routeId", FakeTaxonomy.PrivateData }
        };
 
        string httpPath = "/api/routes/routeId123/chats/chatId123";
        string httpRoute = "/api/routes/{routeId}/chats/{chatId}";
        var ex = Assert.Throws<InvalidOperationException>(
            () => httpFormatter.Format(httpRoute, httpPath, (HttpRouteParameterRedactionMode)redactionMode, parametersToRedact));
        Assert.Equal(TelemetryCommonExtensions.UnsupportedEnumValueExceptionMessage, ex.Message);
    }
 
    [Theory]
    [CombinatorialData]
    public void Format_WithParametersToRedact_ReturnsPathWithSensitiveParamsRedacted(HttpRouteParameterRedactionMode redactionMode)
    {
        bool isRedacted = redactionMode != HttpRouteParameterRedactionMode.None;
        string redactedPrefix = isRedacted ? "Redacted:" : string.Empty;
 
        HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter();
        Dictionary<string, DataClassification> parametersToRedact = new()
        {
            { "userId", FakeTaxonomy.PrivateData },
            { "routeId", FakeTaxonomy.PrivateData }
        };
 
        string httpPath = "/api/routes/routeId123/chats/chatId123";
        string httpRoute = "/api/routes/{routeId}/chats/{chatId}";
        string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
 
        if (redactionMode == HttpRouteParameterRedactionMode.Strict)
        {
            Assert.Equal($"api/routes/Redacted:routeId123/chats/{TelemetryConstants.Redacted}", formattedPath);
        }
 
        if (redactionMode == HttpRouteParameterRedactionMode.Loose)
        {
            Assert.Equal($"api/routes/Redacted:routeId123/chats/chatId123", formattedPath);
        }
 
        if (redactionMode == HttpRouteParameterRedactionMode.None)
        {
            Assert.Equal($"api/routes/routeId123/chats/chatId123", formattedPath);
        }
 
        parametersToRedact.Add("chatId", FakeTaxonomy.PrivateData);
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal($"api/routes/{redactedPrefix}routeId123/chats/{redactedPrefix}chatId123", formattedPath);
 
        // path doesn't begin with forward slash, route does
        httpPath = "api/routes/routeId123/chats/chatId123";
        httpRoute = "/api/routes/{routeId}/chats/{chatId}";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal($"api/routes/{redactedPrefix}routeId123/chats/{redactedPrefix}chatId123", formattedPath);
 
        // route doesn't begin with forward slash, path does
        httpPath = "/api/routes/routeId123/chats/chatId123";
        httpRoute = "api/routes/{routeId}/chats/{chatId}";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal($"api/routes/{redactedPrefix}routeId123/chats/{redactedPrefix}chatId123", formattedPath);
 
        // route has no parameters that needs redaction
        parametersToRedact.Remove("routeId");
        parametersToRedact.Remove("chatId");
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
 
        if (redactionMode == HttpRouteParameterRedactionMode.Strict)
        {
            Assert.Equal($"api/routes/{TelemetryConstants.Redacted}/chats/{TelemetryConstants.Redacted}", formattedPath);
        }
        else
        {
            Assert.Equal($"api/routes/routeId123/chats/chatId123", formattedPath);
        }
    }
 
    [Theory]
    [CombinatorialData]
    public void Format_WithParametersToRedact_DataClassPublicNonPersonalData_DoesNotRedactParameters(HttpRouteParameterRedactionMode redactionMode)
    {
        HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter();
        Dictionary<string, DataClassification> parametersToRedact = new()
        {
            { "userId", DataClassification.None },
            { "routeId", DataClassification.None },
            { "chatId", DataClassification.None },
        };
 
        string httpPath = "/api/routes/routeId123/chats/chatId123";
        string httpRoute = "/api/routes/{routeId}/chats/{chatId}";
        string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal($"api/routes/routeId123/chats/chatId123", formattedPath);
    }
 
    [Theory]
    [CombinatorialData]
    public void Format_WithParametersToRedact_PublicNonPersonalData_NotRedacted(HttpRouteParameterRedactionMode redactionMode)
    {
        bool isRedacted = redactionMode != HttpRouteParameterRedactionMode.None;
        string redactedPrefix = isRedacted ? "Redacted:" : string.Empty;
 
        HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter();
        Dictionary<string, DataClassification> parametersToRedact = new()
        {
            { "userId", FakeTaxonomy.PrivateData },
            { "routeId", DataClassification.None },
            { "chatId", FakeTaxonomy.PrivateData },
        };
 
        string httpPath = "/api/routes/routeId123/chats/chatId123";
        string httpRoute = "/api/routes/{routeId}/chats/{chatId}";
        string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal($"api/routes/routeId123/chats/{redactedPrefix}chatId123", formattedPath);
    }
 
    [Theory]
    [CombinatorialData]
    public void Format_RouteHasFirstParameterToBeRedacted_ReturnsCorrectlyRedactedPath(HttpRouteParameterRedactionMode redactionMode)
    {
        HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter();
        Dictionary<string, DataClassification> parametersToRedact = new() { { "routeId", FakeTaxonomy.PrivateData } };
 
        string httpPath = "routeId123/chats/chatId123";
        string httpRoute = "{routeId}/chats/{chatId}";
        string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
 
        if (redactionMode == HttpRouteParameterRedactionMode.Strict)
        {
            Assert.Equal($"Redacted:routeId123/chats/{TelemetryConstants.Redacted}", formattedPath);
        }
 
        if (redactionMode == HttpRouteParameterRedactionMode.Loose)
        {
            Assert.Equal($"Redacted:routeId123/chats/chatId123", formattedPath);
        }
 
        if (redactionMode == HttpRouteParameterRedactionMode.None)
        {
            Assert.Equal($"routeId123/chats/chatId123", formattedPath);
        }
 
        // path begins with forward slash, route doesn't
        httpPath = "/routeId123/chats/chatId123";
        httpRoute = "{routeId}/chats/{chatId}";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
 
        if (redactionMode == HttpRouteParameterRedactionMode.Strict)
        {
            Assert.Equal($"Redacted:routeId123/chats/{TelemetryConstants.Redacted}", formattedPath);
        }
 
        if (redactionMode == HttpRouteParameterRedactionMode.Loose)
        {
            Assert.Equal($"Redacted:routeId123/chats/chatId123", formattedPath);
        }
 
        if (redactionMode == HttpRouteParameterRedactionMode.None)
        {
            Assert.Equal($"routeId123/chats/chatId123", formattedPath);
        }
 
        // route begins with forward slash, path doesn't
        httpPath = "routeId123/chats/chatId123";
        httpRoute = "/{routeId}/chats/{chatId}";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
 
        if (redactionMode == HttpRouteParameterRedactionMode.Strict)
        {
            Assert.Equal($"Redacted:routeId123/chats/{TelemetryConstants.Redacted}", formattedPath);
        }
 
        if (redactionMode == HttpRouteParameterRedactionMode.Loose)
        {
            Assert.Equal($"Redacted:routeId123/chats/chatId123", formattedPath);
        }
 
        if (redactionMode == HttpRouteParameterRedactionMode.None)
        {
            Assert.Equal($"routeId123/chats/chatId123", formattedPath);
        }
    }
 
    [Theory]
    [CombinatorialData]
    public void Format_RouteHasLastParameterToBeRedacted_ReturnsCorrectlyRedactedPath(HttpRouteParameterRedactionMode redactionMode)
    {
        HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter();
        Dictionary<string, DataClassification> parametersToRedact = new() { { "chatId", FakeTaxonomy.PrivateData } };
 
        string httpPath = "/api/routes/routeId123/chats/chatId123";
        string httpRoute = "/api/routes/{routeId}/chats/{chatId}";
        string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
 
        if (redactionMode == HttpRouteParameterRedactionMode.Strict)
        {
            Assert.Equal($"api/routes/{TelemetryConstants.Redacted}/chats/Redacted:chatId123", formattedPath);
        }
 
        if (redactionMode == HttpRouteParameterRedactionMode.Loose)
        {
            Assert.Equal($"api/routes/routeId123/chats/Redacted:chatId123", formattedPath);
        }
 
        if (redactionMode == HttpRouteParameterRedactionMode.None)
        {
            Assert.Equal($"api/routes/routeId123/chats/chatId123", formattedPath);
        }
    }
 
    [Theory]
    [CombinatorialData]
    public void Format_RouteHasDefaultParametersToBeRedacted_ReturnsCorrectlyRedactedPath(HttpRouteParameterRedactionMode redactionMode)
    {
        string redactedPrefix = redactionMode == HttpRouteParameterRedactionMode.None ? string.Empty : "Redacted:";
 
        HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter();
        Dictionary<string, DataClassification> parametersToRedact = new() { { "routeId", FakeTaxonomy.PrivateData } };
        string httpRoute = "/api/routes/{routeId=defaultRoute}";
 
        // A default parameter is redacted when it is explicitly specified.
        string httpPath = "/api/routes/routeId123";
        string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal($"api/routes/{redactedPrefix}routeId123", formattedPath);
 
        // An http path is correctly formatted if a default parameter is omitted.
        httpPath = "/api/routes";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal("api/routes", formattedPath);
    }
 
    [Theory]
    [CombinatorialData]
    public void Format_RouteHasWellKnownParameters_ReturnsCorrectlyFormattedPath(HttpRouteParameterRedactionMode redactionMode)
    {
        string redactedPrefix = redactionMode == HttpRouteParameterRedactionMode.None ? string.Empty : "Redacted:";
        HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter();
        Dictionary<string, DataClassification> parametersToRedact = new() { { "filter", FakeTaxonomy.PrivateData } };
        string httpRoute = "{controller=home}/{action=index}/{filter=all}";
 
        // An http path includes well known "controller" and "action" parameters, and a parameter "filter".
        // Well known parameters are not redacted, a parameter with default value is redacted.
        string httpPath = "users/list/top10";
        string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal($"users/list/{redactedPrefix}top10", formattedPath);
 
        // An http path doesn't include an optional parameter with a default value.
        // Well known parameters are not redacted.
        httpPath = "users/list";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal("users/list", formattedPath);
 
        // An http path includes only one optional well known parameter.
        // Well known parameters are not redacted.
        httpPath = "users";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal("users", formattedPath);
 
        // An http path doesn't include all optional parameters.
        httpPath = "";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal("", formattedPath);
 
        // A well known parameter is redacted when it is explicitly specified in an http path,
        // and is not redacted when it is omitted.
        parametersToRedact.Add("controller", FakeTaxonomy.PrivateData);
        parametersToRedact.Add("action", FakeTaxonomy.PrivateData);
 
        httpPath = "users";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal($"{redactedPrefix}users", formattedPath);
    }
 
    [Theory]
    [CombinatorialData]
    public void Format_RouteHasOptionalParametersToBeRedacted_ReturnsCorrectlyRedactedPath(HttpRouteParameterRedactionMode redactionMode)
    {
        string redactedPrefix = redactionMode == HttpRouteParameterRedactionMode.None ? string.Empty : "Redacted:";
        HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter();
        Dictionary<string, DataClassification> parametersToRedact = new() { { "routeId", FakeTaxonomy.PrivateData } };
        string httpRoute = "/api/routes/{routeId?}";
 
        // An optional parameter is redacted when it is explicitly specified.
        string httpPath = "/api/routes/routeId123";
        string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal($"api/routes/{redactedPrefix}routeId123", formattedPath);
 
        // An http path is correctly formatted if an optional parameter is omitted.
        httpPath = "/api/routes";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal("api/routes", formattedPath);
    }
 
    [Theory]
    [CombinatorialData]
    public void Format_RouteHasParametersWithConstraintsToBeRedacted_ReturnsCorrectlyRedactedPath(HttpRouteParameterRedactionMode redactionMode)
    {
        string redactedPrefix = redactionMode == HttpRouteParameterRedactionMode.None ? string.Empty : "Redacted:";
        HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter();
        Dictionary<string, DataClassification> parametersToRedact = new() { { "routeId", FakeTaxonomy.PrivateData } };
 
        string httpRoute = "/api/routes/{routeId:int:min(1)}";
        string httpPath = "/api/routes/routeId123";
 
        string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal($"api/routes/{redactedPrefix}routeId123", formattedPath);
    }
 
    [Theory]
    [CombinatorialData]
    public void Format_HttpPathMayHaveTrailingSlash_FormattedHttpPathDoNotHaveTrailingSlash(HttpRouteParameterRedactionMode redactionMode)
    {
        string redactedPrefix = redactionMode == HttpRouteParameterRedactionMode.None ? string.Empty : "Redacted:";
 
        HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter();
        Dictionary<string, DataClassification> parametersToRedact = new() { { "routeId", FakeTaxonomy.PrivateData } };
        string httpRoute = "/api/routes/static_route";
 
        // An http route is static.
        string httpPath = "/api/routes/static_route";
        string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal($"api/routes/static_route", formattedPath);
 
        httpPath = "/api/routes/static_route/";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal($"api/routes/static_route", formattedPath);
 
        // An http route ends with a slash and has an optional parameter which may be omitted.
        httpRoute = "/api/routes/{routeId?}/";
 
        httpPath = "/api/routes/routeId123";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal($"api/routes/{redactedPrefix}routeId123", formattedPath);
 
        httpPath = "/api/routes/routeId123/";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal($"api/routes/{redactedPrefix}routeId123", formattedPath);
 
        httpPath = "/api/routes";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal("api/routes", formattedPath);
 
        httpPath = "/api/routes/";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal("api/routes", formattedPath);
 
        // All segments of an http route are omitted.
        // The formatted http path is always an empty string.
        httpRoute = "{controller=home}/{action=index}";
 
        httpPath = "/";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal("", formattedPath);
 
        httpPath = "";
        formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact);
        Assert.Equal("", formattedPath);
    }
 
    private static HttpRouteFormatter CreateHttpRouteFormatter()
    {
        var redactorProvider = new FakeRedactorProvider(
            new FakeRedactorOptions { RedactionFormat = "Redacted:{0}" });
        var httpParser = new HttpRouteParser(redactorProvider);
        return new HttpRouteFormatter(httpParser, redactorProvider);
    }
}