< Summary

Information
Class: Elsa.Testing.Shared.Authorization.EndpointCoverage
Assembly: Elsa.Testing.Shared
File(s): /home/runner/work/elsa-core/elsa-core/src/common/Elsa.Testing.Shared/Authorization/EndpointCoverage.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 51
Coverable lines: 51
Total lines: 130
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 40
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
AssertEveryEndpointDeclaresAccess(...)100%210%
FindEndpoints(...)100%210%
IsEndpoint(...)0%156120%
DeclaresAccess(...)0%4260%
DeclaresAccessOnAnyBase(...)0%4260%
ReferencesDeclaration(...)0%272160%

File(s)

/home/runner/work/elsa-core/elsa-core/src/common/Elsa.Testing.Shared/Authorization/EndpointCoverage.cs

#LineLine coverage
 1using System.Reflection;
 2using Xunit;
 3
 4namespace Elsa.Testing.Shared.Authorization;
 5
 6/// <summary>
 7/// The fail-closed endpoint gate, as a reusable assertion. Omitting a declaration inherits the
 8/// FastEndpoints default with no Elsa-level fallback, so an endpoint can reach production ungated with
 9/// nobody noticing. This asserts every endpoint in an assembly states its access explicitly.
 10/// </summary>
 11/// <remarks>
 12/// Reflection over <c>Configure()</c> is deliberate. Booting a host would prove more, but would require
 13/// every endpoint-bearing module as a dependency of one test project, which is what stopped such a gate
 14/// existing. A module opts in with a single test calling <see cref="AssertEveryEndpointDeclaresAccess"/>.
 15/// </remarks>
 16public static class EndpointCoverage
 17{
 18    // The declaration helpers are protected, so they are matched by name.
 019    private static readonly string[] Declarations =
 020    [
 021        "RequirePermission",
 022        "RequireAuthenticatedOnly",
 023        "ConfigurePermissions",
 024        "AllowAnonymous"
 025    ];
 26
 27    /// <summary>Asserts every Elsa endpoint in <paramref name="assembly"/> declares its access.</summary>
 28    public static void AssertEveryEndpointDeclaresAccess(Assembly assembly)
 29    {
 030        var endpoints = FindEndpoints(assembly).ToArray();
 31
 32        // A reflection gate that silently matches nothing passes forever.
 033        Assert.True(endpoints.Length > 0, $"No Elsa endpoints found in {assembly.GetName().Name}. The gate is not lookin
 34
 035        var undeclared = endpoints.Where(x => !DeclaresAccess(x)).Select(x => x.FullName).OrderBy(x => x, StringComparer
 36
 037        Assert.True(
 038            undeclared.Length == 0,
 039            $"{undeclared.Length} endpoint(s) declare no access: {string.Join(", ", undeclared)}. "
 040            + "Every endpoint must call exactly one of RequirePermission, RequireAuthenticatedOnly, or AllowAnonymous. "
 041            + "There is no exemption list: an endpoint that states nothing is indistinguishable from one whose author fo
 042    }
 43
 44    /// <summary>The Elsa endpoint types declared in <paramref name="assembly"/>.</summary>
 45    public static IEnumerable<Type> FindEndpoints(Assembly assembly)
 46    {
 47        Type[] types;
 48
 49        try
 50        {
 051            types = assembly.GetTypes();
 052        }
 53        catch (ReflectionTypeLoadException ex)
 54        {
 055            types = ex.Types.Where(x => x is not null).ToArray()!;
 056        }
 57
 058        return types.Where(IsEndpoint);
 59    }
 60
 61    private static bool IsEndpoint(Type type)
 62    {
 063        if (type is not { IsClass: true, IsAbstract: false })
 064            return false;
 65
 066        for (var baseType = type.BaseType; baseType is not null; baseType = baseType.BaseType)
 67        {
 068            if (baseType.FullName?.StartsWith("Elsa.Abstractions.ElsaEndpoint", StringComparison.Ordinal) == true)
 069                return true;
 70        }
 71
 072        return false;
 73    }
 74
 75    private static bool DeclaresAccess(Type endpointType)
 76    {
 077        var configure = endpointType.GetMethod("Configure", BindingFlags.Public | BindingFlags.Instance | BindingFlags.D
 78
 79        // An endpoint inheriting Configure from an abstract base declares through that base.
 080        if (configure is null)
 081            return endpointType.BaseType is not null && endpointType.BaseType.IsAbstract && DeclaresAccessOnAnyBase(endp
 82
 083        return ReferencesDeclaration(configure);
 84    }
 85
 86    private static bool DeclaresAccessOnAnyBase(Type baseType)
 87    {
 088        for (var type = baseType; type is not null; type = type.BaseType)
 89        {
 090            var configure = type.GetMethod("Configure", BindingFlags.Public | BindingFlags.Instance | BindingFlags.Decla
 91
 092            if (configure is not null && ReferencesDeclaration(configure))
 093                return true;
 94        }
 95
 096        return false;
 97    }
 98
 99    private static bool ReferencesDeclaration(MethodInfo configure)
 100    {
 0101        var body = configure.GetMethodBody();
 102
 0103        if (body is null)
 0104            return false;
 105
 0106        var module = configure.Module;
 0107        var il = body.GetILAsByteArray() ?? [];
 108
 0109        for (var i = 0; i < il.Length - 4; i++)
 110        {
 111            // 0x28 call, 0x6F callvirt.
 0112            if (il[i] is not (0x28 or 0x6F))
 113                continue;
 114
 115            try
 116            {
 0117                var called = module.ResolveMethod(BitConverter.ToInt32(il, i + 1));
 118
 0119                if (called is not null && Declarations.Contains(called.Name, StringComparer.Ordinal))
 0120                    return true;
 0121            }
 0122            catch (Exception ex) when (ex is ArgumentException or BadImageFormatException or MissingMethodException)
 123            {
 124                // The bytes at this offset are not a resolvable method token; keep scanning.
 0125            }
 126        }
 127
 0128        return false;
 0129    }
 130}