< Summary

Information
Class: AsiBackbone.Testing.GovernanceTestHarnessOptions
Assembly: AsiBackbone.Testing
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Testing/AsiBackboneTestHarness.cs
Line coverage
96%
Covered lines: 28
Uncovered lines: 1
Coverable lines: 29
Total lines: 479
Line coverage: 96.5%
Branch coverage
50%
Covered branches: 1
Total branches: 2
Branch coverage: 50%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
get_PolicyResults()100%210%
AllowAllPolicies()100%11100%
DenyAllPolicies(...)100%11100%
AllowCapabilityGrants()100%11100%
DenyCapabilityGrants(...)100%11100%
SetPolicyResult(...)100%11100%
SetPolicyResult(...)50%22100%
RequirePolicyResult(...)100%11100%
RequirePolicyResult(...)100%11100%
TryGetPolicyDecision(...)100%11100%
Validate()100%11100%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Testing/AsiBackboneTestHarness.cs

#LineLine coverage
 1using AsiBackbone.AspNetCore.Endpoints;
 2using AsiBackbone.Core.Audit;
 3using AsiBackbone.Core.Constraints;
 4using AsiBackbone.Core.Decisions;
 5using AsiBackbone.Core.Evaluation;
 6using AsiBackbone.Core.Outbox;
 7using AsiBackbone.Core.Signing;
 8using AsiBackbone.Storage.InMemory.Outbox;
 9using Microsoft.AspNetCore.Http;
 10using Microsoft.Extensions.DependencyInjection;
 11using Microsoft.Extensions.DependencyInjection.Extensions;
 12
 13namespace AsiBackbone.Testing;
 14
 15/// <summary>
 16/// Marker type for the AsiBackbone.Testing assembly.
 17/// </summary>
 18public sealed class AsiBackboneTestingAssemblyMarker
 19{
 20    private AsiBackboneTestingAssemblyMarker()
 21    {
 22    }
 23}
 24
 25/// <summary>
 26/// Configures deterministic services for exercising AsiBackbone-governed endpoints in tests.
 27/// </summary>
 28public sealed class GovernanceTestHarnessOptions
 29{
 830    private readonly Dictionary<string, GovernanceDecision> policyResults = new(StringComparer.Ordinal);
 31
 32    /// <summary>
 33    /// Gets the default policy decision used when no explicit policy result is configured.
 34    /// </summary>
 35    public GovernanceDecision DefaultPolicyDecision { get; private set; } = GovernanceDecision.Allow(policyVersion: "tes
 36
 37    /// <summary>
 38    /// Gets the default capability-grant validation decision used by the test harness.
 39    /// </summary>
 40    public GovernanceDecision DefaultCapabilityGrantDecision { get; private set; } = GovernanceDecision.Allow(policyVers
 41
 42    /// <summary>
 43    /// Gets a value indicating whether every policy marker must have an explicit configured result.
 44    /// </summary>
 45    public bool RequireExplicitPolicyResults { get; private set; }
 46
 47    /// <summary>
 48    /// Gets or sets a value indicating whether the harness registers <see cref="GovernanceTestDecisionReceiptSink" /> a
 49    /// </summary>
 50    public bool RegisterInMemoryAuditSink { get; set; } = true;
 51
 52    /// <summary>
 53    /// Gets or sets a value indicating whether the harness registers the non-durable in-memory outbox store.
 54    /// </summary>
 55    public bool RegisterInMemoryOutboxStore { get; set; } = true;
 56
 57    /// <summary>
 58    /// Gets or sets a value indicating whether the harness registers a deterministic no-signature signing service.
 59    /// </summary>
 60    public bool RegisterDeterministicSigningService { get; set; } = true;
 61
 62    /// <summary>
 63    /// Gets the configured deterministic policy results.
 64    /// </summary>
 065    public IReadOnlyDictionary<string, GovernanceDecision> PolicyResults => policyResults;
 66
 67    /// <summary>
 68    /// Uses an allow decision as the default policy result.
 69    /// </summary>
 70    public GovernanceTestHarnessOptions AllowAllPolicies()
 71    {
 372        DefaultPolicyDecision = GovernanceDecision.Allow(policyVersion: "test-harness");
 373        RequireExplicitPolicyResults = false;
 374        return this;
 75    }
 76
 77    /// <summary>
 78    /// Uses a deny decision as the default policy result.
 79    /// </summary>
 80    public GovernanceTestHarnessOptions DenyAllPolicies(
 81        string code = "test_harness.policy_denied",
 82        string message = "Denied by the AsiBackbone test harness.")
 83    {
 284        ArgumentException.ThrowIfNullOrWhiteSpace(code);
 285        ArgumentException.ThrowIfNullOrWhiteSpace(message);
 86
 287        DefaultPolicyDecision = GovernanceDecision.Deny(code, message, policyVersion: "test-harness");
 288        RequireExplicitPolicyResults = false;
 289        return this;
 90    }
 91
 92    /// <summary>
 93    /// Uses an allow decision as the default capability-grant validation result.
 94    /// </summary>
 95    public GovernanceTestHarnessOptions AllowCapabilityGrants()
 96    {
 197        DefaultCapabilityGrantDecision = GovernanceDecision.Allow(policyVersion: "test-harness");
 198        return this;
 99    }
 100
 101    /// <summary>
 102    /// Uses a deny decision as the default capability-grant validation result.
 103    /// </summary>
 104    public GovernanceTestHarnessOptions DenyCapabilityGrants(
 105        string code = "test_harness.capability_denied",
 106        string message = "Capability grant denied by the AsiBackbone test harness.")
 107    {
 2108        ArgumentException.ThrowIfNullOrWhiteSpace(code);
 2109        ArgumentException.ThrowIfNullOrWhiteSpace(message);
 110
 2111        DefaultCapabilityGrantDecision = GovernanceDecision.Deny(code, message, policyVersion: "test-harness");
 2112        return this;
 113    }
 114
 115    /// <summary>
 116    /// Sets a deterministic result for a policy marker type.
 117    /// </summary>
 118    public GovernanceTestHarnessOptions SetPolicyResult<TPolicy>(GovernanceDecision decision)
 119    {
 1120        return SetPolicyResult(typeof(TPolicy), decision);
 121    }
 122
 123    /// <summary>
 124    /// Sets a deterministic result for a policy marker type.
 125    /// </summary>
 126    /// <remarks>
 127    /// This is a harness-only capability. It simulates a host-supplied decision policy that reads the
 128    /// <c>endpoint.policy_types</c> metadata entry and varies its outcome by policy type. AsiBackbone itself does not
 129    /// select constraints or decisions from an endpoint's policy type, so configuring different results for two policy
 130    /// types asserts against host policy the application must actually implement. Without that host decision policy,
 131    /// both endpoints evaluate identically at runtime.
 132    /// </remarks>
 133    /// <param name="policyType">The policy marker type to configure a result for.</param>
 134    /// <param name="decision">The decision the harness returns for endpoints marked with that type.</param>
 135    /// <returns>The same options instance so calls can be chained.</returns>
 136    public GovernanceTestHarnessOptions SetPolicyResult(Type policyType, GovernanceDecision decision)
 137    {
 2138        ArgumentNullException.ThrowIfNull(policyType);
 2139        ArgumentNullException.ThrowIfNull(decision);
 140
 2141        policyResults[policyType.FullName ?? policyType.Name] = decision;
 2142        policyResults[policyType.Name] = decision;
 2143        return this;
 144    }
 145
 146    /// <summary>
 147    /// Requires explicit deterministic policy results and configures the supplied marker type.
 148    /// </summary>
 149    public GovernanceTestHarnessOptions RequirePolicyResult<TPolicy>(GovernanceDecision decision)
 150    {
 1151        return RequirePolicyResult(typeof(TPolicy), decision);
 152    }
 153
 154    /// <summary>
 155    /// Requires explicit deterministic policy results and configures the supplied marker type.
 156    /// </summary>
 157    public GovernanceTestHarnessOptions RequirePolicyResult(Type policyType, GovernanceDecision decision)
 158    {
 1159        RequireExplicitPolicyResults = true;
 1160        return SetPolicyResult(policyType, decision);
 161    }
 162
 163    internal bool TryGetPolicyDecision(string policyToken, out GovernanceDecision decision)
 164    {
 5165        return policyResults.TryGetValue(policyToken, out decision!);
 166    }
 167
 168    internal void Validate()
 169    {
 6170        ArgumentNullException.ThrowIfNull(DefaultPolicyDecision);
 6171        ArgumentNullException.ThrowIfNull(DefaultCapabilityGrantDecision);
 6172    }
 173}
 174
 175/// <summary>
 176/// Provides service registration helpers for the test-only AsiBackbone harness.
 177/// </summary>
 178public static class GovernanceTestHarnessServiceCollectionExtensions
 179{
 180    /// <summary>
 181    /// Adds the AsiBackbone test harness using default allow-all policy and capability behavior.
 182    /// </summary>
 183    public static IServiceCollection AddAsiBackboneTestHarness(this IServiceCollection services)
 184    {
 185        return services.AddAsiBackboneTestHarness(_ => { });
 186    }
 187
 188    /// <summary>
 189    /// Adds the AsiBackbone test harness using deterministic test-only substitutions.
 190    /// </summary>
 191    public static IServiceCollection AddAsiBackboneTestHarness(
 192        this IServiceCollection services,
 193        Action<GovernanceTestHarnessOptions> configure)
 194    {
 195        ArgumentNullException.ThrowIfNull(services);
 196        ArgumentNullException.ThrowIfNull(configure);
 197
 198        GovernanceTestHarnessOptions options = new();
 199        configure(options);
 200        options.Validate();
 201
 202        _ = services.AddSingleton(options);
 203        _ = services.AddSingleton<GovernanceTestDecisionReceiptSink>();
 204        _ = services.AddSingleton<IGovernancePolicyEvaluator<GovernanceEvaluationContext>, GovernanceTestHarnessPolicyEv
 205        _ = services.AddSingleton<IEndpointCapabilityGrantValidator, GovernanceTestHarnessEndpointCapabilityGrantValidat
 206
 207        if (options.RegisterInMemoryAuditSink)
 208        {
 209            _ = services.AddSingleton<IDecisionReceiptSink>(static serviceProvider =>
 210                serviceProvider.GetRequiredService<GovernanceTestDecisionReceiptSink>());
 211        }
 212
 213        if (options.RegisterInMemoryOutboxStore)
 214        {
 215            services.TryAddSingleton<InMemoryGovernanceOutboxStore>();
 216            services.TryAddSingleton<IGovernanceOutboxStore>(static serviceProvider =>
 217                serviceProvider.GetRequiredService<InMemoryGovernanceOutboxStore>());
 218        }
 219
 220        if (options.RegisterDeterministicSigningService)
 221        {
 222            services.TryAddSingleton<IGovernanceSigningService, GovernanceTestSigningService>();
 223        }
 224
 225        return services;
 226    }
 227}
 228
 229/// <summary>
 230/// Deterministic policy evaluator used by the test harness.
 231/// </summary>
 232/// <remarks>
 233/// <para>
 234/// Selecting a decision per policy token simulates a host-supplied decision policy that reads the
 235/// <c>endpoint.policy_types</c> metadata entry and varies its outcome. It does not mirror framework behavior: the
 236/// framework never resolves an endpoint's policy type or selects constraints from it, and the registered evaluator
 237/// evaluates every registered constraint on every governed endpoint. A test that configures different results for
 238/// two policy tokens is asserting against host policy it must actually implement, not against something AsiBackbone
 239/// provides. Without such a host decision policy, both endpoints evaluate identically in production.
 240/// </para>
 241/// <para>
 242/// Initializes a new instance of the <see cref="GovernanceTestHarnessPolicyEvaluator" /> class.
 243/// </para>
 244/// </remarks>
 245public sealed class GovernanceTestHarnessPolicyEvaluator(GovernanceTestHarnessOptions options) : IGovernancePolicyEvalua
 246{
 247    private const string PolicyTypesMetadataKey = "endpoint.policy_types";
 248    private readonly GovernanceTestHarnessOptions options = options ?? throw new ArgumentNullException(nameof(options));
 249
 250    /// <inheritdoc />
 251    public ValueTask<GovernanceDecision> EvaluateAsync(
 252        GovernanceEvaluationContext context,
 253        CancellationToken cancellationToken = default)
 254    {
 255        ArgumentNullException.ThrowIfNull(context);
 256        cancellationToken.ThrowIfCancellationRequested();
 257
 258        foreach (string policyToken in ResolvePolicyTokens(context))
 259        {
 260            if (options.TryGetPolicyDecision(policyToken, out GovernanceDecision? decision))
 261            {
 262                return ValueTask.FromResult(AsiBackboneTestHarnessDecisionFactory.WithTelemetry(decision, context));
 263            }
 264        }
 265
 266        return options.RequireExplicitPolicyResults
 267            ? ValueTask.FromResult(GovernanceDecision.Deny(
 268                "test_harness.policy_result.missing",
 269                "The AsiBackbone test harness requires an explicit policy result for this endpoint policy marker.",
 270                correlationId: context.CorrelationId,
 271                policyVersion: context.PolicyVersion,
 272                policyHash: context.PolicyHash))
 273            : ValueTask.FromResult(AsiBackboneTestHarnessDecisionFactory.WithTelemetry(options.DefaultPolicyDecision, co
 274    }
 275
 276    private static IEnumerable<string> ResolvePolicyTokens(GovernanceEvaluationContext context)
 277    {
 278        if (!context.Metadata.TryGetValue(PolicyTypesMetadataKey, out string? policyTypesValue)
 279            || string.IsNullOrWhiteSpace(policyTypesValue))
 280        {
 281            yield break;
 282        }
 283
 284        foreach (string policyToken in policyTypesValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOp
 285        {
 286            if (!string.IsNullOrWhiteSpace(policyToken))
 287            {
 288                yield return policyToken;
 289            }
 290        }
 291    }
 292}
 293
 294/// <summary>
 295/// Deterministic capability-grant validator used by the test harness.
 296/// </summary>
 297/// <remarks>
 298/// Initializes a new instance of the <see cref="GovernanceTestHarnessEndpointCapabilityGrantValidator" /> class.
 299/// </remarks>
 300public sealed class GovernanceTestHarnessEndpointCapabilityGrantValidator(GovernanceTestHarnessOptions options) : IEndpo
 301{
 302    private readonly GovernanceTestHarnessOptions options = options ?? throw new ArgumentNullException(nameof(options));
 303
 304    /// <inheritdoc />
 305    public ValueTask<GovernanceDecision> ValidateAsync(
 306        HttpContext httpContext,
 307        EndpointGovernanceDescriptor descriptor,
 308        GovernanceDecision currentDecision,
 309        CancellationToken cancellationToken = default)
 310    {
 311        ArgumentNullException.ThrowIfNull(httpContext);
 312        ArgumentNullException.ThrowIfNull(descriptor);
 313        ArgumentNullException.ThrowIfNull(currentDecision);
 314        cancellationToken.ThrowIfCancellationRequested();
 315
 316        return ValueTask.FromResult(AsiBackboneTestHarnessDecisionFactory.WithTelemetry(
 317            options.DefaultCapabilityGrantDecision,
 318            currentDecision.CorrelationId,
 319            currentDecision.TraceId,
 320            currentDecision.PolicyVersion,
 321            currentDecision.PolicyHash));
 322    }
 323}
 324
 325/// <summary>
 326/// In-memory decision receipt sink with inspection helpers for tests.
 327/// </summary>
 328public sealed class GovernanceTestDecisionReceiptSink : IDecisionReceiptSink
 329{
 330    private readonly Lock gate = new();
 331    private readonly List<IDecisionReceipt> entries = [];
 332
 333    /// <summary>
 334    /// Gets a snapshot of decision receipt captured by the test sink.
 335    /// </summary>
 336    public IReadOnlyList<IDecisionReceipt> Entries
 337    {
 338        get
 339        {
 340            lock (gate)
 341            {
 342                return entries.ToArray();
 343            }
 344        }
 345    }
 346
 347    /// <summary>
 348    /// Clears captured decision receipt.
 349    /// </summary>
 350    public void Clear()
 351    {
 352        lock (gate)
 353        {
 354            entries.Clear();
 355        }
 356    }
 357
 358    /// <inheritdoc />
 359    public ValueTask WriteAsync(
 360        IDecisionReceipt residue,
 361        CancellationToken cancellationToken = default)
 362    {
 363        ArgumentNullException.ThrowIfNull(residue);
 364        cancellationToken.ThrowIfCancellationRequested();
 365
 366        lock (gate)
 367        {
 368            entries.Add(residue);
 369        }
 370
 371        return ValueTask.CompletedTask;
 372    }
 373}
 374
 375/// <summary>
 376/// Deterministic no-signature signing service for tests.
 377/// </summary>
 378public sealed class GovernanceTestSigningService : IGovernanceSigningService
 379{
 380    /// <inheritdoc />
 381    public ValueTask<SigningResult> SignAsync(
 382        SigningRequest request,
 383        CancellationToken cancellationToken = default)
 384    {
 385        ArgumentNullException.ThrowIfNull(request);
 386        cancellationToken.ThrowIfCancellationRequested();
 387
 388        return ValueTask.FromResult(SigningResult.NoSignature);
 389    }
 390}
 391
 392internal static class AsiBackboneTestHarnessDecisionFactory
 393{
 394    internal static GovernanceDecision WithTelemetry(
 395        GovernanceDecision decision,
 396        GovernanceEvaluationContext context)
 397    {
 398        ArgumentNullException.ThrowIfNull(context);
 399
 400        return WithTelemetry(
 401            decision,
 402            context.CorrelationId,
 403            traceId: null,
 404            context.PolicyVersion,
 405            context.PolicyHash);
 406    }
 407
 408    internal static GovernanceDecision WithTelemetry(
 409        GovernanceDecision decision,
 410        string? correlationId,
 411        string? traceId,
 412        string? policyVersion,
 413        string? policyHash)
 414    {
 415        ArgumentNullException.ThrowIfNull(decision);
 416
 417        string? resolvedCorrelationId = decision.CorrelationId ?? correlationId;
 418        string? resolvedTraceId = decision.TraceId ?? traceId;
 419        string? resolvedPolicyVersion = decision.PolicyVersion ?? policyVersion;
 420        string? resolvedPolicyHash = decision.PolicyHash ?? policyHash;
 421
 422        return decision.Outcome switch
 423        {
 424            GovernanceDecisionOutcome.Allowed => GovernanceDecision.Allow(
 425                resolvedCorrelationId,
 426                resolvedTraceId,
 427                resolvedPolicyVersion,
 428                resolvedPolicyHash),
 429            GovernanceDecisionOutcome.Warning => GovernanceDecision.Warning(
 430                decision.Reasons,
 431                resolvedCorrelationId,
 432                resolvedTraceId,
 433                resolvedPolicyVersion,
 434                resolvedPolicyHash),
 435            GovernanceDecisionOutcome.Denied => GovernanceDecision.Deny(
 436                decision.Reasons,
 437                resolvedCorrelationId,
 438                resolvedTraceId,
 439                resolvedPolicyVersion,
 440                resolvedPolicyHash),
 441            GovernanceDecisionOutcome.Deferred => GovernanceDecision.Defer(
 442                FirstReasonCode(decision, "test_harness.deferred"),
 443                FirstReasonMessage(decision, "Deferred by the AsiBackbone test harness."),
 444                resolvedCorrelationId,
 445                resolvedTraceId,
 446                resolvedPolicyVersion,
 447                resolvedPolicyHash),
 448            GovernanceDecisionOutcome.AcknowledgmentRequired => GovernanceDecision.RequireAcknowledgment(
 449                FirstReasonCode(decision, "test_harness.acknowledgment_required"),
 450                FirstReasonMessage(decision, "Acknowledgment required by the AsiBackbone test harness."),
 451                resolvedCorrelationId,
 452                resolvedTraceId,
 453                resolvedPolicyVersion,
 454                resolvedPolicyHash),
 455            GovernanceDecisionOutcome.EscalationRecommended => GovernanceDecision.Escalate(
 456                FirstReasonCode(decision, "test_harness.escalation_recommended"),
 457                FirstReasonMessage(decision, "Escalation recommended by the AsiBackbone test harness."),
 458                resolvedCorrelationId,
 459                resolvedTraceId,
 460                resolvedPolicyVersion,
 461                resolvedPolicyHash),
 462            _ => decision
 463        };
 464    }
 465
 466    private static string FirstReasonCode(GovernanceDecision decision, string fallback)
 467    {
 468        return decision.Reasons.Count == 0
 469            ? fallback
 470            : decision.Reasons[0].Code;
 471    }
 472
 473    private static string FirstReasonMessage(GovernanceDecision decision, string fallback)
 474    {
 475        return decision.Reasons.Count == 0
 476            ? fallback
 477            : decision.Reasons[0].Message;
 478    }
 479}