< Summary

Information
Class: AsiBackbone.AspNetCore.Endpoints.AsiBackboneEndpointGovernanceDevelopmentDiagnostics
Assembly: AsiBackbone.AspNetCore
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.AspNetCore/Endpoints/AsiBackboneEndpointGovernanceDevelopmentDiagnostics.cs
Line coverage
96%
Covered lines: 81
Uncovered lines: 3
Coverable lines: 84
Total lines: 177
Line coverage: 96.4%
Branch coverage
86%
Covered branches: 33
Total branches: 38
Branch coverage: 86.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
IsEnabled(...)75%44100%
CreateProblem(...)100%22100%
CreateExtensions(...)50%4493.33%
RedactMetadata(...)75%44100%
ShouldRedactMetadataValue(...)94.44%181892.3%
CreateDocumentationUrl(...)100%44100%
AddIfPresent(...)100%22100%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.AspNetCore/Endpoints/AsiBackboneEndpointGovernanceDevelopmentDiagnostics.cs

#LineLine coverage
 1using AsiBackbone.Core.Decisions;
 2using Microsoft.AspNetCore.Hosting;
 3using Microsoft.AspNetCore.Http;
 4using Microsoft.Extensions.DependencyInjection;
 5using Microsoft.Extensions.Hosting;
 6
 7namespace AsiBackbone.AspNetCore.Endpoints;
 8
 9internal static class AsiBackboneEndpointGovernanceDevelopmentDiagnostics
 10{
 11    private const string RedactedValue = "[redacted]";
 12    private const string DocumentationArticleName = "endpoint-governance-development-diagnostics.html";
 13
 14    public static bool IsEnabled(
 15        HttpContext httpContext,
 16        AsiBackboneEndpointGovernanceOptions options)
 17    {
 1218        ArgumentNullException.ThrowIfNull(httpContext);
 1219        ArgumentNullException.ThrowIfNull(options);
 20
 1221        if (!options.EnableDevelopmentDiagnostics)
 22        {
 823            return false;
 24        }
 25
 426        IWebHostEnvironment? environment = httpContext.RequestServices.GetService<IWebHostEnvironment>();
 27
 428        return environment?.IsDevelopment() == true;
 29    }
 30
 31    public static IResult CreateProblem(
 32        HttpContext httpContext,
 33        AsiBackboneEndpointGovernanceOptions options,
 34        AsiBackboneEndpointGovernanceDescriptor descriptor,
 35        GovernanceDecision? decision,
 36        string decisionStage,
 37        string title,
 38        string detail,
 39        int statusCode,
 40        IReadOnlyDictionary<string, string>? metadata = null)
 41    {
 342        ArgumentNullException.ThrowIfNull(httpContext);
 343        ArgumentNullException.ThrowIfNull(options);
 344        ArgumentNullException.ThrowIfNull(descriptor);
 345        ArgumentException.ThrowIfNullOrWhiteSpace(decisionStage);
 346        ArgumentException.ThrowIfNullOrWhiteSpace(title);
 347        ArgumentException.ThrowIfNullOrWhiteSpace(detail);
 48
 349        Dictionary<string, string> diagnosticMetadata = metadata is null
 350            ? new Dictionary<string, string>(descriptor.ToMetadata(options.MetadataMode), StringComparer.Ordinal)
 351            : new Dictionary<string, string>(metadata, StringComparer.Ordinal);
 352        Dictionary<string, object?> extensions = CreateExtensions(
 353            options,
 354            descriptor,
 355            decision,
 356            decisionStage,
 357            diagnosticMetadata);
 58
 359        return Microsoft.AspNetCore.Http.Results.Problem(
 360            title: title,
 361            detail: detail,
 362            statusCode: statusCode,
 363            extensions: extensions);
 64    }
 65
 66    private static Dictionary<string, object?> CreateExtensions(
 67        AsiBackboneEndpointGovernanceOptions options,
 68        AsiBackboneEndpointGovernanceDescriptor descriptor,
 69        GovernanceDecision? decision,
 70        string decisionStage,
 71        IReadOnlyDictionary<string, string> metadata)
 72    {
 373        var extensions = new Dictionary<string, object?>(StringComparer.Ordinal)
 374        {
 375            ["decisionStage"] = decisionStage,
 376            ["endpointOperationName"] = descriptor.OperationName,
 377            ["endpointPolicyTypes"] = descriptor.PolicyTypes
 178                .Select(static policyType => policyType.FullName ?? policyType.Name)
 079                .OrderBy(static policyType => policyType, StringComparer.Ordinal)
 380                .ToArray(),
 381            ["capabilityScopes"] = descriptor.CapabilityScopes
 082                .OrderBy(static scope => scope, StringComparer.Ordinal)
 383                .ToArray(),
 384            ["metadataMode"] = options.MetadataMode.ToString(),
 385            ["metadataKeys"] = metadata.Keys
 986                .OrderBy(static key => key, StringComparer.Ordinal)
 387                .ToArray(),
 388            ["metadata"] = RedactMetadata(options, metadata)
 389        };
 90
 391        if (decision is not null)
 92        {
 393            extensions["outcome"] = decision.Outcome.ToString();
 394            extensions["reasonCodes"] = decision.ReasonCodes.ToArray();
 395            extensions["reasonMessages"] = decision.Reasons
 396                .Select(static reason => reason.Message)
 397                .ToArray();
 98
 399            AddIfPresent(extensions, "correlationId", decision.CorrelationId);
 3100            AddIfPresent(extensions, "traceId", decision.TraceId);
 3101            AddIfPresent(extensions, "policyVersion", decision.PolicyVersion);
 3102            AddIfPresent(extensions, "policyHash", decision.PolicyHash);
 103        }
 104
 3105        string? documentationUrl = CreateDocumentationUrl(options.DevelopmentDiagnosticsDocumentationBaseUrl);
 3106        AddIfPresent(extensions, "documentationUrl", documentationUrl);
 107
 3108        return extensions;
 109    }
 110
 111    private static Dictionary<string, string> RedactMetadata(
 112        AsiBackboneEndpointGovernanceOptions options,
 113        IReadOnlyDictionary<string, string> metadata)
 114    {
 3115        Dictionary<string, string> redacted = new(StringComparer.Ordinal);
 116
 36117        foreach (KeyValuePair<string, string> item in metadata.OrderBy(static pair => pair.Key, StringComparer.Ordinal))
 118        {
 10119            redacted[item.Key] = ShouldRedactMetadataValue(options, item.Key)
 10120                ? RedactedValue
 10121                : item.Value;
 122        }
 123
 3124        return redacted;
 125    }
 126
 127    private static bool ShouldRedactMetadataValue(
 128        AsiBackboneEndpointGovernanceOptions options,
 129        string key)
 130    {
 18131        if (!options.IncludeDevelopmentDiagnosticsMetadataValues)
 132        {
 0133            return true;
 134        }
 135
 37136        foreach (string sensitiveKey in options.DevelopmentDiagnosticsRedactedMetadataKeys)
 137        {
 1138            if (string.Equals(key, sensitiveKey, StringComparison.OrdinalIgnoreCase))
 139            {
 1140                return true;
 141            }
 142        }
 143
 17144        return key.Contains("secret", StringComparison.OrdinalIgnoreCase)
 17145            || key.Contains("token", StringComparison.OrdinalIgnoreCase)
 17146            || key.Contains("password", StringComparison.OrdinalIgnoreCase)
 17147            || key.Contains("credential", StringComparison.OrdinalIgnoreCase)
 17148            || key.Contains("cookie", StringComparison.OrdinalIgnoreCase)
 17149            || key.Contains("authorization", StringComparison.OrdinalIgnoreCase)
 17150            || key.Contains("key", StringComparison.OrdinalIgnoreCase);
 1151    }
 152
 153    private static string? CreateDocumentationUrl(string? baseUrl)
 154    {
 3155        if (string.IsNullOrWhiteSpace(baseUrl))
 156        {
 1157            return null;
 158        }
 159
 2160        string trimmedBaseUrl = baseUrl.Trim();
 161
 2162        return trimmedBaseUrl.EndsWith('/')
 2163            ? trimmedBaseUrl + DocumentationArticleName
 2164            : trimmedBaseUrl + "/" + DocumentationArticleName;
 165    }
 166
 167    private static void AddIfPresent(
 168        Dictionary<string, object?> extensions,
 169        string key,
 170        string? value)
 171    {
 15172        if (!string.IsNullOrWhiteSpace(value))
 173        {
 10174            extensions[key] = value;
 175        }
 15176    }
 177}