< 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: 159
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 System.Security.Claims;
 2using System.Security.Cryptography;
 3using System.Text;
 4using System.Text.Json;
 5using Elsa.Abstractions;
 6using Elsa.Resilience.Options;
 7using Microsoft.AspNetCore.Http;
 8using Microsoft.Extensions.Options;
 9using static Elsa.Resilience.Endpoints.SimulateResponse.StatusCodeMessageLookup;
 10
 11namespace Elsa.Resilience.Endpoints.SimulateResponse;
 12
 3113public class SimulateResponseEndpoint(SimulateResponseSessionStore sessionStore, IOptions<SimulateResponseOptions> optio
 14{
 115    private static readonly int[] DefaultCodes = [429, 503, 200];
 3116    private readonly SimulateResponseOptions _options = options.Value;
 17
 18    public override void Configure()
 19    {
 1420        Get("/simulate-response");
 1421        ConfigurePermissions("exec:*", "exec:resilience", "exec:resilience:simulate-response");
 1422    }
 23
 24    public override async Task HandleAsync(CancellationToken ct)
 25    {
 1726        if (!TryGetSessionId(out var sessionId, out var error) || !TryGetCodes(out var codes, out error))
 27        {
 628            AddError(error);
 629            await Send.ErrorsAsync(StatusCodes.Status400BadRequest, ct);
 630            return;
 31        }
 32
 1133        var scopedSessionId = CreateScopedSessionId(sessionId);
 1134        if (!sessionStore.TryGetNextIndex(scopedSessionId, codes.Length, out var nextIndex))
 35        {
 136            AddError($"The maximum number of active simulate-response sessions ({_options.SessionCapacity}) has been rea
 137            await Send.ErrorsAsync(StatusCodes.Status429TooManyRequests, ct);
 138            return;
 39        }
 40
 1041        var currentCode = codes[nextIndex];
 1042        var message = StatusMessages.TryGetValue(currentCode, out var reason)
 1043            ? reason
 1044            : $"Status Code {currentCode}";
 45
 1046        await Send.ResponseAsync(new(message), currentCode, ct);
 1747    }
 48
 49    private bool TryGetSessionId(out string sessionId, out string error)
 50    {
 1751        sessionId = "default";
 1752        error = "";
 1753        var sessionIdParam = HttpContext.Request.Query["sessionId"].FirstOrDefault();
 54
 1755        if (string.IsNullOrWhiteSpace(sessionIdParam))
 556            return true;
 57
 1258        if (sessionIdParam.Length > _options.MaxSessionIdLength)
 59        {
 160            error = $"The sessionId query parameter must be {_options.MaxSessionIdLength} characters or fewer.";
 161            return false;
 62        }
 63
 1164        sessionId = sessionIdParam;
 1165        return true;
 66    }
 67
 68    private bool TryGetCodes(out int[] codes, out string error)
 69    {
 1670        codes = DefaultCodes;
 1671        error = "";
 1672        var codesParam = HttpContext.Request.Query["codes"].FirstOrDefault();
 73
 1674        if (string.IsNullOrWhiteSpace(codesParam))
 075            return true;
 76
 1677        if (codesParam.Length > _options.MaxCodesQueryLength)
 78        {
 179            error = $"The codes query parameter must be {_options.MaxCodesQueryLength} characters or fewer.";
 180            return false;
 81        }
 82
 83        try
 84        {
 1585            using var document = JsonDocument.Parse(codesParam);
 86
 1487            if (document.RootElement.ValueKind != JsonValueKind.Array)
 88            {
 089                error = "The codes query parameter must be a JSON array of HTTP status codes.";
 090                return false;
 91            }
 92
 1493            var parsedCodes = new List<int>();
 7994            foreach (var codeElement in document.RootElement.EnumerateArray())
 95            {
 2796                if (parsedCodes.Count >= _options.MaxCodes)
 97                {
 198                    error = $"The codes query parameter can contain at most {_options.MaxCodes} status codes.";
 199                    return false;
 100                }
 101
 26102                if (codeElement.ValueKind != JsonValueKind.Number || !codeElement.TryGetInt32(out var code) || code is <
 103                {
 2104                    error = "The codes query parameter must contain HTTP status codes between 100 and 599.";
 2105                    return false;
 106                }
 107
 24108                parsedCodes.Add(code);
 109            }
 110
 11111            if (parsedCodes.Count == 0)
 112            {
 0113                error = "The codes query parameter must contain at least one status code.";
 0114                return false;
 115            }
 116
 11117            codes = parsedCodes.ToArray();
 11118            return true;
 119        }
 1120        catch (JsonException)
 121        {
 1122            error = "The codes query parameter must be valid JSON.";
 1123            return false;
 124        }
 15125    }
 126
 127    private string CreateScopedSessionId(string sessionId)
 128    {
 11129        var key = $"{GetAuthenticatedIdentityKey()}\0{sessionId}";
 11130        return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(key)));
 131    }
 132
 133    private string GetAuthenticatedIdentityKey()
 134    {
 11135        var user = HttpContext.User;
 11136        var identityClaim = user.FindFirst(ClaimTypes.NameIdentifier)
 11137            ?? user.FindFirst("sub")
 11138            ?? user.FindFirst("client_id")
 11139            ?? user.FindFirst("name")
 11140            ?? user.FindFirst(ClaimTypes.Name);
 141
 11142        if (identityClaim != null)
 11143            return $"{identityClaim.Type}:{identityClaim.Value}";
 144
 0145        if (!string.IsNullOrWhiteSpace(user.Identity?.Name))
 0146            return $"name:{user.Identity.Name}";
 147
 0148        var claimsKey = string.Join('\u001e', user.Claims
 0149            .Where(x => x.Type != "permissions")
 0150            .OrderBy(x => x.Type, StringComparer.Ordinal)
 0151            .ThenBy(x => x.Issuer, StringComparer.Ordinal)
 0152            .ThenBy(x => x.Value, StringComparer.Ordinal)
 0153            .Select(x => $"{x.Type}:{x.Issuer}:{x.Value}"));
 154
 0155        return !string.IsNullOrEmpty(claimsKey)
 0156            ? claimsKey
 0157            : $"auth:{user.Identity?.AuthenticationType ?? "unknown"}";
 158    }
 159}