< Summary

Information
Class: Elsa.Resilience.Endpoints.SimulateResponse.SimulateResponseEndpoint
Assembly: Elsa.Resilience
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Resilience/Endpoints/SimulateResponse/Endpoint.cs
Line coverage
80%
Covered lines: 66
Uncovered lines: 16
Coverable lines: 82
Total lines: 160
Line coverage: 80.4%
Branch coverage
62%
Covered branches: 34
Total branches: 54
Branch coverage: 62.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.cctor()100%11100%
Configure()100%11100%
HandleAsync()87.5%88100%
TryGetSessionId(...)100%44100%
TryGetCodes(...)81.81%242283.33%
CreateScopedSessionId(...)100%11100%
GetAuthenticatedIdentityKey()25%982042.1%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Resilience/Endpoints/SimulateResponse/Endpoint.cs

#LineLine coverage
 1using Elsa.Authorization;
 2using System.Security.Claims;
 3using System.Security.Cryptography;
 4using System.Text;
 5using System.Text.Json;
 6using Elsa.Abstractions;
 7using Elsa.Resilience.Options;
 8using Microsoft.AspNetCore.Http;
 9using Microsoft.Extensions.Options;
 10using static Elsa.Resilience.Endpoints.SimulateResponse.StatusCodeMessageLookup;
 11
 12namespace Elsa.Resilience.Endpoints.SimulateResponse;
 13
 3114public class SimulateResponseEndpoint(SimulateResponseSessionStore sessionStore, IOptions<SimulateResponseOptions> optio
 15{
 116    private static readonly int[] DefaultCodes = [429, 503, 200];
 3117    private readonly SimulateResponseOptions _options = options.Value;
 18
 19    public override void Configure()
 20    {
 1421        Get("/simulate-response");
 1422        RequirePermission(Elsa.Resilience.Permissions.ResiliencePermissions.Simulation, CoreVerbs.Execute);
 1423    }
 24
 25    public override async Task HandleAsync(CancellationToken ct)
 26    {
 1727        if (!TryGetSessionId(out var sessionId, out var error) || !TryGetCodes(out var codes, out error))
 28        {
 629            AddError(error);
 630            await Send.ErrorsAsync(StatusCodes.Status400BadRequest, ct);
 631            return;
 32        }
 33
 1134        var scopedSessionId = CreateScopedSessionId(sessionId);
 1135        if (!sessionStore.TryGetNextIndex(scopedSessionId, codes.Length, out var nextIndex))
 36        {
 137            AddError($"The maximum number of active simulate-response sessions ({_options.SessionCapacity}) has been rea
 138            await Send.ErrorsAsync(StatusCodes.Status429TooManyRequests, ct);
 139            return;
 40        }
 41
 1042        var currentCode = codes[nextIndex];
 1043        var message = StatusMessages.TryGetValue(currentCode, out var reason)
 1044            ? reason
 1045            : $"Status Code {currentCode}";
 46
 1047        await Send.ResponseAsync(new(message), currentCode, ct);
 1748    }
 49
 50    private bool TryGetSessionId(out string sessionId, out string error)
 51    {
 1752        sessionId = "default";
 1753        error = "";
 1754        var sessionIdParam = HttpContext.Request.Query["sessionId"].FirstOrDefault();
 55
 1756        if (string.IsNullOrWhiteSpace(sessionIdParam))
 557            return true;
 58
 1259        if (sessionIdParam.Length > _options.MaxSessionIdLength)
 60        {
 161            error = $"The sessionId query parameter must be {_options.MaxSessionIdLength} characters or fewer.";
 162            return false;
 63        }
 64
 1165        sessionId = sessionIdParam;
 1166        return true;
 67    }
 68
 69    private bool TryGetCodes(out int[] codes, out string error)
 70    {
 1671        codes = DefaultCodes;
 1672        error = "";
 1673        var codesParam = HttpContext.Request.Query["codes"].FirstOrDefault();
 74
 1675        if (string.IsNullOrWhiteSpace(codesParam))
 076            return true;
 77
 1678        if (codesParam.Length > _options.MaxCodesQueryLength)
 79        {
 180            error = $"The codes query parameter must be {_options.MaxCodesQueryLength} characters or fewer.";
 181            return false;
 82        }
 83
 84        try
 85        {
 1586            using var document = JsonDocument.Parse(codesParam);
 87
 1488            if (document.RootElement.ValueKind != JsonValueKind.Array)
 89            {
 090                error = "The codes query parameter must be a JSON array of HTTP status codes.";
 091                return false;
 92            }
 93
 1494            var parsedCodes = new List<int>();
 7995            foreach (var codeElement in document.RootElement.EnumerateArray())
 96            {
 2797                if (parsedCodes.Count >= _options.MaxCodes)
 98                {
 199                    error = $"The codes query parameter can contain at most {_options.MaxCodes} status codes.";
 1100                    return false;
 101                }
 102
 26103                if (codeElement.ValueKind != JsonValueKind.Number || !codeElement.TryGetInt32(out var code) || code is <
 104                {
 2105                    error = "The codes query parameter must contain HTTP status codes between 100 and 599.";
 2106                    return false;
 107                }
 108
 24109                parsedCodes.Add(code);
 110            }
 111
 11112            if (parsedCodes.Count == 0)
 113            {
 0114                error = "The codes query parameter must contain at least one status code.";
 0115                return false;
 116            }
 117
 11118            codes = parsedCodes.ToArray();
 11119            return true;
 120        }
 1121        catch (JsonException)
 122        {
 1123            error = "The codes query parameter must be valid JSON.";
 1124            return false;
 125        }
 15126    }
 127
 128    private string CreateScopedSessionId(string sessionId)
 129    {
 11130        var key = $"{GetAuthenticatedIdentityKey()}\0{sessionId}";
 11131        return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(key)));
 132    }
 133
 134    private string GetAuthenticatedIdentityKey()
 135    {
 11136        var user = HttpContext.User;
 11137        var identityClaim = user.FindFirst(ClaimTypes.NameIdentifier)
 11138            ?? user.FindFirst("sub")
 11139            ?? user.FindFirst("client_id")
 11140            ?? user.FindFirst("name")
 11141            ?? user.FindFirst(ClaimTypes.Name);
 142
 11143        if (identityClaim != null)
 11144            return $"{identityClaim.Type}:{identityClaim.Value}";
 145
 0146        if (!string.IsNullOrWhiteSpace(user.Identity?.Name))
 0147            return $"name:{user.Identity.Name}";
 148
 0149        var claimsKey = string.Join('\u001e', user.Claims
 0150            .Where(x => x.Type != "permissions")
 0151            .OrderBy(x => x.Type, StringComparer.Ordinal)
 0152            .ThenBy(x => x.Issuer, StringComparer.Ordinal)
 0153            .ThenBy(x => x.Value, StringComparer.Ordinal)
 0154            .Select(x => $"{x.Type}:{x.Issuer}:{x.Value}"));
 155
 0156        return !string.IsNullOrEmpty(claimsKey)
 0157            ? claimsKey
 0158            : $"auth:{user.Identity?.AuthenticationType ?? "unknown"}";
 159    }
 160}