From ede9548cba1d24f4d6b9f4cdd0549c98cf3db033 Mon Sep 17 00:00:00 2001 From: Mathias Beaulieu-Duncan Date: Tue, 12 May 2026 16:46:10 -0400 Subject: [PATCH] test(altcha): runtime validation of check pipeline in Svrnty.Sample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a [Altcha]-decorated ProtectedActionCommand with IHasAltchaSolution and a StubAltchaVerifier that treats the literal "valid-solution" as passing PoW. Exercises both the HTTP MinimalApi and gRPC pipelines without requiring an external altcha service. Validated 4 scenarios on each transport (8 total, all pass): HTTP /api/command/protectedAction POST 6001 gRPC :6000 ------------------------------------------------------------------- no AltchaSolution 401 Unauthenticated AltchaSolution = "wrong" 401 Unauthenticated AltchaSolution = "valid-solution" 200 result OK + result addUser (no [Altcha]) 200 result OK + result The last row confirms backward compatibility: a request type that isn't decorated with [Altcha] bypasses the check entirely — the AltchaAuthorizationCheck self-applies and no-ops, and any consumer that doesn't call AddSvrntyAltcha() sees zero behavior change. Generated CommandServiceImpl.g.cs verified to include the ICommandAuthorizationCheck loop after validation, before handler invocation, with the materialized command instance in ctx.Command. Co-Authored-By: Claude Opus 4.7 (1M context) --- Svrnty.Sample/Program.cs | 8 +++++ Svrnty.Sample/ProtectedActionCommand.cs | 37 ++++++++++++++++++++++++ Svrnty.Sample/Protos/cqrs_services.proto | 14 +++++++++ Svrnty.Sample/Svrnty.Sample.csproj | 2 ++ 4 files changed, 61 insertions(+) create mode 100644 Svrnty.Sample/ProtectedActionCommand.cs diff --git a/Svrnty.Sample/Program.cs b/Svrnty.Sample/Program.cs index ecd5bd3..f77f3ab 100644 --- a/Svrnty.Sample/Program.cs +++ b/Svrnty.Sample/Program.cs @@ -1,6 +1,8 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Svrnty.CQRS; using Svrnty.CQRS.Abstractions; +using Svrnty.CQRS.Altcha; +using Svrnty.CQRS.Altcha.Abstractions; using Svrnty.CQRS.DynamicQuery; using Svrnty.CQRS.FluentValidation; using Svrnty.CQRS.Grpc; @@ -27,8 +29,14 @@ builder.Services.AddDynamicQueryWithProvider(); // Register commands and queries with validators builder.Services.AddCommand(); builder.Services.AddCommand(); +builder.Services.AddCommand(); builder.Services.AddQuery(); +// Wire the Altcha check + a stub verifier so [Altcha] commands run +// through the ICommandAuthorizationCheck pipeline. +builder.Services.AddSvrntyAltcha(); +builder.Services.AddSingleton(); + // Configure CQRS with fluent API builder.Services.AddSvrntyCqrs(cqrs => { diff --git a/Svrnty.Sample/ProtectedActionCommand.cs b/Svrnty.Sample/ProtectedActionCommand.cs new file mode 100644 index 0000000..a955b35 --- /dev/null +++ b/Svrnty.Sample/ProtectedActionCommand.cs @@ -0,0 +1,37 @@ +using Svrnty.CQRS.Abstractions; +using Svrnty.CQRS.Altcha.Abstractions; + +namespace Svrnty.Sample; + +// Exercises the ICommandAuthorizationCheck seam at runtime. +// The command is decorated with [Altcha] and carries the solution +// through IHasAltchaSolution. With Svrnty.CQRS.Altcha + a registered +// IAltchaVerifier, the framework's check pipeline reads the field, +// calls the verifier, and short-circuits on failure. +[Altcha] +public sealed class ProtectedActionCommand : IHasAltchaSolution +{ + public string Action { get; set; } = string.Empty; + public string? AltchaSolution { get; set; } +} + +public sealed class ProtectedActionCommandHandler : ICommandHandler +{ + public Task HandleAsync(ProtectedActionCommand command, CancellationToken cancellationToken = default) + { + return Task.FromResult($"executed:{command.Action}"); + } +} + +// Stub verifier that doesn't talk to an external altcha service — +// enough to exercise the check pipeline in isolation. Treats the +// literal string "valid-solution" as a passing PoW solution. +public sealed class StubAltchaVerifier : IAltchaVerifier +{ + public Task VerifyAsync(string payload, CancellationToken cancellationToken = default) + { + if (payload == "valid-solution") + return Task.FromResult(AltchaVerifyResult.Success); + return Task.FromResult(AltchaVerifyResult.Fail("stub-rejected")); + } +} diff --git a/Svrnty.Sample/Protos/cqrs_services.proto b/Svrnty.Sample/Protos/cqrs_services.proto index 10bad1c..8f478ee 100644 --- a/Svrnty.Sample/Protos/cqrs_services.proto +++ b/Svrnty.Sample/Protos/cqrs_services.proto @@ -9,6 +9,9 @@ service CommandService { // AddUserCommand operation rpc AddUser (AddUserCommandRequest) returns (AddUserCommandResponse); + // ProtectedActionCommand operation + rpc ProtectedAction (ProtectedActionCommandRequest) returns (ProtectedActionCommandResponse); + // RemoveUserCommand operation rpc RemoveUser (RemoveUserCommandRequest) returns (RemoveUserCommandResponse); @@ -40,6 +43,17 @@ message AddUserCommandResponse { int32 result = 1; } +// Request message for ProtectedActionCommand +message ProtectedActionCommandRequest { + string action = 1; + string altcha_solution = 2; +} + +// Response message for ProtectedActionCommand +message ProtectedActionCommandResponse { + string result = 1; +} + // Request message for RemoveUserCommand message RemoveUserCommandRequest { int32 user_id = 1; diff --git a/Svrnty.Sample/Svrnty.Sample.csproj b/Svrnty.Sample/Svrnty.Sample.csproj index 2410a08..38231f7 100644 --- a/Svrnty.Sample/Svrnty.Sample.csproj +++ b/Svrnty.Sample/Svrnty.Sample.csproj @@ -33,6 +33,8 @@ + +