< Summary

Information
Class: Elsa.ExternalAuthentication.Services.ExtensionDescriptorValidator
Assembly: Elsa.ExternalAuthentication
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Services/ExtensionDescriptorValidator.cs
Line coverage
78%
Covered lines: 57
Uncovered lines: 16
Coverable lines: 73
Total lines: 130
Line coverage: 78%
Branch coverage
75%
Covered branches: 53
Total branches: 70
Branch coverage: 75.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
Validate(...)100%11100%
Validate(...)100%11100%
Validate(...)100%11100%
Validate(...)100%210%
ValidateDescriptor(...)95%202094.73%
ValidateFields(...)63.04%1394664.7%
IsIdentifier(...)100%22100%
IsSettingFieldName(...)50%22100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Services/ExtensionDescriptorValidator.cs

#LineLine coverage
 1using System.Text.RegularExpressions;
 2using Elsa.ExternalAuthentication.Contracts;
 3using Elsa.ExternalAuthentication.Models;
 4
 5namespace Elsa.ExternalAuthentication.Services;
 6
 7/// <summary>
 8/// Validates extension descriptors before they are exposed to configuration or Studio.
 9/// </summary>
 10public sealed class ExtensionDescriptorValidator
 11{
 212    private static readonly Regex IdentifierPattern = new("^[a-z][a-z0-9]*(?:[-.][a-z0-9]+)*$", RegexOptions.CultureInva
 213    private static readonly Regex SettingFieldNamePattern = new("^[a-z][A-Za-z0-9]*$", RegexOptions.CultureInvariant);
 214    private static readonly HashSet<string> SupportedValueTypes = new(StringComparer.Ordinal)
 215    {
 216        "string", "secret", "boolean", "integer", "number", "uri", "string-array", "json"
 217    };
 18
 19    public ExternalAuthenticationAdapterDescriptor Validate(IExternalAuthenticationAdapter extension)
 20    {
 521        var descriptor = extension.Describe();
 522        ValidateDescriptor(extension.Type, descriptor.Type, descriptor.DisplayName, descriptor.Description, descriptor.S
 323        return descriptor;
 24    }
 25
 26    public UnlinkedIdentityPolicyDescriptor Validate(IUnlinkedIdentityPolicy extension)
 27    {
 328        var descriptor = extension.Describe();
 329        ValidateDescriptor(extension.Type, descriptor.Type, descriptor.DisplayName, descriptor.Description, descriptor.S
 330        return descriptor;
 31    }
 32
 33    public PermissionGrantSourceDescriptor Validate(IPermissionGrantSource extension)
 34    {
 235        var descriptor = extension.Describe();
 236        ValidateDescriptor(extension.Type, descriptor.Type, descriptor.DisplayName, descriptor.Description, descriptor.S
 237        return descriptor;
 38    }
 39
 40    public ExternalUserMatcherDescriptor Validate(IExternalUserMatcher extension)
 41    {
 042        var descriptor = extension.Describe();
 043        ValidateDescriptor(extension.Type, descriptor.Type, descriptor.DisplayName, descriptor.Description, descriptor.S
 044        return descriptor;
 45    }
 46
 47    private static void ValidateDescriptor(
 48        string extensionType,
 49        string descriptorType,
 50        string displayName,
 51        string description,
 52        int settingsVersion,
 53        IReadOnlyList<SettingFieldDescriptor> fields,
 54        CustomEditorContract? customEditor)
 55    {
 1056        var failures = new List<string>();
 1057        if (!IsIdentifier(extensionType))
 158            failures.Add($"Extension type '{extensionType}' is not a stable identifier.");
 1059        if (!string.Equals(extensionType, descriptorType, StringComparison.Ordinal))
 160            failures.Add($"Descriptor type '{descriptorType}' does not match extension type '{extensionType}'.");
 1061        if (string.IsNullOrWhiteSpace(displayName))
 162            failures.Add($"Extension '{extensionType}' must define a display name.");
 1063        if (string.IsNullOrWhiteSpace(description))
 164            failures.Add($"Extension '{extensionType}' must define a description.");
 1065        if (settingsVersion <= 0)
 166            failures.Add($"Extension '{extensionType}' must define a positive settings version.");
 1067        if (fields is null)
 068            failures.Add($"Extension '{extensionType}' must define a fields collection.");
 69        else
 1070            ValidateFields(extensionType, fields, failures);
 1071        if (customEditor is not null && (!IsIdentifier(customEditor.Key) || customEditor.ContractVersion <= 0))
 172            failures.Add($"Extension '{extensionType}' has an invalid custom-editor contract.");
 73
 1074        if (failures.Count > 0)
 275            throw new InvalidOperationException(string.Join(" ", failures));
 876    }
 77
 78    private static void ValidateFields(string extensionType, IReadOnlyCollection<SettingFieldDescriptor> fields, ICollec
 79    {
 1080        var names = new HashSet<string>(StringComparer.Ordinal);
 3281        foreach (var field in fields)
 82        {
 683            if (!IsSettingFieldName(field.Name))
 084                failures.Add($"Extension '{extensionType}' has invalid field name '{field.Name}'.");
 685            else if (!names.Add(field.Name))
 086                failures.Add($"Extension '{extensionType}' defines field '{field.Name}' more than once.");
 687            if (string.IsNullOrWhiteSpace(field.DisplayName) || string.IsNullOrWhiteSpace(field.Description))
 088                failures.Add($"Field '{field.Name}' must define display text.");
 689            if (!SupportedValueTypes.Contains(field.ValueType))
 090                failures.Add($"Field '{field.Name}' has unsupported value type '{field.ValueType}'.");
 691            if (string.IsNullOrWhiteSpace(field.UiHint))
 092                failures.Add($"Field '{field.Name}' must define a UI hint.");
 693            if (field.Validation.MinimumLength is < 0 ||
 694                field.Validation.MaximumLength is < 0 ||
 695                field.Validation.MinimumLength > field.Validation.MaximumLength)
 096                failures.Add($"Field '{field.Name}' has invalid length validation.");
 697            if (field.Validation.Pattern is { Length: > 0 } pattern)
 98            {
 99                try
 100                {
 0101                    _ = new Regex(pattern, RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1));
 0102                }
 0103                catch (ArgumentException)
 104                {
 0105                    failures.Add($"Field '{field.Name}' has an invalid validation pattern.");
 0106                }
 107            }
 6108            if (field.AllowedValues.Any(string.IsNullOrWhiteSpace) ||
 6109                field.AllowedValues.Distinct(StringComparer.Ordinal).Count() != field.AllowedValues.Count)
 0110                failures.Add($"Field '{field.Name}' has invalid or duplicate allowed values.");
 6111            if (field.IsSecretBinding && (!string.Equals(field.ValueType, "secret", StringComparison.Ordinal) || !field.
 1112                failures.Add($"Secret-binding field '{field.Name}' must use the secret value type and be redacted.");
 113        }
 114
 28115        foreach (var field in fields.Where(x => x.VisibleWhen is not null))
 116        {
 1117            var condition = field.VisibleWhen!;
 1118            if (!names.Contains(condition.Field) ||
 1119                string.Equals(condition.Field, field.Name, StringComparison.Ordinal) ||
 1120                string.IsNullOrWhiteSpace(condition.ExpectedValue))
 1121                failures.Add($"Field '{field.Name}' has an invalid visibility condition.");
 122        }
 10123    }
 124
 125    private static bool IsIdentifier(string? value) =>
 15126        !string.IsNullOrWhiteSpace(value) && IdentifierPattern.IsMatch(value);
 127
 128    private static bool IsSettingFieldName(string? value) =>
 6129        !string.IsNullOrWhiteSpace(value) && SettingFieldNamePattern.IsMatch(value);
 130}