to navigate Enter to open "…" all these words ANDOR to combine

Onboarding guide

27. Testing & Quality Infrastructure

What this group covers. Everything the codebase uses to prove itself: the four reusable test-support packages that ship out of MMCA.Common/Source/Hosting (MMCA.Common.Testing, MMCA.Common.Testing.Architecture, MMCA.Common.Testing.E2E, MMCA.Common.Testing.UI, four of the packages published from MMCA.Common and listed in MMCA.Common/FACTS.md:19-38), the architecture-fitness rule library that gates the build, the runtime-conformance bases that gate a booted host, the backend-less component Gallery harness, the BenchmarkDotNet performance suite, and the many per-repo test projects that consume all of it. The distinction to hold onto while reading: most of the types in this group are reusable bases, fixtures, harnesses, and helpers compiled into and shipped by MMCA.Common, while the concrete [Fact]-bearing test classes that subclass them live in each consumer repo (MMCA.Common.*.Tests, MMCA.ADC.*.Tests, MMCA.Store.*.Tests). Those individual test classes are cataloged by project in the companion rollup section; this chapter teaches the machinery they stand on. For how the tiers map onto CI jobs and solution filters, see Testing Architecture & Solution Composition.

There are six moving parts. Five map onto the test pyramid plus one governance layer; the sixth evaluates a dependency that is not deterministic and so cannot be asserted exactly:

  1. Integration-test scaffolding (IIntegrationTestFixture, IntegrationTestBase<TFixture>, SqlServerIntegrationTestFixtureBase<TEntryPoint>, CrossServiceFixtureBase, ProductionHostApplicationFactory<TEntryPoint>, JwtTokenGenerator, FeatureManagementTestExtensions, TestPolling, EntityBuilderBase<TBuilder, TEntity>) boots a real service host in-process against a throwaway SQL Server database and drives it over HTTP.
  2. Architecture fitness functions (IArchitectureMap, ArchitectureMapBase, Layer, LayerRef, ArchitectureAssert, RuleHelpers, CrossEntityNavigationFinder, the twenty-nine ArchitectureRules partial files, and the forty-six abstract *TestsBase classes including RouteAuthorizationTestsBase, ModuleConformanceTestsBase<TModule> and BrandColorTokenTestsBase) turn architectural rules into build-gating assertions that run identically across every repo.
  3. Component (bUnit) testing (BunitComponentTestBase, TestPrincipal, BunitInteractionExtensions, CapturingHttpMessageHandler, UiHttpServiceHarness, HttpTestDoubles, StubTokenStorageService, MarkupSnapshot) render Blazor components in isolation with real MudBlazor services and faked HTTP/auth edges.
  4. End-to-end (Playwright) testing (PlaywrightFixture, E2ETestBase, E2ETestConfiguration, AuthOutcomeRules, PageExtensions, AxeOptions, AccessibilityViolationException, WebVitalsCollector, the reusable page objects LoginPage / RegisterPage / ProfilePage / ForgotPasswordPage / ResetPasswordPage, and the shipped workflow suites such as AuthorizationTestsBase) drive a real browser against a running app, asserting accessibility and performance alongside behavior.
  5. Contract and pipeline bases (SecurityHeadersTestsBase, OpenApiContractTestsBase<TFixture>, ProblemDetailsContractTestsBase<TFixture>, ServiceInfoVersioningContractTestsBase<TFixture>, GracefulShutdownTestsBase<TEntryPoint>, DecoratorPipelineOrderTestsBase<TCommand, TCommandResult, TQuery, TQueryResult>, MiddlewarePipelineOrderTestsBase, DependencyInjectionAssert, HandlerTestBase<THandler>) pin cross-cutting HTTP and pipeline guarantees so a refactor cannot silently drop them.
  6. Model-evaluation tests (GoldenCase, GoldenCorpus, GoldenReplayTests, ReplayHandler, LiveJudgeTests, PromptContractTests) hold ADC's AI session-scoring judge to a committed corpus of recorded cases, one half replayed offline on every CI leg and one half scored by the real model behind a trait gate.

This whole group is the [Rubric §14, Testability] story made concrete: the framework does not merely permit testing, it ships the reusable substrate so every consumer tests the same way. The front-end tiers additionally carry [Rubric §21, Accessibility], [Rubric §22, Responsive/Cross-Browser], [Rubric §23, Front-End Performance], and [Rubric §28, Front-End Testing]; the fitness library carries [Rubric §34, Architecture Governance & Documentation]. Two ADRs govern the two governance tiers and are worth reading before touching anything here: ADR-015 for the structural rules, and ADR-058 for the runtime conformance suites that cover exactly what ADR-015 declared out of scope: "the tests assert structure / registration, not runtime behavior" (Website/docs-src/adr/015-architecture-fitness-functions.md:64-65).

Integration tests: a real host, a throwaway database, a per-test reset

The integration tier boots the actual application, not a mock of it. The abstraction at its center is IIntegrationTestFixture (MMCA.Common.Testing/Fixtures/IIntegrationTestFixture.cs:8): a two-method contract, CreateClient() (IIntegrationTestFixture.cs:11) and ResetDatabaseAsync() (IIntegrationTestFixture.cs:19), that hides how the host and its database are provisioned. Its remarks are load-bearing: a host running multiple physical data sources (database per service, see primer and ADR-006) must reset every relational source, and a fixture can resolve IEntityDataSourceRegistry / IDataSourceResolver from the booted host to enumerate them (IIntegrationTestFixture.cs:13-18).

IntegrationTestBase<TFixture> (MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:13) is the per-test base every integration test class inherits. It implements xUnit's IAsyncLifetime, so InitializeAsync resets the database before each test (IntegrationTestBase.cs:31) and DisposeAsync disposes the HTTP client after (IntegrationTestBase.cs:34-39). It exposes typed HTTP helpers (GetAsync<T>, PostAsync<T>, PutAsync<T>, PutAsync, DeleteAsync, IntegrationTestBase.cs:51-72), bearer-token management (SetBearerToken / ClearAuthentication, IntegrationTestBase.cs:42-48), and a thread-safe NextId() counter seeded at 1000 (IntegrationTestBase.cs:16,75) so parallel tests never collide on generated identifiers. Downstream projects subclass it to add domain-specific auth and entity helpers.

SqlServerIntegrationTestFixtureBase<TEntryPoint> (MMCA.Common.Testing/Fixtures/SqlServerIntegrationTestFixtureBase.cs:27) is the concrete fixture scaffolding. InitializeAsync (SqlServerIntegrationTestFixtureBase.cs:67) mints a GUID-suffixed database name (:71-72), sets ASPNETCORE_ENVIRONMENT=Testing and the top-level connection string as process environment variables (so the host reads them at configure-time, :75-77), builds the subclass-supplied WebApplicationFactory (:79), and forces database creation by requesting the first client, which runs the host's Migrate init strategy (:81-84). It then builds a Respawn checkpoint that ignores __EFMigrationsHistory (:90-94); ResetDatabaseAsync (:99) replays that checkpoint between tests, and DisposeAsync (:115) drops the throwaway database (:167) and restores every pushed environment variable (:130, restore loop at :157-165, first-value-wins bookkeeping at :146-155 so a re-pushed key cannot clobber its own restore point). The Testing environment is chosen deliberately so appsettings.Development.json (which points a module's DataSources entry at localhost) does not load, leaving the resolver to collapse onto the overridden top-level connection string, a single-database monolith shape (:16-24). Server selection defaults to LocalDB but is overridable through SqlBaseEnvironmentVariable (:58, read at :69-70) so CI can target a SQL service container. Subclasses push their own host-specific settings (test JWT key material, throttle lifts, faked gRPC edges) through the ConfigureTestEnvironment hook (:142, invoked at :77), which routes them through the same restore bookkeeping. The fixture also exposes ConnectionString (:45) so SQL-fidelity tests can read the raw tables, and Services (:52) so a cross-service test can resolve a consumer-side handler out of the booted host. Because these fixtures need a reachable SQL Server, the per-module *.Integration.slnf suites build in a headless sandbox but only run in CI.

One tier up sits CrossServiceFixtureBase (MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:41), which boots several hosts in one process against a real Testcontainers SQL Server and a real Testcontainers RabbitMQ (CrossServiceFixtureBase.cs:2-3,18-25), so the genuine outbox to broker to consumer round-trip is exercised rather than faked. Each logical source it routes is a CrossServiceDataSource record pairing the config key with its physical database (CrossServiceFixtureBase.cs:15, list supplied by the subclass at :60). The design note worth internalizing is why the hosts must boot strictly sequentially: every host reads its connection string, MessageBus settings and JWT settings from configuration at configure-time, before builder.Build(), so process environment variables are the only override channel that lands in time, and the one genuinely per-host key is the SQL connection string (CrossServiceFixtureBase.cs:26-39). A booted host has already snapshotted its connection, so mutating the environment for the next one is safe. Two smaller decisions in the same file are worth knowing because they are non-obvious: the databases are pre-created before any host boots, since EF's CREATE DATABASE runs before its migration lock is taken and a double-booted host would race itself (CrossServiceFixtureBase.cs:107-111), and each module is routed to a named data source whose connection string differs only by Application Name, so EF's process-global model cache keys each host's model separately instead of letting the first booted host win (CrossServiceFixtureBase.cs:231-246,260-276). A host that needs no database at all gets the much smaller boot path: ProductionHostApplicationFactory<TEntryPoint> (MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:23) pins UseEnvironment("Production") (ProductionHostApplicationFactory.cs:36) so the production-only branches (restrictive CORS, HSTS emission) are the ones under test, and captures the started IHost (ProductionHostApplicationFactory.cs:29) because StopAsync is not reachable through the WebApplicationFactory surface at all.

Four helpers round out the tier. JwtTokenGenerator (MMCA.Common.Testing/Support/JwtTokenGenerator.cs:30) issues RS256-signed tokens (GenerateToken, JwtTokenGenerator.cs:112, signing credentials built at :130-131) using an embedded development RSA keypair (DefaultPublicKeyPem at :49, DefaultPrivateKeyPem at :68) under a fixed kid of mmca-test-key (:41), so integration tests exercise the exact JWKS/RS256 validation code path production runs (ADR-004); its ConfigureInProcessTokenValidation (:170) is what a test factory calls to re-point a host's JwtBearerOptions at that committed key instead of a network authority (it nulls both Authority and ConfigurationManager, :174-175). The class remarks flag, correctly, that the committed keypair is insecure by design and must never be used in a real deployment (:22-28). FeatureManagementTestExtensions (MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:10) adds a ConfigureTestFeatureFlags extension member (:35) that layers an in-memory FeatureManagement:* collection on top of the configuration the host already registered and re-registers the merged root in its place (:40-59); building a flags-only root instead would silently hand every component constructed afterwards a configuration with nothing but feature flags in it, because .NET DI resolves a non-collection dependency to the last registration (:18-25). TestPolling (MMCA.Common.Testing/Support/TestPolling.cs:9) replaces the pre-assert sleep that eventually-consistent paths tempt you into: PollUntilAsync (TestPolling.cs:22) probes until the condition holds or a 60-second budget expires at a 500 ms interval (TestPolling.cs:31-32) and returns the last probed value either way, so a timeout still fails on the real assertion message. EntityBuilderBase<TBuilder, TEntity> (MMCA.Common.Testing/Builders/EntityBuilderBase.cs:9) is a minimal fluent-builder base whose single abstract Build() (:17) returns the entity through its domain factory, so test setup specifies only what a test cares about. Together these embody [Rubric §11, Security] (real token validation rather than bypassed auth middleware) and [Rubric §14, Testability].

Architecture fitness functions: rules that gate the build

The layering and DDD conventions this codebase commits to are not left to code review, they are executed as tests. The reusable rule library lives in MMCA.Common.Testing.Architecture and is the subject of ADR-015. Its keystone is IArchitectureMap (MMCA.Common.Testing.Architecture/IArchitectureMap.cs:39): the single per-repo boundary every fitness function keys off. Each repo supplies one implementation (for example StoreArchitectureMap) declaring its layer and module assemblies as LayerRef records (IArchitectureMap.cs:31) tagged by the Layer enum (IArchitectureMap.cs:9), and exposes them through query members such as OfLayer, ModuleDomain, ModuleApplication, For, ModuleOf, and OtherModuleNamespaces (IArchitectureMap.cs:51-81). Most of that surface is derived rather than hand-written: ArchitectureMapBase (MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:11) computes the projections from a single DefineLayers() declaration (ArchitectureMapBase.cs:22, lazily materialized at :13,15-16,25), which also centralizes every assembly and namespace string in one file so Ubuntu CI's case sensitivity is handled in one place (ArchitectureMapBase.cs:8-9); it additionally ships a FindRepoRoot(solutionFileName) walker (ArchitectureMapBase.cs:79) so doc- and config-consistency rules can read committed files no matter what the runner's working directory is. The shared rules consume only the interface, which is why one rule body runs identically across MMCA.Common, MMCA.Store, MMCA.ADC, and Helpdesk: the map is the only thing that varies. Layer deliberately includes optional layers (Ui, Grpc, Contracts, ServiceHost, IArchitectureMap.cs:16-19) that a repo simply omits, so a rule iterating them is vacuously satisfied with no compile dependency on an absent assembly (IArchitectureMap.cs:3-7).

The rule bodies are split across thirty ArchitectureRules partial files (cancellation tokens, cascade soft-delete, command validators, contracts, controllers, cycles, domain-event handler saves, domain throws, entities, error catalog, events, folder width, governance, handlers, handler results, idempotency, immutability, layers, localization, localized text, markup, modules, naming, protos, purity, slices, soft delete, specifications, transport, and upcasters; the partial type is declared in the first of them at MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CancellationTokens.cs:5). The aggregate-convention rules live inside ArchitectureRules.Entities.cs (for example DomainExposesAggregateRoots at MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Entities.cs:8, AggregateRootsHaveResultFactory at :19, and the generalized DomainFactoriesReturnResult at :53 that extends the "factories always return Result<T>" convention from aggregate roots to value objects) rather than in a dedicated partial, and are surfaced through AggregateConventionTestsBase (MMCA.Common.Testing.Architecture/Bases/Domain/AggregateConventionTestsBase.cs:10). Above the rules sit the abstract *TestsBase classes under Bases/, forty-seven files today (LayerDependencyTestsBase, DomainPurityTestsBase, MicroserviceExtractionTestsBase, ModuleIsolationTestsBase, PiiConventionTestsBase, DependencyVersionTestsBase, IntegrationEventContractTestsBase, DataResidencyTestsBase, RawQueryableConventionTestsBase, and more), each exposing its rules as [Fact]s that a sealed per-repo subclass activates by supplying its map. AggregateConventionTestsBase shows the shape in miniature: one abstract Map property and one [Fact] per rule (MMCA.Common.Testing.Architecture/Bases/Domain/AggregateConventionTestsBase.cs:12-24). How many fitness methods the package ships, and how many of them MMCA.Common's own build executes, is a generated and CI-gated number in MMCA.Common/FACTS.md:45-50: read it there rather than restating it anywhere else.

Failures report through ArchitectureAssert (MMCA.Common.Testing.Architecture/ArchitectureAssert.cs:8), which has two overloads: one lists the failing types from a NetArchTest TestResult (ArchitectureAssert.cs:11-23), the other lists a reflection-derived violation set (ArchitectureAssert.cs:26-32). Rules NetArchTest cannot express (method return types, generic constraints, property accessors, attribute usage) reflect over loaded types via the internal RuleHelpers (MMCA.Common.Testing.Architecture/RuleHelpers.cs:14), whose LoadableTypes extension property tolerates a partially resolvable assembly by falling back to the ReflectionTypeLoadException's resolved types (RuleHelpers.cs:19-33), and whose HasPublicMutableSetter treats an init-only setter as immutable by checking for the IsExternalInit required modifier (RuleHelpers.cs:121-137). One such walk, CrossEntityNavigationFinder (MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:97), is an ExpressionVisitor that collects the entity types a specification's criteria navigates to beyond its own (ArchitectureRules.Specifications.cs:101-119), because in a polyglot setup that navigation may cross a physical data source where the join is not translatable (ArchitectureRules.Specifications.cs:9-15). These runtime rules are the second of two enforcement layers, the first being the compile-time MSBuild layer guard (MMCA.Common/Source/Build/MMCA.Common.LayerEnforcement.targets, see group 14); ADR-015 describes both, and this is the clearest [Rubric §34, Architecture Governance] expression in the codebase.

The fitness library reaches beyond pure layering into cross-cutting product guarantees. RouteAuthorizationTestsBase (MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs:22, tagged rubric §25 in its own remarks, [Rubric §25, Navigation & IA]) reflects over routable Blazor pages and asserts every governed page keeps its [Authorize(Roles = "...")] gate (RouteAuthorizationTestsBase.cs:49-61), so an admin route cannot regress to a bare [Authorize] any authenticated user can reach. It detects RouteAttribute and AuthorizeAttribute by full-name reflection (RouteAuthorizationTestsBase.cs:24-25) so the package stays free of ASP.NET references, and a MinimumGovernedPages floor (:47, asserted at :63-74) guards against a moved namespace silently emptying the scan. ModuleConformanceTestsBase<TModule> (MMCA.Common.Testing.Architecture/Bases/Layering/ModuleConformanceTestsBase.cs:21) pins the three-member contract ModuleLoader actually registers on (Name, Dependencies, RequiresDependencies), because drift in any of them does not throw, it silently reorders registration or swaps a real service for a disabled stub (ModuleConformanceTestsBase.cs:3-10); it reads those members through the IModule full name by reflection (:24) so the package stays free of the framework's transitive graph and still dispatches to the interface's default implementations (:12-17). BrandColorTokenTestsBase (MMCA.Common.Testing.Architecture/Bases/Ui/BrandColorTokenTestsBase.cs:13, [Rubric §20, Design System & Theming]) reads landing-page stylesheets embedded as manifest resources and fails the build if a host re-hardcodes the brand hex #1565C0 instead of sourcing var(--mmca-primary) from the shared token (BrandColorTokenTestsBase.cs:15-16,41-49), with a non-empty check on the embedded list so the guard cannot pass vacuously (:27-28). DependencyVersionTestsBase (MMCA.Common.Testing.Architecture/Bases/Governance/DependencyVersionTestsBase.cs:15, [Rubric §32, Dependency & Supply-Chain]) checks the repo's pinned package majors and fails the build on two commercial-license traps a blanket package bump would otherwise walk into unnoticed: MassTransit at major 9 (DependencyVersionTestsBase.cs:24-37, ADR-016) and SixLabors.ImageSharp at major 4, whose MSBuild targets fail at build time without a license key (DependencyVersionTestsBase.cs:47-60). ConstructorDependencyCountTestsBase (Bases/ConstructorDependencyCountTestsBase.cs:14) turns the SRP judgement call into a numeric ceiling on Application-service constructors ([Rubric §1, SOLID]), with its own non-vacuity guard so a scan that finds no services fails rather than passes (Bases/ConstructorDependencyCountTestsBase.cs:33-34), and ObservabilityConventionTestsBase (Bases/ObservabilityConventionTestsBase.cs:30) pairs every SLO alert a consumer's infra/main.bicep provisions with a same-severity triage section in its infra/OPERATIONS.md, in both directions, with a minimum-spec floor so a drifted parse anchor fails loudly instead of passing with zero discovered alerts (Bases/ObservabilityConventionTestsBase.cs:6-13,39, [Rubric §13, Observability & Operability]).

Five of the newer bases show how far the pattern has been pushed. CancellationTokenConventionTestsBase (Bases/CancellationTokenConventionTestsBase.cs:16) requires every public async method on a public Application- or Infrastructure-layer type to declare a trailing CancellationToken cancellationToken, because cancellation is only end-to-end if it is uniform: one method that swallows the token turns a cancelled request, an expired CQRS timeout budget or a stopping host into work that keeps running against the database, and a token in a non-trailing position or under a different name defeats the mechanical forwarding the decorator pipeline and the repositories rely on (Bases/CancellationTokenConventionTestsBase.cs:3-9), with signatures the repo does not own exempted automatically (:11-14). NamespaceCycleTestsBase (Bases/NamespaceCycleTestsBase.cs:15) requires the top-level namespaces inside each layer assembly to form a directed acyclic graph, catching the folder pair that has grown into one tangled unit at a granularity the two coarse layer rules cannot see inside; its own remarks are careful to say the rule is signature-level reflection blind to method bodies, so green means "no structural cycle", not "no coupling" (Bases/NamespaceCycleTestsBase.cs:3-13). IdempotencyConventionTestsBase (Bases/IdempotencyConventionTestsBase.cs:10) requires every POST action in a repo's API layer to state in code whether a retried request replays the original response ([Idempotent]) or deliberately does not ([NonIdempotent("why")]), so the answer is a compile-time artifact rather than tribal knowledge (Bases/IdempotencyConventionTestsBase.cs:3-8). ProtoContractTestsBase (Bases/ProtoContractTestsBase.cs:19) is the synchronous counterpart to IntegrationEventContractTestsBase: it rebuilds a repo's live gRPC .proto contract and diffs it against a committed snapshot, and it is explicitly consumer-facing, because MMCA.Common ships the gRPC plumbing rather than any contracts of its own (Bases/ProtoContractTestsBase.cs:3-11). FolderWidthTestsBase (MMCA.Common.Testing.Architecture/Bases/Governance/FolderWidthTestsBase.cs:14, [Rubric §5, Vertical Slice]) walks a repo root and fails when any folder under Source/ or Tests/ holds more than MaxDirectFiles direct code files, twelve by default (FolderWidthTestsBase.cs:23), so a folder keeps naming a feature instead of drifting into a technical bucket (FolderWidthTestsBase.cs:3-13); a .razor component and its code-behind count as one unit, resource and generated files do not count at all, and build output plus tool-owned trees (bin, obj, Migrations, Platforms, Resources, wwwroot) are skipped outright (:8-12). A repo activates it with a thin subclass supplying its RepoRoot (:20) and, where a flat layout is documented, its ExemptFolderSuffixes (:29); the single [Fact] delegates to the ArchitectureRules.FoldersStayNarrow rule body (:31-33), which is how the feature-by-folder convention of ADR-109 became a build gate rather than a review note. Sibling bases pin integration-event contracts (ADR-010), the one-upcaster-per-source-contract rule that keeps the upcast chain a function (so which contract a handler receives cannot depend on DI registration order, MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:7-12, ADR-090), service-contract purity so an extracted service's wire surface carries only Shared and contract types and never the producer's internals (MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:13-18, ADR-007), data residency, forms conventions, localization resources, concurrency, controller shape, state management, UI architecture, and framework-version consistency, so the governance-as-tests pattern spans much of the 34-category rubric.

Component tests: real MudBlazor, faked edges

The bUnit tier renders a single Blazor component in-process with its real dependencies but stubbed network and auth. BunitComponentTestBase (MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:37) registers MudBlazor services and the vendor-neutral toast/confirm facades every migrated page injects (BunitComponentTestBase.cs:46,53), puts JSInterop in loose mode so MudBlazor components that probe JS during render do not throw (BunitComponentTestBase.cs:55), then wires a mutable AuthenticationStateProvider (BunitComponentTestBase.cs:58, the provider class at :162) plus an IsAuthenticatedAuthorizationService (BunitComponentTestBase.cs:57, class at :176) so both <AuthorizeView> cascades and pages that inject the provider directly behave. Tests render anonymously by default via RenderUnderTest<TComponent> (BunitComponentTestBase.cs:124) or as a supplied ClaimsPrincipal via RenderAs<TComponent> (BunitComponentTestBase.cs:130, which sets the provider and the cascading AuthenticationState together at :135-140), with a SetUser hook (:121) for mid-test auth changes and TestPrincipal (MMCA.Common.Testing.UI/Infrastructure/TestPrincipal.cs:7) minting the authenticated principal: a name claim, the user identifier written twice (under sub and under NameIdentifier, because a real principal reaches a page under either name), the requested roles, and an authentication type so IsAuthenticated is true (TestPrincipal.cs:22-32, rationale at :14-21). RenderMudProviders (BunitComponentTestBase.cs:148) mounts the popover, dialog, and snackbar providers and returns them as a MudProviderHandles record (BunitComponentTestBase.cs:157) so components that open a dialog or raise a toast have somewhere to render. One helper is worth knowing before you write your first list-page test: ConfigureDataGridListPageHost (BunitComponentTestBase.cs:100) wires the list-page state services, an inert viewport double so IsMobile is deterministic (:108-111), persistent component state (:114), and SetRendererInfo last (:116-117), because SetRendererInfo freezes the bUnit service provider and every registration made after it is silently ignored (:77-83). The class is pinned to bUnit v2 (the line compatible with xUnit v3 and Microsoft Testing Platform) and isolates every version-specific symbol here so a bUnit change touches only this file (BunitComponentTestBase.cs:29-34). Localization is pre-registered (AddLocalization, BunitComponentTestBase.cs:63-64) so components injecting IStringLocalizer<T> (ADR-027) render without per-test setup, a [Rubric §27, i18n] touch. Test bodies then read as user actions rather than DOM queries through BunitInteractionExtensions (MMCA.Common.Testing.UI/Infrastructure/BunitInteractionExtensions.cs:12), a generic extension<TComponent>(IRenderedComponent<TComponent>) block (:14) offering FindButtonByText (:18, which throws listing every button present when nothing matches), ClickButtonByText (:28), and HasText (:32), all keyed on accessible text rather than brittle CSS paths.

HTTP-backed UI services are exercised without a server through CapturingHttpMessageHandler (MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:19), a canned-response, request-recording HttpMessageHandler supporting both a responder delegate (CapturingHttpMessageHandler.cs:39) and route registration (SetResponse, CapturingHttpMessageHandler.cs:49), with registered routes consulted first and unmatched requests returning 404 to mirror the WebAPI's not-found behavior (CapturingHttpMessageHandler.cs:7-17,119-137); it rebuilds each response fresh so a Polly retry never reuses a consumed HttpContent (CapturingHttpMessageHandler.cs:141-150), and records every request as a CapturedRequest (CapturingHttpMessageHandler.cs:158) against a registered Route (CapturingHttpMessageHandler.cs:139), flattening request and content headers into one case-insensitive lookup so a caller asserting If-Match or Content-Type does not have to know which half carries it (:94-117). UiHttpServiceHarness (MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:12) wraps that handler with a FreshApiClientFactory (UiHttpServiceHarness.cs:73) returning a fresh client per call, which is load-bearing because the UI services dispose the client after each request, so a caching factory would hand later calls a disposed one (UiHttpServiceHarness.cs:66-72), plus a fixed-token StubTokenStorageService (MMCA.Common.Testing.UI/Infrastructure/StubTokenStorageService.cs:13, constructed and exposed at UiHttpServiceHarness.cs:48,61), all on a https://gateway.test/ base address (UiHttpServiceHarness.cs:15). Tests that would rather wire the pieces individually reach for HttpTestDoubles (MMCA.Common.Testing.UI/Infrastructure/HttpTestDoubles.cs:12), which exposes the same factory and token stub standalone (:23,28) alongside the canned JsonResponse/EmptyResponse/ProblemResponse builders (:33,37,47) that reproduce the shapes the WebAPI actually emits. MarkupSnapshot (MMCA.Common.Testing.UI/Infrastructure/MarkupSnapshot.cs:21) adds dependency-free golden-markup regression testing: Match (MarkupSnapshot.cs:31) normalizes the per-render GUIDs MudBlazor injects (MarkupSnapshot.cs:64-70), compares against a committed baseline under Snapshots/ next to the calling test (located through a [CallerFilePath] argument, MarkupSnapshot.cs:31,37), and returns a MarkupSnapshotResult (MarkupSnapshot.cs:104) for the caller to assert on, which keeps the shipped package free of an assertion-library dependency (MarkupSnapshot.cs:10-12). UPDATE_SNAPSHOTS=1 rewrites baselines (MarkupSnapshot.cs:41-46) and a missing baseline is written but reported as a non-match so a regression cannot slip through on an absent snapshot (MarkupSnapshot.cs:48-54). This tier is [Rubric §28, Front-End Testing] and [Rubric §18, UI Architecture].

End-to-end tests: a real browser, accessibility and performance as gates

The E2E tier drives a real browser through Playwright. PlaywrightFixture (MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:6) is an xUnit collection fixture (its E2ETestCollection definition sits beside it at PlaywrightFixture.cs:47-51, the class itself at :48) that launches the engine selected from configuration, chromium, firefox, or webkit, with unknown values falling back to Chromium (PlaywrightFixture.cs:17-22). That environment-selected engine is what lets CI run the same suite as a cross-browser matrix, [Rubric §22, Responsive/Cross-Browser]: MMCA.Common's ui-e2e job is a three-engine matrix (MMCA.Common/.github/workflows/ci.yml:236-237) with E2E_BROWSER handed to each leg (:298). Headless mode, slow motion, base URL, timeouts, trace capture, and the seeded admin/user credentials all come from E2ETestConfiguration (MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:8), whose nested AdminCredentials (E2ETestConfiguration.cs:66) and UserCredentials (E2ETestConfiguration.cs:78) let a downstream project set app-specific defaults through a [ModuleInitializer] while environment variables always win. Two of its knobs exist purely to de-flake CI: AuthTimeout (E2ETestConfiguration.cs:27) tunes the slowest step, the post-auth round-trip, independently of the 30-second general default (:18-19), and AuthGraceTimeout (E2ETestConfiguration.cs:38, default 15 seconds at :39) gives the success signal a window to appear after a transient error alert flashes during the success-path reload.

E2ETestBase (MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:9) is the per-test base on top of that fixture. It opens a fresh browser context per test with IgnoreHTTPSErrors and the configured base URL (E2ETestBase.cs:20-37), optionally records a Playwright trace (:32-35) and, when E2E_TRACE names a directory, keeps only the traces of tests that failed so a full-suite run yields exactly the inspectable failures (:68-90, the failed-only branch at :79-85). Its LoginAsync (E2ETestBase.cs:98) clears both token stores before signing in (localStorage for the WASM host and the HttpOnly session cookie for the Server host, :100-114), and WaitForAuthResultAsync (:214) races three signals so success detection does not depend on the logout button having hydrated (:219-228, rationale at :206-213). The verdict itself is not inline: it is a pure function, AuthOutcomeRules.Classify (MMCA.Common.Testing.E2E/Infrastructure/AuthOutcomeRules.cs:38, invoked at E2ETestBase.cs:236-239), which returns an AuthOutcome (AuthOutcomeRules.cs:4) of Succeeded, ErrorShown, or Silent, so the classification is testable without a browser and the silent case (no navigation and no rendered error) can no longer be mistaken for success (AuthOutcomeRules.cs:19-26); anything other than success gets the grace window once (E2ETestBase.cs:248, AuthSucceededWithinGraceAsync at :282). ScanAsync (:334) and ScanGridAsync (:324) wrap the accessibility gate for settled pages and for MudDataGrid list pages respectively: the grid variant waits for a data row and for the loading bar to disappear before scanning and applies the pager carve-out (:326-329), while the plain scan stays fully strict on WCAG 2.1 AA (:336-337).

The hard part of Blazor E2E is timing, and PageExtensions (MMCA.Common.Testing.E2E/Infrastructure/PageExtensions.cs:23) is where that knowledge is centralized, as C# extension(IPage) and extension(ILocator) blocks (PageExtensions.cs:62,335, see primer §4). The app uses InteractiveAuto with prerendering, so a page appears as static HTML before the runtime wires its event handlers, and the runtime flag alone is a false green. WaitForBlazorAsync (PageExtensions.cs:85) therefore waits in two phases: first for window.Blazor._internal (:39), which only says the WASM CLR or the SignalR circuit is ready, then for the data-mmca-interactive attribute that MmcaThemeProviders stamps on the document element from its first interactive render (:47-48), which is the honest gate, and finally flushes one animation frame (:60,96). GotoAndWaitForBlazorAsync (:103) pairs navigation with that wait and deliberately settles on Load rather than NetworkIdle, because the persistent SignalR WebSocket means network idle never arrives (:106-108); BlazorNavigateAsync (:118) routes client-side so a protected page is not re-prerendered without its token, polling window.location instead of WaitForURLAsync because a same-document navigation fires no load event (:132-138); and GotoProtectedAsync (:160) probes the same runtime predicate and loads a public page first when the runtime is not yet up (:167-188). FillAndVerifyAsync (:347) fills a field then auto-waits until the value sticks, retyping character by character if hydration wiped it (:353-365), and ClickAndVerifyAsync (:380) and ClickAndWaitForUrlAsync (:423) retry a click until its visible effect appears so a click that beats hydration is not silently swallowed (:389-410). List-page helpers built on the same primitives (SearchAndWaitForRowAsync :233, ConfirmDeleteAsync :266, WaitForGridToSettleAsync :293) keep grid interactions out of every page object. These helpers encode hard-won lessons about the prerender and hydration race and are shared by every page object.

Accessibility and performance are asserted here, not deferred to a separate audit. AssertNoAccessibilityViolationsAsync (PageExtensions.cs:307) runs an axe-core scan and throws AccessibilityViolationException (MMCA.Common.Testing.E2E/Infrastructure/AccessibilityViolationException.cs:7) with a compact per-node summary of every violation (PageExtensions.cs:320-331), so an inaccessible page fails the build, [Rubric §21, Accessibility]. The scan scope itself is shipped as AxeOptions (MMCA.Common.Testing.E2E/Infrastructure/AxeOptions.cs:9): Wcag21Aa (AxeOptions.cs:17) pins the documented target of WCAG 2.1 AA tags (AxeOptions.cs:19-23) and deliberately excludes axe's advisory best-practice rules, and Wcag21AaExceptMudPagerCombobox (AxeOptions.cs:35) is the one documented carve-out, disabling only aria-input-field-name for MudBlazor 9.6.0's unlabeled pager select (AxeOptions.cs:26-33,42-45). WebVitalsCollector (MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:20) installs PerformanceObserver-based Core Web Vitals capture (LCP, CLS, FCP, TTFB, INP) as an init script before first paint (InstallAsync, WebVitalsCollector.cs:40, script at :26-35), reads the accumulated values back as a WebVitalsSample (CollectAsync, WebVitalsCollector.cs:47,76), and writes a citable JSON artifact under WEB_VITALS_OUTPUT_DIR (WriteArtifactAsync, WebVitalsCollector.cs:63, artifact record WebVitalsArtifact at :90) for CI, with WebVitalsBudget (:103) as the shared assert mechanics defaulting to the Core Web Vitals "good" band, LCP 2500 ms / FCP 1800 ms / TTFB 800 ms / CLS 0.1 / INP 500 ms (WebVitalsCollector.cs:104-108), and skipping the INP assertion when no interaction cleared the 16 ms threshold (:148-151), [Rubric §23, Front-End Performance] (the source tags it rubric §12). LCP and CLS are Chromium-only, so on Firefox and WebKit those fields stay 0 and the observers fail silently rather than throwing (WebVitalsCollector.cs:14-16,22-25).

Five reusable identity page objects ship with the package: LoginPage (MMCA.Common.Testing.E2E/PageObjects/LoginPage.cs:6), RegisterPage (MMCA.Common.Testing.E2E/PageObjects/RegisterPage.cs:6), ProfilePage (MMCA.Common.Testing.E2E/PageObjects/ProfilePage.cs:6), ForgotPasswordPage (MMCA.Common.Testing.E2E/PageObjects/ForgotPasswordPage.cs:6), and ResetPasswordPage (MMCA.Common.Testing.E2E/PageObjects/ResetPasswordPage.cs:6). They wrap the framework's real auth surfaces with role- and label-based locators (LoginPage.cs:12-18) and route their own fills through the anti-race helper (LoginPage.cs:31-32, invoked at :25-26); downstream apps add their own family, for example the 45 page-object classes under MMCA.ADC.E2E.Tests/PageObjects/ covering events, sessions, speakers, rooms, questions, feedback, sponsors, and the QR check-in and points surfaces. Whole workflows ship too, not just page objects: eight abstract suites under MMCA.Common.Testing.E2E/Workflows/ (six identity flows, AuthorizationTestsBase, UserLoginTestsBase, UserRegistrationTestsBase, LogoutTestsBase, ProfileManagementTestsBase and PasswordResetTestsBase, plus UserPreferencesTestsBase (Workflows/Preferences/UserPreferencesTestsBase.cs:21) and PseudoLocalizationTestsBase (Workflows/Globalization/PseudoLocalizationTestsBase.cs:52)) are authored once and re-run per consumer. Their shape is the same supply-only-your-facts contract as the fitness bases: AuthorizationTestsBase (MMCA.Common.Testing.E2E/Workflows/Identity/AuthorizationTestsBase.cs:18) asks the subclass only for its route lists (ProtectedPaths :26, PublicPaths :29, optional AuthenticatedUserPath :35 and AdminPaths :44) and owns the assertions, including the non-empty guard that keeps the anonymous-redirect check from passing vacuously (:49-50). PasswordResetTestsBase (MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:17) shows where the tier draws its own boundary: it asserts the recovery flow is reachable from the login page (:24-41) and that an unknown address produces the identical anti-enumeration confirmation (:43-58), but deliberately does not consume a real reset token, because that token only reaches the user by email and so belongs to an app-side integration test (:10-16).

Component and E2E coverage of MMCA.Common's own UI needs a page to render, but the framework is not a runnable app. MMCA.Common.UI.Gallery is a deliberately backend-less Blazor host that renders the real MMCA.Common.UI auth pages (/login, /register), the shared notification pages, and a primitives showcase (/components), so a real-browser axe scan can run inside MMCA.Common's own CI (GalleryHost, MMCA.Common.UI.Gallery/GalleryHost.cs:16-20, host built by BuildApp at :28). It is kept outside MMCA.Common.slnx (together with MMCA.Common.UI.E2E.Tests) so the unit-test run stays fast; the CI ui-e2e job builds that out-of-slnx graph directly by csproj path and runs the axe plus render smoke against the gallery (MMCA.Common/.github/workflows/ci.yml:223-227,260-264,293-306). Because the E2E suite self-hosts it in-process, where the entry assembly is the test host and the environment is Production, the host also has to point the static-web-assets loader at the gallery's own runtime manifest and force it on, otherwise the auth pages render unstyled, never become interactive, and axe's contrast checks are meaningless (GalleryHost.cs:38-48).

The host runs without a backend by registering stubs before AddUIShared so its TryAdd* registrations defer to them (GalleryHost.cs:55-63, AddUIShared at :95): NoOpAuthUIService (MMCA.Common.UI.Gallery/Stubs/NoOpAuthUIService.cs:14), NullTokenStorageService (MMCA.Common.UI.Gallery/Stubs/NullTokenStorageService.cs:11), NullTokenRefresher (MMCA.Common.UI.Gallery/Stubs/NullTokenRefresher.cs:9), and GalleryAuthenticationStateProvider (MMCA.Common.UI.Gallery/Stubs/GalleryAuthenticationStateProvider.cs:16), plus canned notification services (StubNotificationInboxUIService (Stubs/StubNotificationInboxUIService.cs:11), StubPushNotificationUIService (Stubs/StubPushNotificationUIService.cs:11)) so the bell and the inbox render populated markup (GalleryHost.cs:78-80), the framework's NullNotificationScopeProvider for the unscoped send page (GalleryHost.cs:85), and one empty GalleryUIModule (Stubs/GalleryUIModule.cs:14, registered at GalleryHost.cs:90) so the shared Router discovers the gallery's own page alongside the real ones. Because the notification pages carry a real [Authorize] that MapRazorComponents surfaces as endpoint metadata, the gallery also needs a genuine authentication scheme: GalleryFakeAuthenticationHandler (MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:19, registered at GalleryHost.cs:69-73) authenticates only requests carrying the gallery_auth=1 cookie (GalleryFakeAuthenticationHandler.cs:26,30-39), so the guarded pages are scanned signed in while /login, /register, and /components are scanned in their deliberate anonymous state (GalleryFakeAuthenticationHandler.cs:8-18). The gallery is also where the i18n evidence is produced: it enables the qps-Ploc pseudo-locale unconditionally (GalleryHost.cs:99-108), which real hosts keep Development-only, because the pseudo-localization pass is a required CI gate for [Rubric §27, i18n] and this host is unpackaged test infrastructure that is never deployed.

Evaluating a nondeterministic dependency: the AI-scoring golden corpus

Every tier above assumes a deterministic system under test. ADC's session-scoring judge is not one: it asks a hosted model for a structured score, and the same prompt can return a different number on a different day. MMCA.ADC.Conference.Scoring.Evaluation.Tests is the tier built for that case, and it is the subject of ADR-111. The corpus is a folder of committed JSON files copied next to the test assembly. GoldenCase (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Scoring.Evaluation.Tests/GoldenCase.cs:11) is one case: a session proposal, the exact Anthropic Messages API response recorded for it, and the band its overall score has to land in (GoldenCase.cs:7-10). GoldenCorpus (GoldenCase.cs:100) loads every case-*.json from the copied Golden/ folder ordered by id so a run is deterministic (GoldenCase.cs:112-123), and throws when it finds none, so a csproj that stopped copying the corpus fails loudly instead of passing on zero cases (GoldenCase.cs:125-128); it also exposes the Golden/prompt-versions.json path that maps a prompt version to its recorded hash (GoldenCase.cs:108-110).

Three suites read that corpus, and they fail for three different reasons. GoldenReplayTests (GoldenReplayTests.cs:25) is the regression half: every case runs through the real AnthropicScoringService against a ReplayHandler (GoldenReplayTests.cs:161) that returns the response recorded for it, so no API key and no network are involved and it runs on every CI leg (GoldenReplayTests.cs:13-15). It pins both directions at once, what goes out (the delimited envelope and the untrusted-input brief are asserted on the wire, including for the cases with no speakers and with an injection attempt) and what comes back (a recorded response still parses, still succeeds, and still produces the same weighted overall inside the case's band), so a change to prompt assembly or to the weighting math fails here with the case that noticed it named (GoldenReplayTests.cs:17-22,47-60). LiveJudgeTests (LiveJudgeTests.cs:27) is the live half, scoring the same proposals through the real API to catch what a replay cannot: a model deprecation, a structured-output contract change, a prompt edit that reads fine but scores everything a point lower. It costs money and needs a key, so it is trait-gated Category=AiEval.Live and skips itself when ANTHROPIC_API_KEY is absent (LiveJudgeTests.cs:14-17,29). Its bands are deliberately generous, roughly plus or minus 1.5 around the recorded value and clamped to the 1.0 to 10.0 range, because a judge model is not deterministic even at the same prompt and a tight band would produce a flaky gate that gets ignored, which is worse than no gate: what the bands catch is a shift, not a wobble (LiveJudgeTests.cs:20-24).

PromptContractTests (PromptContractTests.cs:29) closes the loop that makes a persisted score meaningful. Scores are stored with the prompt version that produced them, which is worth something only if the version actually moves when the prompt does, so this suite renders the system brief plus the user message for one canonical proposal, hashes it, and compares against Golden/prompt-versions.json: a prompt edit without a version bump fails because the recorded hash for the current version no longer matches, and a version bump with no recorded hash fails too (PromptContractTests.cs:16-22). The canonical proposal is fixed in the test file rather than read from the corpus on purpose (PromptContractTests.cs:37-45), because editing a golden case must not be able to change what the contract hash covers (PromptContractTests.cs:25-26); it is chosen to exercise every branch of the prompt assembly, with a description and two speakers, one of them with no tagline and no bio (PromptContractTests.cs:34-35). Together the three suites are [Rubric §14, Testability] applied to a dependency that cannot be asserted exactly, and [Rubric §29, Resilience & Business Continuity] on the operational side: the offline gate is free and always on, the paid gate is opt-in, and neither one can pass vacuously.

Contract, pipeline, and benchmark bases

The last family pins guarantees that live in the composition of the stack rather than in any one type, and it is the subject of ADR-058: these suites ship in MMCA.Common.Testing as abstract behavioral bases and every one of them runs against a host that was actually booted. SecurityHeadersTestsBase (MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:16, [Rubric §11, Security] and [Rubric §26, Front-End Security]) probes an always-responding endpoint (ProbePath, default /alive, SecurityHeadersTestsBase.cs:19) and asserts the hardened header set: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, a Permissions-Policy containing geolocation=(), a Content-Security-Policy containing frame-ancestors 'none', and, in the Production environment, HSTS (SecurityHeadersTestsBase.cs:29-35). Its siblings OpenApiContractTestsBase<TFixture> (MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:22, with a MinimumPathCount floor at :37, a pinned CorePublicResources list at :50, and deliberately no committed snapshot file so a new controller can never leave a stale baseline behind, :14-16), ProblemDetailsContractTestsBase<TFixture> (MMCA.Common.Testing/Conformance/ProblemDetailsContractTestsBase.cs:22, asserting the RFC 9457 shape across both error-shaping paths, model validation at :30 and the framework's HandleFailure mapping at :42, through one shared AssertProblemDetailsShapeAsync at :67), and ServiceInfoVersioningContractTestsBase<TFixture> (MMCA.Common.Testing/Conformance/ServiceInfoVersioningContractTestsBase.cs:20, driving /ServiceInfo at both api-version: 1.0 and 2.0 and checking the deprecated/supported reporting headers at :38,:54) all subclass IntegrationTestBase<TFixture> and pin the corresponding API contracts, [Rubric §9, API & Contract Design]. The non-HTTP member of the family is GracefulShutdownTestsBase<TEntryPoint> (MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:25, [Rubric §29, Resilience & Business Continuity]): it boots a host through ProductionHostApplicationFactory, calls a real IHost.StopAsync under a bounded token defaulting to 20 seconds (:28,:55-56), and asserts ApplicationStopping then ApplicationStopped fired (:58-61). The failure it catches, a hosted service that refuses to drain, is invisible in production until it wedges a rolling deploy.

Four bases guard the composition of the pipelines themselves. DecoratorPipelineOrderTestsBase<TCommand, TCommandResult, TQuery, TQueryResult> (MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:38) is the opt-in fitness function for ADR-014: it builds a real ServiceCollection through the repo's own registration sequence (ConfigureServices, :46), resolves the decorated handlers, unwraps each decorator's private inner-handler field by reflection so it verifies the constructed object graph rather than the registration list (:105-125), and asserts the runtime nesting is exactly FeatureGate, Authorization, Logging, Caching, Validating, Timeout, Transactional, handler for commands (DecoratorPipelineOrderTestsBase.cs:49-58) and FeatureGate, Authorization, Logging, Caching, Validating, Timeout, handler for queries (:61-69), through the two [Fact]s at :71-77, with a final check that the innermost element is not itself a decorator (:96-97). Authorization and Timeout are the newest links in both chains, inserted by ADR-014's 2026-08-18 revision (Website/docs-src/adr/014-cqrs-decorator-pipeline.md:96-107); note that the revision's query line (:107) omits Validating, while the shipped registration and this base both include it (MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:141), so the code is the authority here. Because Scrutor's TryDecorate applies decorators in reverse registration order, an innocent-looking reorder of the AddApplicationDecorators() lines silently changes runtime behavior, and this base turns that into a test failure (see group 5). MiddlewarePipelineOrderTestsBase (MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:29) is its counterpart on the HTTP edge: it seeds MiddlewarePipelineBuilder.CreateDefault, applies the host's own Configure customization when it has one (:35), and asserts the eighteen-step order from the exception handler down to the controller endpoints (:38-58). Several adjacencies there are load-bearing (the pre-forwarded capture immediately before UseForwardedHeaders, authentication immediately before tenant resolution, authentication before the rate limiter per ADR-019, forwarded headers before the HTTPS redirect), and a reorder that breaks one of them fails at runtime looking like a configuration bug: an unreachable jwks_uri, a tenant that never resolves, a per-user rate cap that never engages (:13-18). Unlike the contract bases it needs no host at all, because the steps are pure data until they are applied, so it runs in the fast unit tier (:24-27). DependencyInjectionAssert (MMCA.Common.Testing/Support/DependencyInjectionAssert.cs:13) guards the other half of that composition: ReturnsSameCollection (:21) asserts a registration extension hands back the very IServiceCollection it was given (:29-31), because an extension that returns a new collection silently drops every registration chained after it and nothing else catches services that are simply absent (:6-11). HandlerTestBase<THandler> (MMCA.Common.Testing/Support/HandlerTestBase.cs:38) is the fast unit-tier counterpart for exercising a single handler without a host: it owns a Mock<IUnitOfWork> whose SaveChangesAsync is pre-configured to succeed (HandlerTestBase.cs:41-42,45), a NullLogger<THandler> (:48), and RegisterRepository<TEntity, TIdentifierType>() (:56) / RegisterReadRepository<TEntity, TIdentifierType>() (:72) helpers that wire a repository mock into the read and write accessors (the read-only variant exists for child entities, which expose no read-write repository).

A smaller final tier measures rather than asserts behavior. MMCA.Common.Benchmarks (BenchmarkDotNet) covers the per-request query pipeline, where the dynamic-LINQ predicate is re-parsed per call and the shaper reflects over DTO properties (QueryPipelineBenchmarks, MMCA.Common.Benchmarks/QueryPipelineBenchmarks.cs:9-17), and the specification hot path (SpecificationBenchmarks, MMCA.Common.Benchmarks/SpecificationBenchmarks.cs:8-14), both with [MemoryDiagnoser] allocation tracking. Its results are compared in CI by build/perfgate against the committed MMCA.Common/Tests/Performance/perf-baseline.json (MMCA.Common/.github/workflows/ci.yml:371), so moving a number has to be a deliberate, reviewed change, [Rubric §12, Performance & Scalability]. The same job family carries one more quiet gate worth knowing: the unit run is invoked with --minimum-expected-tests 2000 (MMCA.Common/.github/workflows/ci.yml:144), so a discovery regression that silently drops thousands of tests fails the build instead of reporting a green, empty run.

The takeaway for a new engineer: pick the tier that matches what you are proving (a fast unit test for domain logic, bUnit for a component, an integration fixture for a full request path, a cross-service fixture for a real broker round-trip, an E2E page object for a browser flow, a *TestsBase subclass for an architectural invariant, a contract base for a runtime guarantee of the composed host, a golden case for a change in what the scoring model returns, a benchmark for an allocation budget), and, for every tier but the ADC-local model-evaluation suite, the reusable base you need is already in one of the four MMCA.Common.Testing.* packages. Adoption is opt-in per host in both governance tiers, which is the standing caveat in ADR-015 and ADR-058 alike: the framework ships the gate, a host gets it only once someone writes the subclass (Website/docs-src/adr/015-architecture-fitness-functions.md:68-70, Website/docs-src/adr/058-runtime-conformance-suites-as-a-package.md:173-175). Every remaining concrete test class is cataloged by project in the companion per-project test rollup for this chapter.

AbstractAnonymousFixtureControllerBase, AnonymousFixtureController, TypeLevelAnonymousFixtureController

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · (see per-type table) · Level 0 · class

Three throwaway MVC controllers nested inside AnonymousEndpointTestsBaseTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AnonymousEndpointTestsBaseTests.cs). Between them they cover every placement of [AllowAnonymous] the scan has to recognize: on an action of a concrete controller, on an action of an abstract base, and on the controller type itself. They are the input side of the anonymous-endpoint allow-list gate; the subclasses that consume them supply the expectations.

  • Depends on - Microsoft.AspNetCore.Mvc.ControllerBase and Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute (AnonymousEndpointTestsBaseTests.cs:1-:2). No state, no behavior: every action is => Ok().
  • Concept introduced - an allow-list is only as good as the identifier shapes it can be written in. AnonymousEndpointTestsBase turns each discovered [AllowAnonymous] into a string, and there are exactly two shapes: a type-level attribute becomes the type's FullName, and a method-level attribute becomes {declaring type FullName}.{method name} (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:133-:152). A repo's allow-list is hand-written against those strings, so if the emitter and the hand-written convention ever disagree the gate silently reports phantom offenders or, worse, accepts a stale entry. These three fixtures exist so both shapes are produced by a real scan and matched by a real allow-list in the framework's own CI. Two scoping decisions in the base are also on display here: ControllerBase, RouteAttribute and AllowAnonymousAttribute are all matched by full name through reflection (:32-:34) so the rule package carries no ASP.NET reference, and abstract controllers are deliberately included in the scan (IsController walks BaseType, :101-:112) because a framework base action is where the attribute is declared. [Rubric §11 - Security] assesses whether the authorization posture of the HTTP surface is deliberate; an ungated endpoint that nobody reviewed is the single cheapest way to lose an app. [Rubric §26 - Front-End Security] extends the same scan to routable Blazor components (IsRoutableComponent, :117-:118). [Rubric §14 - Testability] covers the fixtures themselves.
  • Walkthrough - the scan enumerates LoadableTypes of each target assembly, keeps controllers and routable components, and projects each survivor through AnonymousEndpointsOf, distinct and ordinal-ordered (AnonymousEndpointTestsBase.cs:125-:131). Methods are read with BindingFlags.DeclaredOnly and inherit: false (:142,:146), which is what decides where an inherited action gets reported (see InheritingFixtureController).
Type File:Line The identifier shape it produces
AnonymousFixtureController MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:74 The ordinary case: a sealed ControllerBase whose PeekAsync carries [HttpGet] and [AllowAnonymous] (:78-:80). Emits the method-level identifier ...AnonymousFixtureController.PeekAsync, and is the name the drifted subclass's failure message must contain (:22).
AbstractAnonymousFixtureControllerBase MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:84 The declaration site: an abstract controller whose virtual InheritedAnonymousAsync carries [HttpGet("inherited")] and [AllowAnonymous] (:88-:90). Emits one identifier at the base, mirroring how the framework's own AuthControllerBase actions are declared once for every consumer that derives from them.
TypeLevelAnonymousFixtureController MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:98 The other identifier shape: [AllowAnonymous] on the type (:97) with a body-less declaration and no actions at all. Emits the bare FullName, with no method suffix.
  • Why they're built this way - a gate whose fixtures only ever exercise one attribute placement would pass while being blind to the other, and the failure would land in a consumer repo rather than here. Nesting the fixtures inside the test class keeps them out of the assembly's public surface and, critically, out of the framework's real AnonymousEndpointTests scan, which targets the API and UI assemblies rather than this one. See ADR-015.
  • Where they're used - scanned by the four nested subclasses DriftedTests, StaleAllowListTests, EmptyScanTests and ConformantTests, and named in the assertions of AnonymousEndpointTestsBaseTests.

AbstractFitnessControllerBase, IdempotentFitnessController, NonIdempotentFitnessController, UndeclaredFitnessController

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · (see per-type table) · Level 0 · class

Four throwaway MVC controllers nested inside IdempotencyFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/IdempotencyFitnessTests.cs). Each is a ControllerBase subclass with exactly one [HttpPost] action, and the only thing that varies between them is which idempotency attribute the action carries. Together they form the truth table for the PostActionsDeclareIdempotencyIntent fitness function: one offender, one covered directly, one covered by inheritance, one opted out with a reason, and one abstract declaration site that must be skipped entirely.

  • Depends on - Microsoft.AspNetCore.Mvc.ControllerBase (IdempotencyFitnessTests.cs:1), plus IdempotentAttribute and NonIdempotentAttribute from MMCA.Common.API.Idempotency (:2). Nothing else: these types have no state and no behavior worth exercising.
  • Concept introduced - declared idempotency intent, and the truth table that proves a gate reads it. A POST that is retried by a client, a proxy, or a mobile app on a flaky connection either replays its original response or writes twice. IdempotentAttribute opts an action into the replay path served by IdempotencyFilter; NonIdempotentAttribute records in code why replaying would be wrong (issuing a token, revoking a credential, exchanging a single-use code). What the framework will not accept is silence, so ArchitectureRules.PostActionsDeclareIdempotencyIntent (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:44) fails the build on any POST carrying neither attribute. These four fixtures exist because a gate like that has four distinct behaviors to prove, not one. [Rubric §9 - API & Contract Design] assesses whether the HTTP surface states its own semantics; a declared retry posture is part of the contract, not an implementation detail. [Rubric §29 - Resilience & Business Continuity] is the reason the rule exists at all: retries are the default failure response of every modern client, so a POST with undeclared replay semantics is a duplicate-write incident waiting for a network blip. [Rubric §14 - Testability] covers the fixtures themselves, which turn each branch of the rule into an observable case.
  • Walkthrough - the rule reads attributes by simple type name ("IdempotentAttribute", "NonIdempotentAttribute", "HttpPostAttribute", ArchitectureRules.Idempotency.cs:6,:9,:12) so the rule package keeps no ASP.NET reference, and it reads them with inherit: true (:80-:81), which is what makes the inherited case work. Two filters decide what is even looked at: .SelectMany(a => a.ConcreteClasses) (:49), whose helper keeps only non-abstract classes (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/RuleHelpers.cs:36-:37), and .Where(IsController) (:51).
Type File:Line The case it models
IdempotentFitnessController MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:61 The primary covered shape: CreateThingAsync carries [HttpPost] and [Idempotent] on the action itself (:65-:67). The rule must not report it.
AbstractFitnessControllerBase MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:71 The declaration site: an abstract controller whose virtual CreateInheritedAsync carries [HttpPost("inherited")] and [Idempotent] (:75-:77). Because it is abstract it never reaches ConcreteClasses, so it is skipped rather than judged; its concrete subclasses are what actually route.
NonIdempotentFitnessController MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:84 The justified opt-out: IssueTokenAsync carries [NonIdempotent("Issues a token; a replayed response would hand back credentials minted for an earlier call.")] (:88-:90). A declaration, not an omission, so the rule stays quiet.
UndeclaredFitnessController MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:94 The offender: a bare [HttpPost] CreateThingAsync with neither attribute (:98-:99). This is the one name the rule's exception message must contain.
  • Why they're built this way - a fitness function that only ever sees compliant code proves nothing, and one that flags everything is worse than none. Four controllers whose only difference is the attribute set turn the rule into something with a pass and a fail on either side of each branch (ADR-015). Keeping them nested inside the test class keeps them out of the assembly's public surface and out of any other rule's reach.
  • Where they're used - reflected over by IdempotencyFitnessTests through IdempotencyTestMap, which registers this test assembly as the map's single Api layer.
  • Caveats / not-in-source - these fixtures deliberately derive from raw ControllerBase rather than ApiControllerBase, which the framework's separate controller-inheritance rule would flag. They escape it only because that rule runs over CommonArchitectureMap, which does not include this test assembly.

ArchiveTicketCommand, CreateTicketCommand, PurgeTicketsCommand, RebuildTicketIndexCommand, ReopenTicketRequest, UpdateTicketRequest

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CommandValidatorFixtures · (see per-type table) · Level 0 · record

Six internal sealed record payload types in one file (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs), each a positional record with at most one property. They are the input side of the command-validation coverage truth table: four commands that a handler will pick up, and two request records that a bridged command embeds.

  • Depends on - nothing beyond the BCL, except that the file as a whole references ICommandHandler<in TCommand, TResult> and ICommandWithRequest<out TRequest> from MMCA.Common.Application.UseCases (CommandValidatorFixtures.cs:2) and FluentValidation (:1). These six records implement neither: they are pure payload.
  • Concept introduced - what makes a command "data-carrying", and why that decides whether it is judged at all. The validation-coverage rule does not ask "does every command have a validator". It asks the narrower question "does every command that could carry bad input have something checking it", and the test for that is mechanical: at least one public instance property with a public setter, init included (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:135-:138). A positional record is exactly that shape, which is why the usual command is inspected and why a payload-free marker command is skipped as having nothing to validate (rule remarks, :19-:21). The rule then keeps only commands whose type appears as the TCommand of some concrete ICommandHandler in scope, and only closed ones: an open generic handler base contributes a type parameter, not a command a repo ships (:96-:102). [Rubric §6 - CQRS & Event-Driven] assesses whether the command surface is a governed contract rather than an ad-hoc bag of DTOs. [Rubric §24 - Forms/Validation/UX Safety] is the property being defended at its far end: the Validating decorator runs before the transaction opens, so a command with no validator carries whatever the caller sent straight into the handler and the pipeline stage exists with nothing to run (rule doc, :9-:13). [Rubric §14 - Testability] covers the fixtures.
  • Walkthrough - the file's header doc (:7-:12) enumerates the shapes deliberately: a command covered by its own validator, one covered only through the request bridge, one covered by neither, one carrying no payload at all, and a bridge command whose request has no validator.
Type File:Line The case it models
CreateTicketCommand MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:13 record CreateTicketCommand(string Title). Direct coverage: CreateTicketCommandValidator validates it explicitly, so it must be absent from the report.
UpdateTicketRequest MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:28 record UpdateTicketRequest(string Title), the request a bridged command embeds. Never a command itself (no handler takes it), so the rule never judges it; it exists to be the validated half of the bridge.
ReopenTicketRequest MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:45 record ReopenTicketRequest(string Reason), deliberately with no validator (doc, :44). It is what makes the half-bridge case possible.
ArchiveTicketCommand MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:60 record ArchiveTicketCommand(Guid TicketId). Carries data, has a handler, has no validation of any kind (doc, :59). The primary offender, and the name the failure message must contain.
PurgeTicketsCommand MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:69 record PurgeTicketsCommand(DateTime OlderThan). A second uncovered command, kept separate so one fact can allowlist it and prove an exemption silences exactly the command it names while the other offender still fails (doc, :68).
RebuildTicketIndexCommand MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:78 record RebuildTicketIndexCommand(), a parameterless marker with no settable property (doc, :77). Skipped by HasSettablePublicProperty, so it is neither covered nor reported, and it is the one command excluded from the inventory count.
  • Why they're built this way - two uncovered commands rather than one is the load-bearing choice: an allowlist test that had only a single offender could not distinguish "the exemption worked" from "the rule stopped firing". Keeping every record to one property means a change in the rule's classifier is diagnosed by which fixture moved rather than by reading a wall of assertion output. See ADR-015.
  • Where they're used - handled by the six fixture handlers (ArchiveTicketHandler and family), scanned through FixtureModuleMap, and named by nameof in the assertions of CommandValidatorCoverageFitnessTests.

ArgumentGuardFixture, IndirectThrowFixture, InvalidOperationThrowingFixture, NonThrowingFixture, RethrowingFixture, TicketDomainException

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainThrowFixtures · (see per-type table) · Level 0 · class

Six types in one file (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs): five static classes holding compiled throw sites and one custom exception for them to throw. They are the truth table for the domain-throw rule, which reads IL, so the only honest way to test it is to compile the throws it must and must not flag into this assembly and point a map at it (file doc, :3-:8).

  • Depends on - the BCL only: ArgumentException, ArgumentNullException, ArgumentOutOfRangeException, InvalidOperationException, Exception, Action, ICollection<string>.
  • Concept introduced - the Result pattern's build-time boundary, and the three exceptions it still allows. ADR-013 says the domain reports failure by returning Result, never by throwing: a thrown business failure skips Result.Combine invariant composition, turns a 4xx outcome into a 500, and pays an exception unwind on a path that is not exceptional (rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:23-:29). The rule is not "never throw", though. Argument guards report a caller bug rather than a business outcome, and a caller cannot recover from passing null, so there is nothing for a Result to carry; exactly three exception types are permitted (:16-:21). Everything else in a Domain assembly is a violation. The mechanism is deliberately narrow: for each throw opcode the rule looks at the immediately preceding instruction, skipping the nops a Debug build interleaves, and reads the exception type off a newobj (:143-:154). If the preceding instruction is anything else, the thrown value came from somewhere the instruction stream cannot name, and the site is reported as UNVERIFIABLE rather than guessed at (:127-:131). [Rubric §4 - DDD] assesses whether the domain expresses outcomes in its own vocabulary; an exception is control flow borrowed from the host. [Rubric §15 - Best Practices & Code Quality] and [Rubric §14 - Testability] cover the rest.
  • Walkthrough - a bare throw; inside a catch compiles to the distinct rethrow opcode, not throw, so it never even enters the scan (fixture doc, :72-:75). The rule also skips compiler-synthesized bodies: the skeleton members of a C# extension(T) block, and the explicit interface implementations on compiler-generated types such as the read-only list wrapper emitted for a collection expression (:156-:177).
Type File:Line What it models
ArgumentGuardFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:9 All three permitted guards in one type: ArgumentNullException via a null-coalescing throw (:11-:12), ArgumentOutOfRangeException from a range check (:14-:20), and ArgumentException after an ArgumentNullException.ThrowIfNull (:22-:30). The whole type name must be absent from the report.
InvalidOperationThrowingFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:37 The canonical offender: a business outcome signalled by throwing, throw new InvalidOperationException("The ticket is already closed") (:43). Its doc names the fix, a Result.Failure (:33-:36).
TicketDomainException MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:49 The only public type in the file: a three-constructor Exception subclass (:51-:63). It carries no throw of its own; it exists so the rule can be shown to judge by the constructed type rather than by a name allowlist, since a custom exception is the same defect wearing a domain name (doc, :48).
RethrowingFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:76 The negative control for opcode discrimination: a try/catch that logs and then rethrows bare (:83-:91). Preserving a caught exception must stay free, and the rethrow opcode is what makes that possible without an allowlist entry.
IndirectThrowFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:99 The blind spot, stated honestly: Fail(Exception prepared) => throw prepared (:101). The value was constructed elsewhere, so the preceding instruction is not a newobj and the rule reports UNVERIFIABLE instead of passing or failing it.
NonThrowingFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:105 The shape the rule exists to protect: CanClose(bool alreadyClosed) => !alreadyClosed (:107). Returns instead of throwing.
  • Why they're built this way - the rule has five outcomes (allowed guard, business exception, custom exception, rethrow, unverifiable) plus a clean case, and each needs a subject that trips exactly one of them. Every fixture except TicketDomainException is internal static, so nothing here widens the assembly's public surface, and putting them in their own namespace lets one allowlist entry silence the whole set for the escape-hatch facts. See ADR-015.
  • Where they're used - scanned by DomainThrowFitnessTests through its FixtureAssemblyMap, which registers this test assembly as the map's single Domain layer.

CompliantFixtureService, MissingTokenFixtureService, MisplacedTokenFixtureService, MisnamedTokenFixtureService, ExemptableFixtureService, ExternalContractFixtureService

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CancellationFixtures · (see per-type table) · Level 0 · class

Six tiny services in one file (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs), each a hand-shaped input for one branch of the AsyncMethodsDeclareTrailingCancellationToken fitness function. None of them does any work; the method signatures are the fixture.

  • Depends on - the BCL only: Task/ValueTask, CancellationToken, IAsyncEnumerator<T>, and one [SuppressMessage] from System.Diagnostics.CodeAnalysis (CancellationTokenFixtures.cs:1).
  • Concept introduced - the trailing-token convention, and why it is enforced mechanically. Work that cannot be cancelled keeps running against the database after its caller is gone. The framework's rule is not "accept a token somewhere" but the far stricter "every public awaitable method on an Application or Infrastructure type ends with a parameter of type CancellationToken named exactly cancellationToken" (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CancellationTokens.cs:35). The uniformity is the point: only a positionally and nominally fixed parameter lets the decorator pipeline and the repositories forward a linked token mechanically, without a per-method decision. The rule scans only the Application and Infrastructure layers (:45-:47), only publicly visible non-delegate types (:92-:94), and only public, non-special-name, non-Dispose methods whose return type is Task, Task<T>, ValueTask, or ValueTask<T> (:97-:103). TokenProblem (:115) then classifies each survivor into one of three messages: "no CancellationToken parameter" (:134), "the CancellationToken must be the LAST parameter" (:131), or "the trailing CancellationToken must be named 'cancellationToken'" (:130). [Rubric §12 - Performance & Scalability] assesses whether the system sheds abandoned work instead of paying for it; an uncancellable query holds a connection for the full duration of a request nobody is waiting for. [Rubric §6 - CQRS & Event-Driven Design] applies because cancellation is plumbing that has to be uniform to be automatable, and [Rubric §14 - Testability] because these six fixtures are what make the classifier provable rather than assumed.
  • Walkthrough - two escape hatches also need fixtures, and they work differently. The explicit one is the exemptMethods parameter (ArchitectureRules.CancellationTokens.cs:37), a caller-supplied list matched as "TypeName.MethodName" (:78); it exists for shipped public APIs where adding a parameter would be a breaking change. The automatic one is ExternallyFixedMethods (:141), which drops any method that overrides a base declared outside the map's assemblies or implicitly implements an interface declared outside them: a signature this repo does not own is not this repo's to fix.
Type File:Line What it models
CompliantFixtureService MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:6 The clean case, and two out-of-scope shapes in the same type: GetAsync(string, CancellationToken) and DoAsync(CancellationToken) both comply (:9-:17), Count(string) is not awaitable (:20), and HiddenAsync() is internal (:23), so neither is scanned. The whole type's name must be absent from the report.
MissingTokenFixtureService MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:27 No token at all, twice: RunAsync(string) (:30) and PingAsync() (:33). The parameterless one is deliberate: it pins the ruling that a method with no parameters is not excused, because the token would simply be its only one.
MisplacedTokenFixtureService MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:37 The token leads instead of trailing: RunAsync(CancellationToken, string) (:44). Writing it required suppressing CA1068, and the justification says exactly why (:40-:43), which is the honest way to keep an analyzer-hostile fixture in a warnings-as-errors repo.
MisnamedTokenFixtureService MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:49 The token trails but is named token (:52). Proves the rule checks the name, not just the position and type.
ExemptableFixtureService MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:57 A plain offender, LegacyAsync(string) (:60), used twice by the test: once with no exemptions (reported) and once listed in exemptMethods (silent). It is what makes the explicit escape hatch provable.
ExternalContractFixtureService MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:67 An IAsyncEnumerator<int> implementation whose MoveNextAsync() (:73) is awaitable, public, and token-less, yet is declared by the BCL. Proves the automatic exemption: an implicit implementation of an interface outside the map is never flagged.
  • Why they're built this way - the six shapes are the rule's entire decision surface. Putting them in their own CancellationFixtures namespace keeps them out of the way of the other fitness maps in this assembly, and keeping every one of them trivial means a future change to the rule's classifier is diagnosed by which fixture moved, not by a wall of assertion output. See ADR-015.
  • Where they're used - driven entirely by CancellationTokenFitnessTests through CancellationTestMap.

IBadgeGranter

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainEventSaveFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:64 · Level 0 · interface

  • What it is - a one-method fixture abstraction with a single implementation, existing only so that one of the domain-event handler fixtures reaches its save through an interface call rather than a direct one.
  • Depends on - nothing. internal interface IBadgeGranter with Task GrantAsync(CancellationToken cancellationToken) (DomainEventSaveFixtures.cs:64-:67).
  • Concept introduced - why an IL walk has to resolve interface dispatch, and why a fixture must force it to. Domain event handlers run inside the save that raised the event: dispatch happens after SaveChangesAsync, or after commit inside a transactional command. A handler that saves again therefore opens a second write in the middle of the first one, re-entering the change tracker, possibly raising a fresh event cascade, and persisting work the outer transaction may still roll back (rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:23-:29). Catching that means following what a handler calls, and in IL a call through an abstraction records only the interface method: callvirt IBadgeGranter::GrantAsync, with no trace of the implementation that will actually run. A walker that stopped there would report nothing. The rule therefore expands each callee to the implementations in scope through CallGraphIndex, and this interface is the fixture that makes that expansion observable. The doc says it in one line: the collaborator is an abstraction, so IL records only the interface call (:63). [Rubric §6 - CQRS & Event-Driven] is the property; [Rubric §14 - Testability] is why the fixture exists. Full mechanism at DomainEventHandlerSaveFitnessTests.
  • Walkthrough - one member, GrantAsync(CancellationToken) (:66). It takes a token but returns nothing meaningful; the signature exists only to be callable.
  • Where it's used - implemented by BadgeGranter (:70) and injected into InterfaceDispatchSavingHandler (:83).

IFakeExportService

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:25 · Level 0 · interface

  • What it is - a marker-only cross-module contract that exists purely as test data: it stands in for the kind of service one module exports and another module resolves, so the module-conformance base can be proven to reach a real DI container.
  • Depends on - nothing. The whole declaration is public interface IFakeExportService; (ModuleConformanceTestsBaseTests.cs:25), a body-less interface using the semicolon form, with a one-line doc above it (:24).
  • Concept introduced - the cross-module export contract, in miniature. Under the module system (IModule, ModuleLoader), a module that is switched off in ModulesSettings still has to leave behind stub registrations, otherwise every other module that resolves its exported interface fails to construct. IFakeExportService is the smallest possible stand-in for such an exported interface. [Rubric §14 - Testability] assesses whether a rule can be proven with focused inputs; a marker interface is the least amount of surface that still lets the fitness base observe a real ServiceDescriptor.
  • Walkthrough - no members. Its only role is to be the ServiceType that FakeDependentModule registers a stub against (:44) and that FakeDependentModuleConformanceTests looks up in the built collection (:74).
  • Why it's built this way - a real cross-module contract would drag a module's whole application surface into the framework's architecture-test assembly. A local marker keeps the fixture self-contained, which is the same discipline the rule library itself follows (it matches framework types by full name rather than referencing them).
  • Where it's used - implemented by DisabledFakeExportService and registered by FakeDependentModule.

LeftModelBase

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CycleFixtures.Left · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Left/LeftFixtures.cs:13 · Level 0 · class

  • What it is - one arm of a deliberately-built namespace cycle: an abstract base living in the Left fixture namespace, which a type in the Right namespace derives from. That inheritance edge is what closes the loop back into Left.
  • Depends on - nothing. It is public abstract class LeftModelBase with a single int Id { get; set; } (LeftFixtures.cs:13-:17).
  • Concept introduced - namespace cycles, and how a structural rule sees them. ArchitectureRules.NamespacesHaveNoDependencyCycles (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:45) groups a layer's types by the namespace segment directly beneath the layer's RootNamespace, records a directed edge A -> B whenever a type in A structurally references a type in B in the same assembly, and reports every strongly connected component of that graph (:12-:16). "Structurally" is precise here: base types, implemented interfaces, field and property types, method return and parameter types (public and non-public), and attribute types, with generics and array elements expanded (:25-:27). What it deliberately cannot see is a reference that exists only inside a method body, because the package carries no IL or Roslyn dependency (:30-:36), so a clean report is a statement about the type surface, not a claim of zero coupling. A base class is therefore the cheapest possible cycle edge to build: inheritance is always visible to reflection. [Rubric §34 - Architecture Governance & Documentation] assesses whether the codebase stays reasonable to reason about, and a two-namespace cycle is the classic early warning that two folders can no longer be understood, tested, or moved independently. [Rubric §7 - Microservices Readiness] is the sharper consequence: neither namespace can be extracted without the other.
  • Walkthrough - one auto-property, int Id (:16), present only so the type is not empty. Being abstract is irrelevant to the cycle rule (which walks the type surface, not ConcreteClasses); what matters is that RightModel names it as a base.
  • Why it's built this way - the fixture pair models the smallest honest cycle: one edge through a property type and one through inheritance, so the rule is proven against both edge kinds it claims to detect rather than just the easy one.
  • Where it's used - the base of RightModel; the pair drives NamespaceCycleFitnessTests through CycleTestMap.

CreateTicketCommandValidator, UpdateTicketRequestValidator

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CommandValidatorFixtures · (see per-type table) · Level 1 · class

Two one-line FluentValidation validators, identical in shape and different only in what they validate. Between them they supply both forms of coverage the command-validation rule accepts: a validator for the command itself, and a validator for the request a bridged command embeds.

  • Depends on - FluentValidation.AbstractValidator<T> (CommandValidatorFixtures.cs:1) and the record each one validates.
  • Concept introduced - direct coverage versus bridge coverage, and why the bridge needs both halves. A command is covered when a concrete IValidator<TCommand> exists for it, or when the command implements ICommandWithRequest<TRequest> and a concrete IValidator<TRequest> exists (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:112-:115). The second condition insists on both halves for a reason the rule doc spells out (:23-:29): CommandRequestValidator<TCommand, TRequest> is auto-registered for every bridged command, so a naive check for "is a validator registered" would always say yes, while the bridge adds no rule at all when no request validator resolves. Half a bridge is a validator that validates nothing. The rule also reads commands and validators from the same place the framework's AddValidatorsFromAssembly scan looks, the per-module Application assemblies (:32-:36), because a validator living anywhere else would not resolve at run time either. [Rubric §24 - Forms/Validation/UX Safety] and [Rubric §6 - CQRS & Event-Driven] are the properties; [Rubric §14 - Testability] covers the fixtures.
  • Walkthrough - both are internal sealed, both declare an internal constructor with a single expression-bodied rule, and both use NotEmpty() on the one string property. Nothing about the rule content matters to the fitness function; the existence of a closed IValidator<T> implementation is the whole signal.
Type File:Line What it covers
CreateTicketCommandValidator MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:16 AbstractValidator<CreateTicketCommand> with RuleFor(c => c.Title).NotEmpty() (:18). Direct coverage: the validator names the command itself.
UpdateTicketRequestValidator MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:33 AbstractValidator<UpdateTicketRequest> with RuleFor(r => r.Title).NotEmpty() (:35). Bridge coverage: it names the request, and UpdateTicketCommand is covered only through it.
  • Why they're built this way - having exactly one validator on each side of the bridge is what lets a single rule execution produce a positive result for both coverage forms while ReopenTicketCommand, whose request deliberately has none, still fails. Adding a second rule to either validator would prove nothing extra.
  • Caveats / not-in-source - the rule's own limits section records two behaviors these fixtures do not exercise: a validator declared for a base command type does not count for its derived commands (matching FluentValidation's closed-type resolution), and abstract validators are ignored because nothing registers them (ArchitectureRules.CommandValidators.cs:37-:41).
  • Where they're used - discovered by ValidatedTypes (ArchitectureRules.CommandValidators.cs:105-:109) during every fact of CommandValidatorCoverageFitnessTests.

DuplicateTicketErrors, DynamicErrors, SharedCodeErrors, TicketErrors, TwoBranchTicketErrors, UnprefixedErrors

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ErrorCodeFixtures · (see per-type table) · Level 2 · class

Six internal static classes in one file (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs), each holding one or two expression-bodied factory methods that construct an Error. Together they compile the error catalog the two catalog rules have to judge: well-formed codes, a cross-type collision, an unprefixed code, one code reused across two branches of a single type, a shared framework static, and a code built at run time.

  • Depends on - Error and its static factories NotFoundError, Conflict and Validation (ErrorCodeFixtures.cs:1). Nothing else.
  • Concept introduced - the error code as the module's public vocabulary, read out of IL. A client switches on Order.NotFound and a support ticket quotes it, so two modules both shipping Item.Invalid make that vocabulary ambiguous in a way that only surfaces in production (rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.ErrorCatalog.cs:31-:37). Two rules police it. ErrorCodesAreUnique (:62) fails when the same literal code is constructed by more than one type, and the choice of "type" as the unit is deliberate: one error with two exits in the same class is not a collision, which keeps a normal two-branch handler out of the report (:45-:50). ErrorCodesUseAnAllowedPrefix (:102) fails on any literal code whose prefix a caller-supplied delegate rejects, so the convention stays the consumer's rather than the framework's (:92-:98). Both read the code out of the compiled IL: the scan looks for calls to any of nine Error factory names plus .ctor (:17-:29) and reads the first argument, and both deliberately skip the record copy constructor through a first-parameter-is-string check so error with { Source = ... } is not mistaken for a new code (:11-:15). Codes that are not literals cannot be judged statically, and the rules refuse to guess: they are listed as UNVERIFIABLE in the failure message so the reader knows the catalog has a blind spot (:51-:55). [Rubric §9 - API & Contract Design] assesses whether the published failure vocabulary is governed; [Rubric §15 - Best Practices & Code Quality] covers traceability, since a prefixed code alone says where it came from; [Rubric §14 - Testability] covers the fixtures.
  • Walkthrough - scope is the per-module Domain and Application assemblies only (:38-:44), which is why the self-test map registers this assembly as a module Application layer rather than a framework one.
Type File:Line What it models
TicketErrors MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:11 The clean catalog: Tickets.NotFound via Error.NotFoundError (:13) and Tickets.AlreadyClosed via Error.Conflict (:15). Correctly prefixed, so it must pass the prefix rule, while being one half of the collision below.
DuplicateTicketErrors MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:19 Ships Tickets.NotFound a second time from a different type, with a different message (:21). The collision the uniqueness rule exists to catch, and the failure message must name both owning types.
TwoBranchTicketErrors MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:28 The near-miss: one method with a ternary that constructs Tickets.Invalid on both branches with different reasons (:30-:33). Two construction sites, one owning type, so it is not a collision and the rule must stay silent.
UnprefixedErrors MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:37 Error.Validation("SomethingBroke", ...) (:39), a code with no module prefix at all. The prefix rule must report it and name the owning type.
DynamicErrors MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:46 The blind spot: Error.Validation("Tickets." + entity, ...) (:48). The code argument is a concatenation rather than a literal, so the scan reports the site as UNVERIFIABLE rather than passing or failing it.
SharedCodeErrors MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:52 Error.NotFoundError("Error.NotFound", ...) (:54), reusing one of the generic statics on Error itself. The allowed-shared-codes list exempts it from both rules, which is what those generics exist for.
  • Why they're built this way - the pairs are what make each rule discriminating rather than merely loud. TicketErrors and DuplicateTicketErrors are the collision; TwoBranchTicketErrors is the shape a naive "count the construction sites" implementation would wrongly flag; SharedCodeErrors proves the exemption is honored on the prefix rule as well as the uniqueness rule. Keeping every fixture internal static with expression-bodied members means the IL is minimal and the reported owner name is unambiguous. See ADR-015.
  • Where they're used - compiled into this assembly and scanned by ErrorCatalogFitnessTests through its FixtureModuleMap, which anchors the module Application layer on typeof(TicketErrors).Assembly (ErrorCatalogFitnessTests.cs:112).

ArchiveTicketHandler, CreateTicketHandler, PurgeTicketsHandler, RebuildTicketIndexHandler, ReopenTicketHandler, UpdateTicketHandler

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CommandValidatorFixtures · (see per-type table) · Level 3 · class

Six byte-identical handlers, one per fixture command, each a single expression-bodied HandleAsync returning Task.FromResult(Result.Success()). They do nothing, and that is the design: their only job is to make their command handled, which is the condition that puts a command into the coverage rule's scope at all.

  • Depends on - ICommandHandler<in TCommand, TResult> closed over the fixture command and Result (CommandValidatorFixtures.cs:2-:3).
  • Concept introduced - "has a handler" is the scoping predicate, not "is named Command". The rule never matches on a naming convention. It projects every concrete class in the module Application assemblies through the closed generic arguments of the command-handler interface and takes the first type argument (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:96-:102). A record that nobody handles is not a command the repo dispatches, so it is not a command the pipeline can fail to validate, and inventing a naming rule instead would both miss unconventionally-named commands and flag DTOs that merely end in Command. These six handlers exist so the fixture commands satisfy that predicate honestly rather than by declaration.
  • Walkthrough - every one has the same body: public Task<Result> HandleAsync({Command} command, CancellationToken cancellationToken = default) => Task.FromResult(Result.Success());. Note the trailing token with its conventional name, which keeps these fixtures compliant with the framework's own cancellation rule even though nothing here awaits.
Type File:Line Its command
CreateTicketHandler MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:21 CreateTicketCommand, the directly-validated case.
UpdateTicketHandler MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:38 UpdateTicketCommand, covered through the request bridge.
ReopenTicketHandler MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:53 ReopenTicketCommand, the half-bridge offender.
ArchiveTicketHandler MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:62 ArchiveTicketCommand, the plain offender.
PurgeTicketsHandler MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:71 PurgeTicketsCommand, the offender the allowlist fact exempts.
RebuildTicketIndexHandler MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:80 RebuildTicketIndexCommand, the payload-free marker. Handled like the rest, and still skipped, because the skip is decided by the payload test rather than by the handler.
  • Why they're built this way - one handler per command, with no shared abstract base, keeps the discovery pass honest: an abstract or open-generic handler would contribute a type parameter rather than a command and would silently shrink the inventory. The uniform empty body means no handler can accidentally become the reason a fact passes.
  • Where they're used - counted by HandledCommandCount (ArchitectureRules.CommandValidators.cs:80), which CommandInventory_CountsTheDataCarryingCommands pins at exactly 5, the five data-carrying commands with the payload-free marker excluded (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:100-:108).

FixtureCompliantV1, FixtureCompliantV2, FixtureCompliantV3, FixtureContestedV1, FixtureContestedV2, FixtureContestedV3, FixtureBackwardsV1, FixtureBackwardsV2

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.UpcasterFixtures.IntegrationEvents · (see per-type table) · Level 3 · record

Eight throwaway integration-event contracts in one file (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs), forming three groups: a compliant three-rung version ladder, a pair of rival successors to one contested source, and a backwards pair whose "successor" is older than its source. They are the data the two event-upcaster fitness rules are proven against.

  • Depends on - BaseIntegrationEvent (every one of them derives from it, EventUpcasterFixtures.cs:2). Each is a one-parameter positional record carrying a single string Sku.
  • Concept introduced - SchemaVersion as the ordering a fixture set has to make visible. BaseIntegrationEvent declares public virtual int SchemaVersion => 1 (MMCA.Common/Source/Core/MMCA.Common.Domain/DomainEvents/BaseIntegrationEvent.cs:32), so a contract that overrides nothing is version 1 and a successor states its own number (ADR-010). That default is what lets these fixtures be as small as they are: the V1 of each group is a bare record and only the successors carry an override. The rule reads the property without running a constructor, through RuntimeHelpers.GetUninitializedObject (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:103), which is why a get-only virtual returning a literal is the required shape and why no event needs a parameterless factory just to be inspected. [Rubric §6 - CQRS & Event-Driven] assesses whether event contracts are versioned and governed rather than edited in place; [Rubric §9 - API & Contract Design] applies because a published event is a wire contract; [Rubric §14 - Testability] covers the fixtures.
  • Walkthrough - the file's header doc records a subtlety worth internalizing (:9-:13): the contracts sit in a *.IntegrationEvents namespace so that the residency rule, which EventScopeFitnessTests exercises over this same assembly through a consumer-shaped map, stays satisfied. A fixture that broke a neighbouring rule to prove its own would be a poor fixture. They also never leave this test assembly, so no shipped event contract churns because of them.
Type File:Line SchemaVersion Role
FixtureCompliantV1 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:15 1 (inherited default) First rung of the compliant ladder, and the source of exactly one upcaster.
FixtureCompliantV2 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:18 2 (:20) The middle rung: a target of one upcaster and the source of the next. That dual role is the whole point, since it is a chain rather than a duplicate claim.
FixtureCompliantV3 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:24 3 (:26) Terminal contract of the ladder.
FixtureContestedV1 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:30 1 (inherited default) The offending source: two upcasters both read it, which is what the unique-source rule must report.
FixtureContestedV2 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:33 2 (:35) One of the two rival successors.
FixtureContestedV3 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:39 3 (:41) The other rival successor. Both targets are legal on their own; the offence is the shared source.
FixtureBackwardsV1 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:45 1 (inherited default) The older contract of the backwards pair, used as the upcaster's target, which is the offence.
FixtureBackwardsV2 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:48 2 (:50) The newer contract, used as the upcaster's source.
  • Why they're built this way - three groups because the two rules have three distinct outcomes to prove between them (clean, duplicate claim, wrong direction), and because a chain has to be distinguishable from a duplicate claim. Nothing but Sku on any of them: the payload is irrelevant to both rules, and every byte of fixture that is not load-bearing is a byte that can mislead a future reader about what is being tested. See ADR-090.
  • Where they're used - the type arguments of the five fixture upcasters (FixtureCompliantV1ToV2Upcaster and family), named by nameof in the assertions of EventUpcasterFitnessTests, and, because they are this assembly's only integration events, the live contract IntegrationEventContractTestsBaseTests mutates.

FixtureCompliantV1ToV2Upcaster, FixtureCompliantV2ToV3Upcaster, FixtureContestedClaimUpcaster, FixtureRivalClaimUpcaster, FixtureBackwardsVersionUpcaster

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.UpcasterFixtures.IntegrationEvents · (see per-type table) · Level 4 · class

Five internal sealed upcasters over the eight fixture contracts, each a single expression-bodied Upcast that copies the one field across. Between them they are the truth table for the two event-upcaster fitness rules (ADR-090): two clean rungs of a chain, two rivals claiming one source, and one pointing backwards down the version ladder.

  • Depends on - IEventUpcaster in its two-parameter form (EventUpcasterFixtures.cs:1) and the fixture event contracts they are generic over.
  • Concept introduced - the upcast chain must be a function, and it must run forwards. When a retired contract arrives from the outbox, the consumer resolves the one upcaster registered for that type and replays the message as its successor. Two properties make that mechanical rather than lucky. First, at most one upcaster may read a given source contract: with two claimants, which one runs would depend on DI registration order, so EventUpcastersHaveUniqueSourceTypes groups by source and reports any group with more than one entry (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:14-:17). Second, the target must declare a strictly higher SchemaVersion than the source, because an upcaster that moves sideways or down is not producing a successor at all and the chain stops being a ladder anyone can reason about; EventUpcastersIncreaseSchemaVersion compares the two declared versions and flags targetVersion <= sourceVersion (:44-:48). Note what the second rule deliberately does not do: a missing or non-int SchemaVersion is skipped rather than reported (:37-:42), because that is the business of the separate IntegrationEventsDeclareSchemaVersion rule. One rule, one judgement. Both rules find their subjects by matching the interface on name and arity, "IEventUpcaster`2" (:81-:83), which is how the rule library stays free of a compile dependency on the framework's own Application package. [Rubric §6 - CQRS & Event-Driven] assesses whether asynchronous contracts evolve safely; [Rubric §9 - API & Contract Design] covers the versioning discipline; [Rubric §29 - Resilience & Business Continuity] is the operational consequence, since an outbox row written against a retired contract has to be replayable weeks later.
  • Walkthrough - every one of the five has the same body shape, public {Target} Upcast({Source} integrationEvent) => new(integrationEvent.Sku);, which is the typed overload declared by IEventUpcaster (MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/Events/IEventUpcaster.cs:82); SourceType and TargetType come free as default interface implementations off the generic arguments (:72,:75), which is exactly what the rules read.
Type File:Line The case it models
FixtureCompliantV1ToV2Upcaster MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:54 Clean: the only claimant on FixtureCompliantV1, and its target declares version 2. Must be absent from both rules' reports.
FixtureCompliantV2ToV3Upcaster MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:60 The second rung, and the sharper of the two clean cases: its source is the previous upcaster's target. That is a chain, not a duplicate claim, and a naive implementation that grouped on "types that appear in more than one upcaster" would wrongly flag it. It gets its own dedicated fact.
FixtureContestedClaimUpcaster MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:66 Offender: reads FixtureContestedV1 and produces V2. Legal in isolation.
FixtureRivalClaimUpcaster MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:72 The second claimant on the same FixtureContestedV1, producing V3 instead. The offence is the pair, so the rule's message must name the contested contract and both upcasters.
FixtureBackwardsVersionUpcaster MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:78 Offender for the other rule: source FixtureBackwardsV2 (version 2), target FixtureBackwardsV1 (version 1). It is the only claimant on its source, so it passes the unique-source rule and fails the version rule, which is what keeps the two rules' proofs independent.
  • Why they're built this way - the two rules judge different things about the same set of types, so the fixture set is designed so that each offender trips exactly one of them. If the backwards upcaster also shared a source, a failure in the unique-source rule would mask a regression in the version rule. Being internal keeps them off the assembly's public surface while still visible to ConcreteClasses, which is what the rule enumerates (ArchitectureRules.Upcasters.cs:67).
  • Where they're used - reflected over by EventUpcasterFitnessTests through UpcasterTestMap, and named by nameof in its assertions.

ChildlessFixture, ExemptedOffenderFixture, HelperCascadingFixture, LoopCascadingFixture, MissingOverrideFixture, SelfOnlyDeleteFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CascadeFixtures · (see per-type table) · Level 5 · class

Six aggregate roots in one file, every one an internal sealed class deriving AuditableAggregateRootEntity<TIdentifierType> over int, differing only in what (if anything) their Delete() override does with their child collection. They are the six-way truth table for the cascade-soft-delete rule.

  • Depends on - AuditableAggregateRootEntity<TIdentifierType>, CascadeChildFixture, and Result (CascadeFixtures.cs:1-:2).
  • Concept - cross-references the cascade concept introduced by CascadeChildFixture. What this set adds is the body half of the rule, and it is the sharper half. The rule reads the IL of the Delete() override and accepts exactly two shapes: a direct call to DeleteChildren (the helper on the aggregate base, MMCA.Common/Source/Core/MMCA.Common.Domain/Entities/AuditableAggregateRootEntity.cs:273), or a zero-argument Delete reached through the callvirt opcode, which is what a hand-rolled foreach (var line in _lines) line.Delete(); compiles to (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.CascadeSoftDelete.cs:172-:186). What it deliberately refuses to accept is base.Delete(), and the reason is a small piece of C# trivia doing real work: base.X() on a virtual member is the one thing C# emits as a non-virtual call, and nothing else in the language produces that pairing, so excluding it separates "delete myself" from "delete my children" without guessing (:178-:182). That single discrimination is what makes SelfOnlyDeleteFixture catchable at all.
  • Walkthrough - the rule reports two distinct reasons and names the offending collection in both, so the failure text tells a developer which member to fix: "no Delete() override, so its children ({names}) stay active" (:160) and "Delete() never deletes a child, so its children ({names}) stay active" (:165). Allowlist entries are type full names or namespace prefixes, matched ordinally exactly as the hard-delete rule matches (:79-:83).
Type File:Line What it models
HelperCascadingFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:23 The framework-idiomatic cascade: Result.Combine(DeleteChildren<CascadeChildFixture, int>(_children), base.Delete()) (:29-:30). Must pass.
LoopCascadingFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:37 The hand-rolled cascade every aggregate used before the helper existed: a foreach calling each child's Delete() (:45-:48) then base.Delete(). Must also pass, which is what stops the rule from mandating one spelling.
MissingOverrideFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:58 Owns _orphans and never overrides Delete() at all (:60-:62). The first violation reason, and the report must name _orphans.
SelfOnlyDeleteFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:70 The subtle offender: it does override Delete(), and even touches the collection with _ignored.Clear() before calling base.Delete() (:76-:81). Detaching children from an in-memory list is not a soft delete, so every row stays active. Its doc names the point: this is the shape a naive "does it override Delete" check would pass, which is why the rule reads the body (:65-:69).
ExemptedOffenderFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:88 Structurally identical to MissingOverrideFixture, kept separate so one fact can allowlist it and prove an exemption silences exactly the type it names while the other offender still fails (:84-:87).
ChildlessFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:99 The out-of-scope case: an aggregate with no Delete() override whose only collection is a List<string> (:101-:103). A list of strings is not a child-entity collection, so the rule must ignore the type entirely rather than report it.
  • Why they're built this way - three passing and three interesting failing cases, with the two offenders made structurally identical so the allowlist fact has an unambiguous control. Every fixture is internal and namespaced, which is what keeps them invisible to the framework rules that run over CommonArchitectureMap and to the other self-tests in this assembly (file doc, :10-:15). See ADR-005 and ADR-015.
  • Caveats / not-in-source - the rule's documented limits apply to these fixtures too (ArchitectureRules.CascadeSoftDelete.cs:84-:93): only direct calls in the override are read, so an aggregate that delegates the loop to a private helper is reported; the rule proves a cascade exists, not that it covers every collection; only fields declared on the aggregate are inspected; and grandchildren are the child's own cascade to own. No fixture here exercises those boundaries.
  • Where they're used - scanned by CascadeSoftDeleteFitnessTests through its FixtureAssemblyMap, and named by nameof in all six of its facts.

AggregateConventionTests, CancellationTokenConventionTests, DomainPurityTests, EventVersioningConventionTests, HandlerResultConventionTests, IdempotencyConventionTests, LayerDependencyTests, LocalizedTextConventionTests, MicroserviceExtractionTests, NamespaceCycleTests, PiiConventionTests, RawQueryableConventionTests, SliceCohesionTests, StateManagementConventionTests, UIArchitectureConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · (see per-type table) · Level 13 · class

These fifteen sealed classes share one shape: each is a thin subclass of a shared *TestsBase rule from the MMCA.Common.Testing.Architecture package, supplying the repo's CommonArchitectureMap (and, for a few, one extra override) so the same rule body runs identically across MMCA.Common, MMCA.Store, and MMCA.ADC. This is the [Rubric §34 - Architecture Governance & Documentation] and [Rubric §14 - Testability] story: architecture conventions are executable and enforced in CI rather than left to review, and the rule logic lives in exactly one place (ADR-015). See the thin-subclass pattern introduced by DependencyVersionTests. The canonical body of each rule is the corresponding *TestsBase; these subclasses only wire in the map and any repo-specific floor or allowlist. Each fails the build-and-test CI job on violation, and a couple are deliberately vacuous today (they assert nothing until the framework grows a type that could break the convention, at which point they fire).

Where a subclass carries an override beyond Map, that override is itself a documented architectural decision, not configuration: the exemption lists below (NamespaceCycleTests, CancellationTokenConventionTests, StateManagementConventionTests, RawQueryableConventionTests) all carry their justification in code, which is the point of putting the escape hatch in a compiled file rather than a wiki.

Type File:Line Base rule What it enforces / what differs
AggregateConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/AggregateConventionTests.cs:9 AggregateConventionTestsBase DDD aggregate-root factory rules for the framework's own aggregates: Domain exposes aggregate roots, each has a Result<T>-returning static Create factory and no public constructor. The minimal variant for repos with no business modules. Supplies only Map (:11). [Rubric §4 - DDD.]
CancellationTokenConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CancellationTokenConventionTests.cs:10 CancellationTokenConventionTestsBase The trailing-token convention taught above, applied to the real Application and Infrastructure packages. Supplies Map (:12) and overrides CancellationTokenExemptMethods with the two SignalR hub methods NotificationHub.JoinChannelAsync and NotificationHub.LeaveChannelAsync (:23-:27). The justification (:14-:22) is worth reading: a hub method signature is the client-visible RPC contract, bound by name and argument list by SignalR's dispatcher, so adding a parameter would break every shipped consumer's client for a token the hub already has (both methods pass Context.ConnectionAborted straight into the group calls). [Rubric §12 - Performance & Scalability.]
DomainPurityTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/DomainPurityTests.cs:9 DomainPurityTestsBase Domain and Shared stay framework-free, and Application stays host-agnostic (no EF Core, no ASP.NET Core). Supplies only Map (:11); the base's extra-forbidden-dependency hook (used by Store for "Stripe", ADC for "RabbitMQ") stays empty here. [Rubric §3 - Clean Architecture.]
EventVersioningConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventVersioningConventionTests.cs:12 EventConventionTestsBase Every integration event declares a SchemaVersion, inherits BaseIntegrationEvent, and lives in an *.IntegrationEvents namespace (ADR-010). Supplies only Map (:14). No longer vacuous: the framework ships one concrete integration event today, OutputCacheEvictionRequested in MMCA.Common.Domain.IntegrationEvents (class doc, :7-:10). [Rubric §6 - CQRS & Event-Driven.]
HandlerResultConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:12 HandlerResultConventionTestsBase Every concrete command/query handler's TResult must be Result or Result<T>, the constraint the decorator pipeline otherwise only enforces at runtime when ResultFailureFactory throws on a short-circuit (:9-:10); scans the framework's Notifications handlers. Supplies only Map (:14). [Rubric §6 - CQRS & Event-Driven.]
IdempotencyConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:10 IdempotencyConventionTestsBase The POST idempotency-intent gate proven by IdempotencyFitnessTests, applied to the framework's own API layer: every POST it ships either replays on an Idempotency-Key or says in code why it must not (:6-:8). Supplies only Map (:12). [Rubric §9 - API & Contract Design, Rubric §29 - Resilience.]
LayerDependencyTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/LayerDependencyTests.cs:9 LayerDependencyTestsBase Clean Architecture layer-flow for the framework packages: Domain, Application, Infrastructure, Shared, and Ui each reference only what they may, plus the map-completeness facts LayerMapDeclaresLayers and ModulesDeclareLayers whose override is proven by LayerDependencyOverrideTests. Supplies only Map (:11), so the base's default required-layer set applies and the per-module half is vacuous here. [Rubric §3 - Clean Architecture.]
LocalizedTextConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:11 LocalizedTextConventionTestsBase Shared MMCA.Common.UI ships no hard-coded user-visible literals: snackbar messages, page titles, <PageTitle> markup, and breadcrumb labels must resolve through IStringLocalizer (ADR-027). Supplies Map (:13) and overrides MinimumScannedFiles => 20 (:16), a floor that catches a wrong scan root (the comment notes MMCA.Common.UI alone carries about 30 razor files, :15). [Rubric §27 - i18n.]
MicroserviceExtractionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/MicroserviceExtractionTests.cs:11 MicroserviceExtractionTestsBase Domain, Application, and Shared stay free of MassTransit, Grpc, and Protobuf, so a module behaves identically in-process or extracted (ADR-007, ADR-008). Supplies only Map (:12); Common-only transport sanity lives in FrameworkSanityTests instead (:7-:8). [Rubric §7 - Microservices Readiness.]
NamespaceCycleTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/NamespaceCycleTests.cs:9 NamespaceCycleTestsBase The acyclicity rule proven by NamespaceCycleFitnessTests, applied to the real framework packages. Supplies Map (:11) and overrides AllowedCycleNamespaces with the one accepted tangle inside MMCA.Common.Infrastructure: root, .Persistence, .Settings (:39-:44). The 26-line justification (:13-:38) argues each of the three edges individually (composition root binds settings; TenancySettingsValidator takes an optional IDataSourceResolver so a bad tenant override fails the boot instead of resolving cross-tenant at runtime; the [UseDataSource] and [UseDatabase] marker attributes stay in the root namespace because consumers annotate with them) and notes that none of the three is separately extractable anyway, being one assembly and one package. Because the allowance must cover the whole strongly connected component, a fourth namespace joining the tangle still fails. [Rubric §15 - Best Practices & Code Quality.]
PiiConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiConventionTests.cs:13 PiiConventionTestsBase Every domain entity declaring a [Pii] property implements IAnonymizable (ADR-005). Supplies only Map (:15). Structurally vacuous in the framework, and its own doc says so (:7-:12); the machinery is proven non-vacuously by PiiErasureContractFitnessTests. [Rubric §30 - Compliance/Privacy.]
RawQueryableConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:13 RawQueryableConventionTestsBase Bans the raw IQueryable repository surfaces in Application code via a textual scan; a raw-queryable handler is EF-coupled and cannot move behind a gRPC boundary. Because Common declares no modules, it overrides ApplicationSourceDirectories() (:18-:22) to scan the framework's own Source/Core/MMCA.Common.Application project (resolving the repo root through ArchitectureMapBase.FindRepoRoot("MMCA.Common.slnx"), :20), and overrides AllowedFiles (:25-:37) to whitelist the deliberate composition root EntityQueryService.cs and the five Notifications handlers whose cross-entity joins are the documented exception. Supplies Map at :15. [Rubric §8 - Data Architecture.]
SliceCohesionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionTests.cs:10 SliceCohesionTestsBase Handlers and validators each sit in the same namespace as the command or query they serve, so a Notifications use-case slice stays one cohesive unit. Supplies only Map (:12). [Rubric §5 - Vertical Slice.]
StateManagementConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/StateManagementConventionTests.cs:11 StateManagementConventionTestsBase The shared MMCA.Common.UI assembly carries no mutable static state (a static is shared across every Blazor Server circuit) and its stateful services stay scoped rather than singleton. Supplies Map (:13) and overrides AllowedStaticMembers to whitelist MMCA.Common.UI.Pages.Common.ErrorMessages._localizer (:21-:22), a write-once wiring point configured idempotently by the root layout, not per-user state (:15-:20). [Rubric §19 - State Management.]
UIArchitectureConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:11 UIArchitectureConventionTestsBase Holds the container/presentational split with mechanical caps: *.razor.cs code-behind files and inline @code blocks each stay within the base's convention limit. Supplies only Map (:13), so the base's defaults apply. [Rubric §18 - UI Architecture.]
  • Why they're built this way - see the two-layer "Architecture Enforcement" model in MMCA.Common/CLAUDE.md: rules are enforced at compile time (Source/Build/MMCA.Common.LayerEnforcement.targets) and at runtime here, with the runtime bodies factored into one shared package so Common, Store, and ADC stay identical. Each subclass exists only so xUnit discovers the rule in this repo's assembly with this repo's map.
  • Where they're used - all fifteen run in the MMCA.Common.Architecture.Tests project during CI's build-and-test job (fast, no database).
  • Caveats / not-in-source - the per-rule fact counts live in each *TestsBase, not in these subclasses; the base sections elsewhere in this chapter are the authority on exactly what each rule asserts.

CascadeSoftDeleteConventionTests, ConcurrencyConventionTests, ContractImplementationTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · (see per-type table) · Level 13 · class

Three more thin subclasses of the same shape as the fifteen above, each supplying only Map. They are grouped separately because they share a second property that is worth understanding on its own: all three are ratchets rather than assertions in this repo. MMCA.Common has nothing today that could violate them, so a green run proves nothing about the framework's current code, and each subclass's own doc says so plainly.

  • Depends on - the corresponding *TestsBase and CommonArchitectureMap, and nothing else.
  • Concept introduced - a deliberately vacuous gate, kept because of when it will first fire. The instinct is to delete a test that asserts nothing, and for a unit test that instinct is right. For a convention gate it is wrong, and the reason is timing. A convention is cheapest to hold on the first type that could break it, when the shape is still a draft and nobody depends on it; it is most expensive to retrofit later, across every type that has since copied the first one. Running the rule from the start means the invariant is enforced from that first type onward with no test for anyone to remember to write. The cost is that the suite carries three facts that currently read nothing, and the mitigation is that each carries its own note saying so, so a reader is never misled into thinking the framework has been checked. Compare PiiConventionTests, which is vacuous in the same way and is paired with a non-vacuous behavioral test, and ServiceContractPurityTests, which spells the ratchet argument out at length. [Rubric §34 - Architecture Governance & Documentation] is the category: a ratchet is governance expressed as code with a known, dated cost.
Type File:Line Base rule What it will enforce, and why it is vacuous today
CascadeSoftDeleteConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:14 CascadeSoftDeleteConventionTestsBase An aggregate root that owns children deletes them in its own Delete() override. Supplies only Map (:16). The framework's Source/ declares no child-bearing aggregate today, so this run is the ratchet; it fires the moment the framework grows one whose children a delete would leave active. Its doc points at CascadeSoftDeleteFitnessTests as where the rule's own behavior is proven (:5-:12). [Rubric §8 - Data Architecture, Rubric §30 - Compliance/Privacy.]
ConcurrencyConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:12 ConcurrencyConventionTestsBase No *UpdateRequest carries a concurrency token in its body: the token belongs in the If-Match header, not in the payload, so a client cannot forge or omit it silently. Supplies only Map (:14). MMCA.Common is module-less, so there is no update request to judge; the rule fires if the framework itself grows one that reintroduces a body token (:5-:10). [Rubric §9 - API & Contract Design.]
ContractImplementationTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ContractImplementationTests.cs:11 ContractImplementationTestsBase The class serving a published [ServiceContract] interface must not itself be public, so consumers bind to the contract rather than to the implementation. Supplies only Map (:13). MMCA.Common marks no interface with the attribute today, so the rule is attribute-driven and currently selects nothing (:5-:9). Its purity sibling ServiceContractPurityTests is in the same position for the same reason. [Rubric §7 - Microservices Readiness, Rubric §1 - SOLID.]
  • Why they're built this way - each is a two-line subclass with a doc comment that is longer than its body, which is the correct ratio for a ratchet: the code says nothing surprising and the comment carries the whole decision. Keeping them in this repo also means the framework is held to the same bar it exports, so a new framework type that would break a consumer's gate fails here first rather than in the consumers after a release.
  • Where they're used - all three run in the MMCA.Common.Architecture.Tests project during CI's build-and-test job. The same three bases are subclassed non-vacuously by MMCA.Store and MMCA.ADC over their own modules.
  • Caveats / not-in-source - a green run of any of these three says nothing about the framework's code today. Only CascadeSoftDeleteConventionTests has a companion that proves its rule fires at all (CascadeSoftDeleteFitnessTests); the other two rules have no fixture-backed proof in this repo.

CustomExceptionThrowingFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainThrowFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:67 · Level 1 · class

  • What it is - a one-method static fixture that throws the file's custom domain exception, so the domain-throw rule can be shown to judge by constructed type rather than by a list of framework exception names.
  • Depends on - TicketDomainException (DomainThrowFixtures.cs:69).
  • Concept - cross-references the domain-throw concept introduced by its sibling fixtures. What this one adds is the discrimination that matters most in practice: teams that adopt the Result pattern often keep a DomainException hierarchy as a compromise, and a rule built around a deny-list of BCL exception names would wave it through. The rule instead allows exactly three types and reports everything else (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:16-:21,:132-:135), so a custom exception is caught for what it is. The fixture doc puts it in one sentence: the same defect wearing a domain name (:48).
  • Walkthrough - the whole type is internal static class CustomExceptionThrowingFixture (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:67) with one member, internal static void Reject() => throw new TicketDomainException("The ticket was rejected"); (:69). The newobj immediately precedes the throw, so ConstructedExceptionType names it and the site is a hard violation rather than an UNVERIFIABLE.
  • Where it's used - the subject of CustomDomainException_IsAlsoFlagged (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DomainThrowFitnessTests.cs:48-:57), and the residual offender the allowlist facts assert is still reported when only its sibling is exempted (:114-:126).

DisabledFakeExportService

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:28 · Level 1 · class

  • What it is - the stub implementation a disabled FakeDependentModule leaves behind, so the fixture models the real "module off, contract still resolvable" behavior rather than describing it.
  • Depends on - IFakeExportService (public sealed class DisabledFakeExportService : IFakeExportService;, ModuleConformanceTestsBaseTests.cs:28).
  • Concept - cross-references the disabled-stub concept introduced by IFakeExportService. The pairing matters: a stub type distinct from the real implementation is what lets an assertion prove which implementation the container holds, not merely that the service type is registered.
  • Walkthrough - no members; a body-less sealed class with its doc comment at :27. It is the exact type FakeDependentModuleConformanceTests asserts on via descriptor.ImplementationType.Should().Be<DisabledFakeExportService>() (:76).
  • Where it's used - registered as a singleton by FakeDependentModule.RegisterDisabledStubs (:43-:44).

FixtureDomainEvent

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainEventSaveFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:13 · Level 1 · record

  • What it is - the single throwaway domain event every handler fixture in DomainEventSaveFixtures is generic over. It carries no meaning; it exists so the four fixture handlers can implement a real, closed IDomainEventHandler<T>.
  • Depends on - IDomainEvent (public sealed record FixtureDomainEvent(DateTime DateOccurred, Guid MessageId) : IDomainEvent;, DomainEventSaveFixtures.cs:13, with the interface import at :3).
  • Concept - cross-references the handler-save concept introduced by IBadgeGranter. Its own contribution is the closure: the rule finds handlers by asking CallGraphIndex whether a type implements the open interface MMCA.Common.Application.Interfaces.IDomainEventHandler1 matched by full name with Cecil's arity marker (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:8-:9,:96`), so any closed instantiation works and one event type is enough for the whole fixture set.
  • Walkthrough - two positional members, DateTime DateOccurred and Guid MessageId, which are the two members IDomainEvent requires. It is the only public type in the file; the handlers around it are all internal.
  • Where it's used - the type argument of DirectSavingHandler, TransitiveSavingHandler, InterfaceDispatchSavingHandler and InnocentHandler, and the assembly anchor of DomainEventHandlerSaveFitnessTests's FixtureAssemblyMap (DomainEventHandlerSaveFitnessTests.cs:116).

InheritingFitnessController

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:81 · Level 1 · class

  • What it is - a body-less concrete controller that inherits AbstractFitnessControllerBase and, with it, the base's declared POST action. It is the fixture for the "covered by inheritance" branch of the idempotency gate.
  • Depends on - AbstractFitnessControllerBase (public sealed class InheritingFitnessController : AbstractFitnessControllerBase;, IdempotencyFitnessTests.cs:81).
  • Concept introduced - inherited attributes are the reason a base-class action does not have to be redeclared. Reflection over a derived type reports the base's CreateInheritedAsync as one of its public methods, and the rule reads attributes with inherit: true (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:80-:81), so the [Idempotent] sitting on the base action covers every subclass. That is not a detail of the fixture: in the real framework, a concrete controller deriving from AuthControllerBase or AggregateRootEntityControllerBase satisfies the gate through those bases (ArchitectureRules.Idempotency.cs:31-:34), which is the only reason the rule is not a wall of duplicate annotations. This fixture is what pins that behavior. [Rubric §9 - API & Contract Design] and [Rubric §14 - Testability] apply, as for its sibling fixtures.
  • Walkthrough - no members at all; the declaration ends at its semicolon (:81). Being concrete is what puts it into the rule's ConcreteClasses scan while its abstract base stays out.
  • Where it's used - asserted absent from the rule's failure message by Rule_AcceptsDirectAndInheritedAndOptedOutDeclarations (IdempotencyFitnessTests.cs:34-:36).

InheritingFixtureController

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:94 · Level 1 · class

  • What it is - a body-less concrete controller deriving AbstractAnonymousFixtureControllerBase. It is the fixture for the ruling that an inherited [AllowAnonymous] is reported once, at the base that declares it, and never again on the derived type.
  • Depends on - AbstractAnonymousFixtureControllerBase (public sealed class InheritingFixtureController : AbstractAnonymousFixtureControllerBase;, AnonymousEndpointTestsBaseTests.cs:94).
  • Concept introduced - the same inheritance question, answered the opposite way from the idempotency gate. InheritingFitnessController exists because the idempotency rule reads attributes with inherit: true, so a base declaration covers every subclass. The anonymous-endpoint scan does the reverse: it reads methods with BindingFlags.DeclaredOnly and attributes with inherit: false (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:142,:146). Both choices are correct for their own rule, and the reason is who writes the allow-list. Idempotency intent is declared by the framework and must flow down to consumers, so inherited reads are what stop a wall of duplicate annotations. An anonymous endpoint must be reviewed, and if the scan reported the framework's base action once per derived controller, every consumer repo would have to re-approve the framework's six credential-exchange endpoints under its own type names, forever. The base's own comment states exactly that (:140-:141). [Rubric §11 - Security] is the property; [Rubric §33 - Developer Experience] is the reason for the shape, since a gate that demands the same approval in every downstream repo gets rubber-stamped rather than read.
  • Walkthrough - no members; the declaration ends at its semicolon (:94). Being concrete puts it in the scan (IsController walks the base chain, AnonymousEndpointTestsBase.cs:101-:112), and the DeclaredOnly filter is what keeps it silent: it declares no methods of its own, so AnonymousEndpointsOf yields nothing for it.
  • Where it's used - the subject of Base_DoesNotReport_AnInheritedAttributeOnTheDerivedController (AnonymousEndpointTestsBaseTests.cs:61-:71), which reads ConformantTests's scan output and asserts it does not contain {InheritingFixtureController.FullName}.InheritedAnonymousAsync (:69-:70). The inline comment there names the consequence being prevented (:64-:66).

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/NavigationContractTests.cs:19 · Level 1 · class

  • What it is - a documentation-drift gate for navigation (rubric §25): it asserts that the "Routes shipped by the framework" table in NavigationFlow.md stays in lockstep with the routable pages the MMCA.Common.UI assembly actually ships, that each route's documented auth posture matches the [Authorize] reality on the page, and that every route parameter carries a type constraint.
  • Depends on - UISharedAssemblyReference (the reflection anchor for the shared UI assembly, NavigationContractTests.cs:105), plus externals: ASP.NET Core RouteAttribute and AuthorizeAttribute (the route and guard metadata reflected over, :107,:113), two source-generated regexes via [GeneratedRegex] partial properties (:139,:142), the embedded NavigationFlow.md manifest resource (:125; wired by the csproj at MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/MMCA.Common.Architecture.Tests.csproj:11-:13), xUnit [Fact], and AwesomeAssertions.
  • Concept introduced - documentation as an executable contract. This is the first place a test asserts a hand-authored markdown doc against live code reality rather than the reverse. NavigationFlow.md lives next to the framework code (it is an embedded resource of this test project, per MMCA.Common/CLAUDE.md) precisely so this gate can parse it; a page added, removed, or re-routed without the doc moving in the same change fails the build, and so does an auth-posture lie in either direction. [Rubric §25 - Navigation & IA] assesses whether routing and information architecture are deliberate and documented; this test turns the route table into a build-enforced invariant instead of review discipline, and its fourth fact holds the typed-deep-link half of the same category. [Rubric §11 - Security] and [Rubric §26 - Front-End Security] also apply: a route the doc calls Authenticated must carry [Authorize], and a route it calls Anonymous must not, so a mis-documented guard cannot pass silently. [Rubric §34 - Architecture Governance & Documentation] is the umbrella: the documentation is proven current by CI.
  • Walkthrough - two constants pin the contract: MinimumRoutes = 8 (:21) and DocResource = "NavigationFlow.md" (:22). Four [Fact]s enforce it.
    • RoutablePages_AreDiscovered_GateIsNotVacuous (:24-:28) asserts reflection finds at least eight routed pages, so a broken anchor cannot let the whole gate pass having scanned nothing.
    • EveryRoutablePage_IsDocumented_AndEveryDocumentedRoute_Exists (:30-:43) computes the set difference both ways: undocumented routes (real but missing from the doc, :36) and phantom routes (documented but no longer real, :37) must both be empty.
    • EveryDocumentedAuthPosture_MatchesTheRouteAttributeReality (:45-:79) walks each documented route, skips ones the set-equality fact already reports as phantoms (:54-:57), classifies the auth cell as Authenticated..., Anonymous, or Any (:59-:61), and flags three violation kinds: an unrecognized posture string (:63-:66), a doc that promises authentication where the page carries no [Authorize] (:67-:69), and a doc that promises an open route where the page is actually guarded (:71-:73).
    • RouteTemplateParameters_AllCarryTypeConstraints (:81-:100) matches every {...} segment of every discovered template with RouteParameterRegex and fails any whose body carries no : (:90-:94). The violation text states the ruling: an unconstrained parameter accepts arbitrary strings, so the page rather than the router becomes the validation boundary, where a constrained one renders NotFound before the component is reached.
    • Two private helpers supply the two sides. DiscoverRoutedPages (:102-:121) reflects over the shared-UI assembly, collecting each RouteAttribute.Template and whether the type carries [Authorize] (inherited attributes included, :113); DiscoverDocumentedRoutes (:123-:137) reads the embedded doc, throwing a clear InvalidOperationException when the resource is missing (:125-:126), and extracts route rows via the source-generated RouteRowRegex (:139-:140, a 2000ms-timeout [GeneratedRegex] partial property).
  • Why it's built this way - NavigationFlow.md is deliberately kept in MMCA.Common next to the routed pages (not in the Website docs library) so this gate can embed and parse it; making route and auth-posture documentation a compiled assertion is what keeps a public navigation contract from rotting. ADR-015 establishes the fitness-function approach this class applies to documentation.
  • Where it's used - an independent class in the MMCA.Common.Architecture.Tests suite; it runs in CI's build-and-test job (fast, no database) and has no Store or ADC counterpart because it guards the framework's own shared-UI route table. It is also one of the two entries DomainThrowFitnessTests allowlists, because its missing-resource guard is a real InvalidOperationException throw inside this assembly (DomainThrowFitnessTests.cs:31).

ObservabilityConventionTestsBaseTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/ObservabilityConventionTestsBaseTests.cs:14 · Level 1 · class

  • What it is - a cross-assembly regression guard for the shared SLO alert-to-runbook pairing rule. It subclasses ObservabilityConventionTestsBase from an assembly other than the one the base ships in, re-points it at a fixture IaC pair embedded in this test project, and asserts the base resolves its manifest resources from the derived type's assembly.
  • Depends on - ObservabilityConventionTestsBase (public sealed class ObservabilityConventionTestsBaseTests : ObservabilityConventionTestsBase, ObservabilityConventionTestsBaseTests.cs:14) and the two fixture resources embedded by the csproj under the logical names fixtures.observability-main.bicep and fixtures.observability-OPERATIONS.md (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/MMCA.Common.Architecture.Tests.csproj:17-:22). Externals: System.Reflection.Assembly, xUnit [Fact], AwesomeAssertions.
  • Concept introduced - proving a shipped base class's defaults from outside its own assembly. The rule library ships as the MMCA.Common.Testing.Architecture package (ArchitectureRules plus its *TestsBase family), so a base can carry a default that is only ever wrong for a consumer: ResourceAssembly => GetType().Assembly (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs:51) must bind to the subclass's assembly, because the base's own assembly never contains a consumer's bicep. Testing that default inside the base's own assembly would prove nothing (both assemblies would be the same one), so the guard is deliberately located here, one assembly away. [Rubric §13 - Observability & Operability] assesses whether alerts stay operable, and the rule this class exercises is the one that keeps every provisioned SLO alert paired with a severity-correct runbook section. [Rubric §14 - Testability] applies because this is a meta-test that keeps a shared gate honest, and [Rubric §33 - Developer Experience] because the alternative failure mode is a break that surfaces only in the first downstream repo to adopt the base.
  • Walkthrough - two property overrides re-point the base's resource names at the fixtures: BicepResource => "fixtures.observability-main.bicep" (:16) and RunbookResource => "fixtures.observability-OPERATIONS.md" (:18), replacing the base defaults infra.main.bicep and infra.OPERATIONS.md (ObservabilityConventionTestsBase.cs:42,:45). One own [Fact], ResourceAssembly_DefaultsToTheDerivedTypesAssembly (:24-:29), asserts the resolved ResourceAssembly is the same object as this class's assembly and is not the base's assembly. Inheritance supplies the rest: the base's three [Fact]s (ObservabilityConventionTestsBase.cs:53,:63,:91) run against the fixture pair, so the whole discovery-and-pairing path is exercised across the assembly boundary. The fixture bicep declares exactly three well-formed specs plus the two parse anchors the base looks for, var sloAlertSpecs and resource sloAlerts (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Fixtures/observability-main.bicep:6-:25), which meets the base's default MinimumAlertSpecs => 3 (ObservabilityConventionTestsBase.cs:39); the fixture runbook carries a matching ### fixture-alert-<key> (sev N) heading per spec (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Fixtures/observability-OPERATIONS.md:9-:19), so both pairing directions and the severity check pass.
  • Why it's built this way - the class doc records the exact regression it exists for (:5-:13): resolving against the base's own assembly instead of the subclass's is a silent break, since the framework's CI would stay green and only the first adopting consumer would fail. Keeping the guard in a different assembly is the only way to make that difference observable. It is the packaging discipline ADR-058 describes for shipped conformance suites, applied to a fitness base; the rule it guards implements the alerting side of ADR-041.
  • Where it's used - an independent class in the Common architecture suite. The real adopters are ObservabilityConventionTests in MMCA.ADC and MMCA.Store (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/ObservabilityConventionTests.cs:7, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/ObservabilityConventionTests.cs:7), which subclass the same base with no overrides at all and rely entirely on the default this class pins.
  • Caveats / not-in-source - the fixture bicep is deliberately minimal (its own header comment says so, observability-main.bicep:1-:4): it proves the base's parse-and-pair logic, not that any real consumer template is well formed. The consumers' own subclasses do that against their real infra/ files.

ReopenTicketCommand, UpdateTicketCommand

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CommandValidatorFixtures · (see per-type table) · Level 1 · record

Two bridged commands: each is a one-property record that wraps a request record and implements ICommandWithRequest<out TRequest>. They differ in exactly one thing, whether the wrapped request has a validator, and that single difference is what proves the bridge is checked on both halves.

  • Depends on - ICommandWithRequest<out TRequest> (CommandValidatorFixtures.cs:2) and the request record each wraps.
  • Concept - cross-references the coverage concept introduced by CreateTicketCommandValidator and UpdateTicketRequestValidator. The pair is the negative-control design that makes the bridge check meaningful: both commands are structurally identical to the rule's discovery pass (both are closed types with a settable property, both are handled, both implement the bridge marker), so the only variable left is whether ValidatedTypes contains their request. One passes, one fails, and nothing else about them can explain the difference.
  • Walkthrough - the rule reaches the request type by projecting the command through the closed generic arguments of ICommandWithRequest1 matched by full name (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureRules.CommandValidators.cs:5,:114), then asks whether any of those arguments is in the validated set. Matching the interface by name rather than by reference is what keeps the rule package free of a compile dependency on the framework's Application layer (:117-:120`).
Type File:Line The case it models
UpdateTicketCommand MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:31 record UpdateTicketCommand(UpdateTicketRequest Request) : ICommandWithRequest<UpdateTicketRequest>. No validator of its own, but its request has one, so the bridge covers it and the rule must stay silent.
ReopenTicketCommand MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:51 record ReopenTicketCommand(ReopenTicketRequest Request) : ICommandWithRequest<ReopenTicketRequest>. Implements the bridge marker, but its request has no validator, so CommandRequestValidator resolves nothing and adds no rule. Its doc states the ruling: half a bridge is not coverage (:47-:50).
  • Why they're built this way - the half-bridge is the failure mode a validation-coverage rule is most likely to miss, because the registration exists and only the resolution is empty. Compiling both halves side by side is what turns that from a code-review concern into an assertion.
  • Where they're used - named by nameof in CommandCoveredThroughTheRequestBridge_IsCovered and BridgeWithNoRequestValidator_IsNotCoverage (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:44-:64), and handled by ReopenTicketHandler and UpdateTicketHandler.

RightModel

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CycleFixtures.Right · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Right/RightFixtures.cs:6 · Level 1 · class

  • What it is - the other arm of the deliberate namespace cycle: a type in the Right fixture namespace that derives from a base in Left, supplying the Right -> Left edge.
  • Depends on - LeftModelBase (public sealed class RightModel : LeftModelBase, RightFixtures.cs:6, with the using of the Left namespace at :1).
  • Concept - cross-references the namespace-cycle concept introduced by LeftModelBase. This half contributes the inheritance edge; LeftService contributes the property-type edge in the other direction. Two edges pointing opposite ways between two namespace nodes is the minimum strongly connected component the rule can report.
  • Walkthrough - one auto-property, string Name initialized to string.Empty (:9), present only so the type is not empty.
  • Where it's used - the property type of LeftService.Model, which is what closes the loop; both are scanned by NamespaceCycleFitnessTests.

FakeDependentModule

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:31 · Level 2 · class

  • What it is - the "hard" module fixture: a module that declares dependencies, refuses to start without them, and exports a disabled stub. It exercises every member of the module contract that a leaf module leaves at its default.
  • Depends on - IModule (public sealed class FakeDependentModule : IModule, ModuleConformanceTestsBaseTests.cs:31), ApplicationSettings (the Register parameter, :39), IFakeExportService and DisabledFakeExportService (the stub pair, :44), plus IServiceCollection and IConfigurationBuilder from Microsoft.Extensions.* (:1-:2).
  • Concept introduced - the full module contract as a testable surface. IModule has five members, three of which are defaulted (Dependencies => [], RequiresDependencies => false, RegisterDisabledStubs as an empty body, MMCA.Common/Source/Core/MMCA.Common.Application/Modules/IModule.cs:17,:23,:34), so the entire contract ModuleLoader registers on can be silently wrong without a compile error. This fixture is the one that overrides all three, so the conformance base is exercised against real (not defaulted) values. [Rubric §14 - Testability] covers the fixture role; [Rubric §7 - Microservices Readiness] is what the stub half protects, since a module that can be switched off without breaking the modules that import its contract is a module that can also be extracted (ADR-059).
  • Walkthrough
    • Name => "FakeDependent" (:33): the key ModulesSettings entries and other modules' dependency lists match on.
    • Dependencies => ["FakeLeaf", "FakeOther"] (:35): two entries, one of which (FakeLeafModule) exists in the fixture set and one of which deliberately does not, so the list is a pure data declaration rather than a resolvable graph.
    • RequiresDependencies => true (:37): the flag that turns a disabled dependency into a startup failure instead of a substituted stub.
    • Register(...) (:39-:41): an empty body. Registration behavior is not what the conformance base asserts, so the fixture spends nothing on it.
    • RegisterDisabledStubs(IServiceCollection services) (:43-:44): a single expression-bodied services.AddSingleton<IFakeExportService, DisabledFakeExportService>(), which is the only member with observable behavior and the one FakeDependentModuleConformanceTests inspects.
  • Why it's built this way - the doc comment (:30) names the intent: this is the shape of the two real consumer modules that are not leaves (Store Sales, ADC Notification). Modeling them locally means the shared base is proven against both module shapes inside MMCA.Common's own CI, before any consumer sees it (ADR-015).
  • Where it's used - the TModule of FakeDependentModuleConformanceTests (:60) and of the deliberately-drifted DriftedTests (:131).

FakeLeafModule

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:15 · Level 2 · class

  • What it is - the minimal module fixture: Name plus an empty Register, and nothing else. Its whole purpose is to not override Dependencies or RequiresDependencies, so the conformance base has to reach the interface's default implementations to read them.
  • Depends on - IModule (public sealed class FakeLeafModule : IModule, ModuleConformanceTestsBaseTests.cs:15) and ApplicationSettings (the Register parameter, :19).
  • Concept introduced - default interface implementations are only reachable through the interface. A default member declared on IModule (IModule.cs:17,:23) does not exist on the concrete class, so module.Dependencies will not compile against FakeLeafModule and a reflection lookup on the concrete type finds nothing. The shared base solves this by locating the IModule interface by full name and reading the property off the interface (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleConformanceTestsBase.cs:80-:99), which dispatches to the override when there is one and to the framework default when there is not. Before the base existed, the hand-written consumer tests needed an explicit (IModule) cast to get the same reach (class doc, :9-:14). [Rubric §14 - Testability] and [Rubric §1 - SOLID] both apply: the fixture exists so the base's interface-dispatch contract is provable rather than assumed.
  • Walkthrough - Name => "FakeLeaf" (:17) and an empty Register(...) (:19-:21). That is the entire type; the absence of members is the fixture.
  • Why it's built this way - this is the exact shape the three byte-identical consumer {X}ModuleTests files collapse into (class doc, :12-:13), so the leaf path is the most-travelled one and the one whose silent breakage would be widest.
  • Where it's used - the TModule of FakeLeafModuleConformanceTests (:51), which in turn is driven directly by two of ModuleConformanceTestsBaseTests's facts (:112-:129).

InnocentHandler

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainEventSaveFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:95 · Level 2 · class

  • What it is - the negative control for the domain-event handler save rule: a handler that mutates in-memory state and returns, which is what a conforming handler looks like.
  • Depends on - IDomainEventHandler<in TDomainEvent> closed over FixtureDomainEvent (DomainEventSaveFixtures.cs:95, imports at :1).
  • Concept - cross-references the handler-save concept from IBadgeGranter. Its contribution is discrimination. The rule walks the call graph out of every method on a handler type up to a depth bound, and a walker that reported any handler with any outbound call would be useless. This fixture makes calls (GetType(), List<string>.Add, ArgumentNullException.ThrowIfNull) and reaches no save, so a green result for it is a statement that the classifier looked and found nothing rather than that it stopped looking.
  • Walkthrough - one private List<string> _seen initialized with a collection expression (:97), and a HandleAsync that guards its argument, records the event's type name, and returns Task.CompletedTask (:99-:105). No constructor and no injected collaborator, which is the point: nothing it holds can lead to a unit of work.
  • Why it's built this way - the doc states the ruling directly (:91-:94): this is what the rule protects, so it must never appear in the report.
  • Where it's used - asserted absent from the failure message by HandlerThatOnlyMutates_IsNotFlagged (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:58-:67).

InterfaceDispatchSavingHandler

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainEventSaveFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:81 · Level 2 · class

  • What it is - the handler fixture whose save is reachable only through an abstraction: it calls IBadgeGranter and never touches a unit of work itself.
  • Depends on - IDomainEventHandler<in TDomainEvent>, FixtureDomainEvent, and IBadgeGranter (the constructor-injected collaborator, DomainEventSaveFixtures.cs:83,:85).
  • Concept - cross-references IBadgeGranter. The mechanism this fixture forces is CallGraphIndex.TargetsOf, which the walk calls for every callee that is not itself a save (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:170-:177): an interface method reference is expanded into the implementations present in the scanned modules, and each unvisited, non-allowlisted target is queued one depth deeper. Without that expansion the walk would see IBadgeGranter.GrantAsync and stop, because an interface method has no body to read.
  • Walkthrough - one field, one constructor, and a HandleAsync that returns _granter.GrantAsync(cancellationToken) directly (:87-:88). It is not async, which is deliberate: the save is exactly two IL calls away with no state machine in between, so the reported chain stays short and the fixture isolates interface resolution from the state-machine traversal that TransitiveSavingHandler exercises.
  • Where it's used - asserted present in the failure message by SaveBehindAnInterface_IsResolvedToItsImplementation (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:47-:56), whose because states the ruling: IL records the interface call, so the walk must expand it to the implementations in scope.

LeftService

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CycleFixtures.Left · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Left/LeftFixtures.cs:6 · Level 2 · class

  • What it is - the Left -> Right half of the fixture cycle: a Left type whose property type lives in the Right namespace.
  • Depends on - RightModel (the nullable Model property, LeftFixtures.cs:9, with the using at :1).
  • Concept - cross-references LeftModelBase. What this type adds is the second edge kind: a property type rather than a base type. The cycle rule records both (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:25-:27), and having one of each in the fixture means a regression that drops either edge kind from the graph builder is caught instead of being masked by the other.
  • Walkthrough - one member, public RightModel? Model { get; set; } (:9). Nullability is irrelevant to the rule (it reads the property type); the reference itself is the fixture.
  • Where it's used - referenced by AcyclicConsumer and scanned by NamespaceCycleFitnessTests.

PasswordHashingFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PasswordHashingFitnessTests.cs:15 · Level 2 · class

  • What it is - a structural credential-storage gate asserted against compiled IL rather than source text: PasswordHasher must depend on Rfc2898DeriveBytes (a deliberately slow key-derivation function) and on CryptographicOperations (the constant-time comparison), so a rewrite cannot quietly swap either out.
  • Depends on - PasswordHasher as the assembly anchor and the type under scan (PasswordHashingFitnessTests.cs:1,:21), ArchitectureAssert (:37,:50), NetArchTest's Types / PredicateList query API (:56-:59), and externals xUnit plus AwesomeAssertions.
  • Concept introduced - asserting on a dependency edge instead of on an output value. A hashing routine can be verified two ways. A known-answer test pins the values (iteration count, salt length, digest length) and is what PasswordHasherSecurityTests does; this class pins the shape, which catches a class of change the value tests cannot. Swap PBKDF2 for a single SHA-512 pass and keep the same 32-byte output length and the known-answer pin can be regenerated by whoever made the change, but the Rfc2898DeriveBytes reference disappears from the IL and this test goes red. Same for replacing CryptographicOperations.FixedTimeEquals with SequenceEqual: identical behavior on every test input, and a timing side channel in production. The two because strings spell out the attacks precisely: commodity GPUs trying billions of candidates per second against a stolen table (:38-:39), and a short-circuiting comparison leaking the matching prefix length so a digest is recovered byte by byte (:51-:53). [Rubric §11 - Security] assesses whether credential storage resists offline cracking and side channels; this is the build-time half of that guarantee. [Rubric §14 - Testability] covers the technique, and [Rubric §32 - Dependency & Supply-Chain] applies at the edges, since the assertion is that a specific BCL cryptographic primitive is actually reached.
  • Walkthrough - two private const string fully-qualified type names, CryptographicOperationsType (:17) and Rfc2898DeriveBytesType (:19), keep the matched names in one place. Infrastructure (:21) anchors the scanned assembly through typeof(PasswordHasher).Assembly, and the private PasswordHasherTypes() helper (:56-:59) narrows it with Types.InAssembly(Infrastructure).That().HaveName(nameof(PasswordHasher)).
    • ScannedPasswordHasherSet_IsNotEmpty (:23-:27): the non-vacuity guard, and the first fact for a reason. HaveName matches on the simple name, so a renamed or moved type would leave the predicate list empty and both dependency assertions would pass having inspected nothing. ContainSingle on the full name (:25-:26) closes that.
    • PasswordHasher_DependsOnASlowKeyDerivationFunction (:29-:40) and PasswordHasher_DependsOnConstantTimeComparison (:42-:54): each runs .Should().HaveDependencyOnAll(<type name>).GetResult() and routes the result through ArchitectureAssert.NoViolations. HaveDependencyOnAll is the positive form (the dependency must be present), which is the unusual direction for an architecture rule and the right one here.
  • Why it's built this way - ADR-032 fixes the hashing scheme; the real implementation calls Rfc2898DeriveBytes.Pbkdf2 (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Auth/PasswordHasher.cs:35) and CryptographicOperations.FixedTimeEquals (:58). Pinning the decision as a fitness function rather than a code-review convention is the ADR-015 approach, and it is the one that survives a refactor by someone who has not read the ADR.
  • Where it's used - an independent class in the Common architecture suite. It is the shape half of a pair; the parameter values are pinned by PasswordHasherSecurityTests in the Infrastructure unit-test project (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.Tests/Auth/PasswordHasherSecurityTests.cs:18), which holds PBKDF2-HMAC-SHA512 at 600,000 iterations with a 32-byte salt and a 64-byte output as reflected private constants (:20-:22). The class doc here records the division of labour (PasswordHashingFitnessTests.cs:10-:13).
  • Caveats / not-in-source - HaveDependencyOnAll reports a reference in the compiled IL, not that the reference is on the hashing path. It cannot tell an actually-used PBKDF2 call from a dead one, which is why the value pins in the companion test remain load-bearing.

AcyclicConsumer

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CycleFixtures.Acyclic · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Acyclic/AcyclicFixtures.cs:6 · Level 3 · class

  • What it is - the negative control for the namespace-cycle rule: a third fixture namespace that points into Left and that nothing points back at, so it must never appear in a cycle report.
  • Depends on - LeftService (the one-way Service property, AcyclicFixtures.cs:9, using at :1).
  • Concept - cross-references LeftModelBase. Its role is the discrimination half of the proof: a rule that reported every namespace with any edge would also "pass" the cycle test, so the fixture set needs a namespace that is genuinely coupled yet genuinely acyclic. Depending on Left (a namespace that is in a cycle) is deliberate: it proves the rule reports strongly connected components, not merely everything reachable from a cycle.
  • Walkthrough - one member, public LeftService? Service { get; set; } (:9).
  • Where it's used - asserted absent from the failure message by Rule_FlagsTwoNamespaceCycle_ButNotAcyclicNamespaces (NamespaceCycleFitnessTests.cs:23-:25).

DriftedTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:131 (and a same-named sibling in Api/AnonymousEndpointTestsBaseTests.cs:100) · Level 3 · class

  • What it is - the name is used twice in this assembly, once per fitness base under proof, and both are the same idea: a private sealed subclass of a shipped *TestsBase whose configuration is deliberately wrong, so the base's assertions can be shown to actually fail on the drift they claim to catch. They are nested in different outer classes, so their full names differ and neither is visible outside its own file.
  • Depends on - the base each one drifts from, plus the fixture it points at: ModuleConformanceTestsBase<TModule> with FakeDependentModule for one, AnonymousEndpointTestsBase with the fixture controllers for the other.
  • Concept introduced - negative fixtures, and hiding them from test discovery. A fitness base that asserts nothing passes everywhere; the only way to know an assertion bites is to feed it a case that must fail. But a public subclass of an xUnit base is itself collected, so its inherited [Fact]s would run and report as red tests of their own. Declaring the drifted subclass private and nested keeps xUnit from collecting it, while the enclosing class can still instantiate it and invoke the inherited methods directly as delegates (var assert = new DriftedTests().Module_ShouldDeclare_ExpectedName;, ModuleConformanceTestsBaseTests.cs:91). Both class docs record that reasoning in the same words (ModuleConformanceTestsBaseTests.cs:81-:85, AnonymousEndpointTestsBaseTests.cs:10-:11). [Rubric §14 - Testability] assesses whether the guardrails themselves are trustworthy; this is the fixture shape that earns that trust. Compare NavigatingSpec, the same negative-fixture technique applied to a specification rule, and ProbeTests, which parameterizes the drift instead of hard-coding it.
Type File:Line What is deliberately wrong
DriftedTests (in AnonymousEndpointTestsBaseTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:100 Points TargetAssemblies at this test assembly (:102-:103), so the scan finds the fixture controllers' [AllowAnonymous] attributes, and then supplies an empty AllowedAnonymousEndpoints (:105). Every discovered endpoint is therefore an offender, and the failure message must name AnonymousFixtureController.
DriftedTests (in ModuleConformanceTestsBaseTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:131 Three overrides, each wrong in a different way against FakeDependentModule's real declarations: ExpectedName => "NotTheDeclaredName" (:133) against the module's "FakeDependent" (:33); ExpectedDependencies => ["FakeLeaf"] (:135) against the module's two-entry ["FakeLeaf", "FakeOther"] (:35), so one dependency is missing; and ExpectedRequiresDependencies => false (:137) against the module's true (:37). AssertDisabledStubs is deliberately not overridden, so the fourth inherited fact stays vacuous here.
  • Walkthrough - one wrong value per assertion, and no more. If a single drifted subclass were wrong in three ways at once and the base only ever threw on the first, the other two assertions could rot undetected; the module-side fixture avoids that by being driven three separate times, once per fact.
  • Where it's used - the anonymous-endpoint one drives Base_Fails_WhenAnAnonymousEndpointIsNotAllowListed (AnonymousEndpointTestsBaseTests.cs:15-:24); the module one is instantiated three times by ModuleConformanceTestsBaseTests (:91,:99,:107), once per assertion under proof.

EmptyScanTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:117 · Level 3 · class

  • What it is - the adversarial fixture for the anonymous-endpoint gate's non-vacuity guard: a subclass pointed at an assembly that contains no controllers and no routable components at all, so the scan finds nothing and the guard must fail.
  • Depends on - AnonymousEndpointTestsBase (private sealed class EmptyScanTests : AnonymousEndpointTestsBase, AnonymousEndpointTestsBaseTests.cs:117) and the MMCA.Common.Shared assembly reached through typeof(Shared.Abstractions.Result).Assembly (:121).
  • Concept introduced - guarding the guard: why a fitness function needs a floor. The allow-list assertion is offenders.Should().BeEmpty() (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:60-:62), and an empty scan produces zero offenders, so the gate passes loudest exactly when it has stopped looking. A renamed assembly, a moved anchor type, or a repo re-layout all produce that failure mode, and none of them is visible in a green run. The base therefore ships a third fact, ScannedEndpointSet_IsNotEmpty (:66-:76), that counts the discovered controller and routable-component types and requires at least MinimumScannedTypes, whose default is 1 (:51). This fixture is what proves the counter is real: point it at an assembly that genuinely has neither shape and the fact must throw. Compare the equivalent floors elsewhere in the suite: MinimumScannedTypes => 21 on AnonymousEndpointTests, MinimumBaseResources => 3 on LocalizationResourceTests, MinimumRoutes = 8 in NavigationContractTests, and the DistinctErrorCodeCount and HandledCommandCount inventories the catalog and validator rules expose for the same purpose. [Rubric §14 - Testability] assesses whether the guardrails are themselves trustworthy, and a vacuity floor is the cheapest thing that keeps one honest over a decade of refactors.
  • Walkthrough - two overrides and an inline comment that states why the chosen assembly works: the Shared package has neither controllers nor routable components (:119). TargetAssemblies returns that one assembly (:120-:121), and AllowedAnonymousEndpoints is empty (:123), which is irrelevant here because nothing is discovered to compare against. Being private and nested is what keeps xUnit from collecting its three inherited facts as deliberately-red tests of their own (class doc, :10-:11).
  • Where it's used - the subject of Base_Fails_WhenNothingWasScanned (AnonymousEndpointTestsBaseTests.cs:37-:43), which converts the inherited ScannedEndpointSet_IsNotEmpty into a delegate and asserts it throws (:40-:42).
  • Caveats / not-in-source - that fact asserts only that an exception is thrown, not what its message says. It proves the floor bites; it does not pin the wording.

FakeDependentModuleConformanceTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:60 · Level 3 · class

  • What it is - the shared module-conformance base wired up against the "hard" fixture module: the subclass shape a real non-leaf module (Store Sales, ADC Notification) uses, with all three expectations declared and the disabled-stub hook implemented.
  • Depends on - ModuleConformanceTestsBase<TModule> (public sealed class FakeDependentModuleConformanceTests : ModuleConformanceTestsBase<FakeDependentModule>, ModuleConformanceTestsBaseTests.cs:60), FakeDependentModule, IFakeExportService, DisabledFakeExportService, and Microsoft.Extensions.DependencyInjection's ServiceCollection / ServiceLifetime (:70,:77).
  • Concept introduced - asserting on a ServiceCollection instead of a built container. The disabled-stub hook is a registration contract, not a resolution one, so the fixture never builds a provider: it constructs a bare ServiceCollection (:70), calls RegisterDisabledStubs on it (:72), and then inspects the resulting ServiceDescriptor directly (:74). That is what lets one assertion cover all three things that can go wrong (wrong service type, wrong implementation type, wrong lifetime) without any of the module's real dependencies having to exist. [Rubric §7 - Microservices Readiness] is the property under guard: a module that can be switched off while its exported contract stays resolvable is a module that can be extracted. [Rubric §14 - Testability] covers the technique.
  • Walkthrough - three property overrides declare the expectations: ExpectedName => "FakeDependent" (:62), ExpectedDependencies => ["FakeOther", "FakeLeaf"] (:64), and ExpectedRequiresDependencies => true (:66). The dependency list is written in the opposite order to the module's own declaration (:35), which is deliberate: the base compares with BeEquivalentTo (ModuleConformanceTestsBase.cs:51), so order must not matter, and writing it reversed proves it does not. AssertDisabledStubs(FakeDependentModule module) (:68-:78) is the one real override: single descriptor for IFakeExportService (:74-:75), implementation type DisabledFakeExportService (:76), lifetime Singleton (:77).
  • Where it's used - collected and run directly by xUnit (it is public and sealed, so its four inherited [Fact]s are real tests of the fixture module), and referenced by ModuleConformanceTestsBaseTests as the non-leaf half of the base's coverage.

FakeLeafModuleConformanceTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:51 · Level 3 · class

  • What it is - the same shared base wired up against the leaf fixture, declaring only ExpectedName and leaving every other expectation at the base's default. It is the exact subclass shape the three byte-identical consumer {X}ModuleTests files collapse into.
  • Depends on - ModuleConformanceTestsBase<TModule> (public sealed class FakeLeafModuleConformanceTests : ModuleConformanceTestsBase<FakeLeafModule>, ModuleConformanceTestsBaseTests.cs:51) and FakeLeafModule.
  • Concept - cross-references the default-interface-dispatch concept from FakeLeafModule. The pairing is what makes the proof work: the fixture module declares neither Dependencies nor RequiresDependencies, and this subclass declares neither expectation, so the base's defaults ([] and false, ModuleConformanceTestsBase.cs:29,:35) must line up with IModule's defaults read through the interface. If either side stopped reaching the interface, this pair would go red.
  • Walkthrough - one member: ExpectedName => "FakeLeaf" (:53). Everything else is inherited, including the vacuous AssertDisabledStubs default, which is exactly what makes the fourth fact a no-op here.
  • Where it's used - collected by xUnit in its own right, and driven directly (as an object, not as a test class) by two of ModuleConformanceTestsBaseTests's facts (:112-:129).

StubMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleIsolationTestsBaseTests.cs:75 · Level 3 · class

  • What it is - a hand-built IArchitectureMap implementation, written from scratch rather than derived from ArchitectureMapBase, so a test can declare exactly one forbidden namespace for exactly one layer.
  • Depends on - IArchitectureMap, LayerRef and the Layer enum (ModuleIsolationTestsBaseTests.cs:75-:79). Four constructor parameters carry the whole configuration: fromLayer, forbiddenLayer, forbiddenNamespace, and the assembly to register.
  • Concept introduced - when the map base is the wrong tool, and why implementing the interface directly is the honest alternative. Every other map in this chapter subclasses ArchitectureMapBase, which derives every other module's namespaces mechanically from the repo token. That derivation is exactly what makes a real map cheap and exactly what makes it useless here: the isolation rules ask a map for OtherModuleNamespaces(module, layer), and to test one specific cross-layer pair the answer has to be one namespace for one layer and empty for the rest. The base cannot express that, so this stub implements all eleven interface members itself (:81-:110) and puts the whole test in OtherModuleNamespaces, a one-line conditional (:109-:110). The class doc states the reasoning verbatim (:70-:74). [Rubric §7 - Microservices Readiness] is the property the rules under test defend; [Rubric §1 - SOLID] is why this substitution is even possible, since the rule library consumes only the interface and never the base; [Rubric §14 - Testability] is the technique.
  • Walkthrough
    • RepoToken => "MMCA.Fixture" (:81) and ModuleNames => ["Beta", "Gamma"] (:83): two declared modules, so the isolation rules have a cross product to iterate, even though only one of them registers an assembly.
    • Layers (:85-:86) is a single LayerRef for module "Beta" at the caller's fromLayer, pointing at the caller's assembly with a derived root namespace.
    • OfLayer and the five convenience accessors (:88-:99) all project off that one list, so a layer with no entry yields an empty sequence and the corresponding rule is vacuous rather than broken.
    • OtherModuleNamespaces(module, layer) (:109-:110) returns the single forbidden namespace when layer == forbiddenLayer and an empty array otherwise. That single line is what isolates the pair under test from every other pair the rules would otherwise check.
  • Why it's built this way - the test it serves needs a violation that is real: the registered assembly genuinely does depend on MMCA.Common.Application (it is the Infrastructure assembly, :25), and the map declares that namespace as another module's Application layer. A hand-written map is the only way to arrange a true dependency edge into a fabricated module boundary without inventing assemblies. See ADR-059 for the module boundary the rules enforce.
  • Where it's used - constructed twice in ModuleIsolationTestsBaseTests: once as the static CrossLayerViolation shared by two facts (:21-:25) and once as a clean map pointed at a namespace nothing references (:59-:63).

AnonymousEndpointTestsBaseTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:13 · Level 4 · class

  • What it is - the meta-test for the shipped anonymous-endpoint base: five facts proving each of its three assertions fails on the drift it claims to catch, that both allow-list identifier shapes are accepted, and that an inherited attribute is not double-reported on the derived controller.
  • Depends on - AnonymousEndpointTestsBase (the type under test), its own nested fixtures (AnonymousFixtureController and family, InheritingFixtureController, DriftedTests, StaleAllowListTests, EmptyScanTests, ConformantTests), plus xUnit [Fact] and AwesomeAssertions' delegate assertions.
  • Concept introduced - cross-references the "test a shipped test base from the outside by invoking its facts as delegates" technique introduced by ModuleConformanceTestsBaseTests. What this class adds is a third assertion axis. The module base is proven by failing it three ways; this base also has to be proven to accept the two identifier shapes its allow-list is written in, because that is a data contract between the base's emitter and every consumer's hand-written list, and getting it wrong makes the gate fail in the one repo the framework's CI never runs. The class doc states both halves (:8-:11). [Rubric §14 - Testability] assesses whether guardrails are trustworthy; [Rubric §11 - Security] is the property at stake, since a broken anonymous-endpoint gate is a gate that stops noticing lost authorization; [Rubric §33 - Developer Experience] covers the failure mode, a break that would only appear downstream after a release (ADR-058).
  • Walkthrough
    • Base_Fails_WhenAnAnonymousEndpointIsNotAllowListed (:15-:24): drives DriftedTests and asserts the thrown message names AnonymousFixtureController (:22), with the because stating the ruling, that the offender message must name the endpoint that lost its gate (:23).
    • Base_Fails_WhenTheAllowListHasAStaleEntry (:26-:35): drives StaleAllowListTests and asserts the message contains "NoLongerAnonymous" (:33), the type name from an entry that matches nothing.
    • Base_Fails_WhenNothingWasScanned (:37-:43): drives EmptyScanTests and asserts the non-vacuity fact throws (:42).
    • Base_Accepts_TypeLevelAndMethodLevelEntries (:45-:59): the positive case. It calls all three of ConformantTests's inherited facts inside one delegate (:50-:55) and asserts NotThrow (:57-:58). Running all three together matters: the allow-list is only correct if it is simultaneously complete (no offenders) and exact (no stale entries).
    • Base_DoesNotReport_AnInheritedAttributeOnTheDerivedController (:61-:71): reads the scan output through ConformantTests's AnonymousEndpointsForTest() and asserts the derived-controller identifier is absent (:69-:70). This is the only fact that inspects the emitted set rather than an assertion's outcome.
  • Why it's built this way - the four drifted and conformant subclasses are all private, which is what keeps xUnit from collecting their inherited facts as deliberately-failing tests of their own (class doc, :10-:11). The base ships in the MMCA.Common.Testing.Architecture package, so proving it here is proving it before a release rather than after one.
  • Where it's used - an independent class in the Common architecture suite. The base it protects is bound for real by AnonymousEndpointTests here and by the equivalent subclass in each consumer repo.

CascadeChildFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CascadeFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:17 · Level 4 · class

  • What it is - the child entity every cascade fixture aggregate owns a collection of. It is what makes those collections child-entity collections in the rule's eyes rather than ordinary lists.
  • Depends on - AuditableBaseEntity<TIdentifierType> closed over int (CascadeFixtures.cs:17, import at :1).
  • Concept introduced - soft-delete does not cascade, and the shape of a rule that can tell. Setting IsDeleted = true on an aggregate root hides the root behind the global query filter and leaves every child row ACTIVE: an order line of a deleted order, a session of a deleted event. Nothing reads those rows through the root any more, so the orphans are invisible until a report, an export or a per-child query surfaces them, and an erasure request that walks the child table finds data whose owner was "deleted" months ago. The database cannot help either, because a soft delete is an ordinary UPDATE and there is no ON DELETE CASCADE to fire (rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.CascadeSoftDelete.cs:35-:42). AggregatesCascadeSoftDeleteToChildren (:100) therefore fails any concrete type deriving from AuditableAggregateRootEntity<T> that declares a collection of AuditableBaseEntity<T> children and does not delete them in its own Delete() override. The child-collection test is deliberately narrow: an instance field on the aggregate whose type is one of six named generic collection types (:25-:33) over an element type deriving from the auditable entity base (:200). Auto-properties are covered by the same pass, because an auto-property is a compiler-generated instance field, and the backing field name is normalized back to the property name for the report (:192-:197,:242). The aggregate's own _domainEvents escapes twice over: it is declared on the base rather than on the aggregate, and IDomainEvent is not an entity (:66-:68). [Rubric §8 - Data Architecture] assesses whether the data model's lifecycle rules actually hold; [Rubric §30 - Compliance/Privacy/Data Governance] is the sharper consequence, since an orphaned child row is data nobody accounts for at erasure time (ADR-005); [Rubric §14 - Testability] covers the fixtures.
  • Walkthrough - one member, public string Label { get; init; } = string.Empty; (:19). Deriving from AuditableBaseEntity<TIdentifierType> is the entire fixture: strip that base and every collection in the file becomes invisible to the rule, which is precisely what ChildlessFixture's List<string> demonstrates from the other side.
  • Why it's built this way - the file doc explains why every fixture here is internal and in its own namespace (:10-:15): it keeps them invisible to the framework rules that run over CommonArchitectureMap, which registers the Source/ assemblies and never this test assembly.
  • Where it's used - the element type of five collections across the six cascade aggregates, and the assembly anchor of CascadeSoftDeleteFitnessTests's FixtureAssemblyMap (CascadeSoftDeleteFitnessTests.cs:103).

ConformantTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:126 · Level 4 · class

  • What it is - the positive fixture for the anonymous-endpoint gate: a subclass whose allow-list correctly names every anonymous endpoint in the fixture set, in both identifier shapes, so all three inherited facts must pass.
  • Depends on - AnonymousEndpointTestsBase (private sealed class ConformantTests : AnonymousEndpointTestsBase, AnonymousEndpointTestsBaseTests.cs:126) and the three fixture controllers it allow-lists.
  • Concept introduced - building allow-list entries with typeof(...).FullName and nameof(...) rather than string literals. Each of the three entries is interpolated from live metadata (:133-:135): $"{typeof(AnonymousFixtureController).FullName}.{nameof(AnonymousFixtureController.PeekAsync)}" for the method-level shape, the same construction against the abstract base for the inherited action, and a bare typeof(TypeLevelAnonymousFixtureController).FullName! for the type-level shape. Renaming any fixture is then a compile error instead of a silently-drifting test. That discipline is not available to a real consumer's list (the framework's endpoints are strings there, as in AnonymousEndpointTests), which is precisely why the stale-entry fact exists to catch what nameof would have caught. [Rubric §15 - Best Practices & Code Quality] and [Rubric §14 - Testability] apply.
  • Walkthrough - TargetAssemblies is this test assembly (:128-:129), so the scan sees the nested fixture controllers. AllowedAnonymousEndpoints (:131-:136) holds the three entries described above; note the abstract base is listed at its own name rather than the deriving controller's, which is the ruling InheritingFixtureController pins. MinimumScannedTypes is left at the base's default of 1, which the fixture set clears comfortably. One member is added beyond the base's surface: internal IReadOnlyCollection<string> AnonymousEndpointsForTest() => [.. AnonymousEndpoints()]; (:138), which materializes the base's protected enumeration so the enclosing test class can assert on the emitted set directly. The base exposes AnonymousEndpoints() as protected for exactly this kind of extension (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:120-:125).
  • Where it's used - Base_Accepts_TypeLevelAndMethodLevelEntries (AnonymousEndpointTestsBaseTests.cs:45-:59) runs its three inherited facts, and Base_DoesNotReport_AnInheritedAttributeOnTheDerivedController (:61-:71) reads its AnonymousEndpointsForTest() output.

DriftedTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests · (see per-type table) · Level 4 · class

The name is used twice in this assembly, once per fitness base under proof, and both are the same idea: a private sealed subclass of a shipped *TestsBase whose configuration is deliberately wrong, so the base's assertions can be shown to actually fail on the drift they claim to catch. They are nested in different outer classes, so their full names differ and neither is visible outside its own file.

  • Depends on - the base each one drifts from, plus the fixture it points at: ModuleConformanceTestsBase<TModule> with FakeDependentModule for one, AnonymousEndpointTestsBase with the fixture controllers for the other.
  • Concept introduced - negative fixtures, and hiding them from test discovery. A fitness base that asserts nothing passes everywhere; the only way to know an assertion bites is to feed it a case that must fail. But a public subclass of an xUnit base is itself collected, so its inherited [Fact]s would run and report as red tests of their own. Declaring the drifted subclass private and nested keeps xUnit from collecting it, while the enclosing class can still instantiate it and invoke the inherited methods directly as delegates (var assert = new DriftedTests().Module_ShouldDeclare_ExpectedName;, ModuleConformanceTestsBaseTests.cs:91). Both class docs record that reasoning in the same words (ModuleConformanceTestsBaseTests.cs:81-:85, AnonymousEndpointTestsBaseTests.cs:10-:11). [Rubric §14 - Testability] assesses whether the guardrails themselves are trustworthy; this is the fixture shape that earns that trust. Compare NavigatingSpec, the same negative-fixture technique applied to a specification rule, and ProbeTests, which parameterizes the drift instead of hard-coding it.
Type File:Line What is deliberately wrong
DriftedTests (in AnonymousEndpointTestsBaseTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:100 Points TargetAssemblies at this test assembly (:102-:103), so the scan finds the fixture controllers' [AllowAnonymous] attributes, and then supplies an empty AllowedAnonymousEndpoints (:105). Every discovered endpoint is therefore an offender, and the failure message must name AnonymousFixtureController.
DriftedTests (in ModuleConformanceTestsBaseTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:131 Three overrides, each wrong in a different way against FakeDependentModule's real declarations: ExpectedName => "NotTheDeclaredName" (:133) against the module's "FakeDependent"; ExpectedDependencies => ["FakeLeaf"] (:135) against the module's two-entry list, so one dependency is missing; and ExpectedRequiresDependencies => false (:137) against the module's true. AssertDisabledStubs is deliberately not overridden, so the fourth inherited fact stays vacuous here.
  • Why they're built this way - one wrong value per assertion, and no more. If a single drifted subclass were wrong in three ways at once and the base only ever threw on the first, the other two assertions could rot undetected; the module-side fixture avoids that by being driven three separate times, once per fact.
  • Where they're used - the anonymous-endpoint one drives Base_Fails_WhenAnAnonymousEndpointIsNotAllowListed (AnonymousEndpointTestsBaseTests.cs:15-:24); the module one is instantiated three times by ModuleConformanceTestsBaseTests (:91,:99,:107), once per assertion under proof.

FakeArchitectureMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/LayerDependencyOverrideTests.cs:127 · Level 4 · class

  • What it is - a map that declares exactly the module and layer pairs a test hands it, with no relation to any real assembly layout. Its only purpose is to let the map-completeness rule's arithmetic be tested directly.
  • Depends on - ArchitectureMapBase (private sealed class FakeArchitectureMap(params IEnumerable<LayerRef>[] modules) : ArchitectureMapBase, LayerDependencyOverrideTests.cs:127) and LayerRef.
  • Concept - cross-references the map concept from CommonArchitectureMap. What is different here is that the map is data, not a description of reality: a primary constructor takes any number of LayerRef sequences and DefineLayers() simply flattens them (:131). Every LayerRef points at this same test assembly with a fabricated root namespace of the form MMCA.Fake.{module}.{layer} (:123-:124), and RepoToken is "MMCA.Fake" (:129), so nothing about the entries can be confused with the framework's own map. The class doc says why that is legitimate (:14-:16): the point under test is the rule's arithmetic over module names and layers, not any particular assembly, so a fixture map is more honest than borrowing a real one.
  • Walkthrough - the test class supplies the data through two helpers rather than the map: Full(module) yields a LayerRef for all five layers, and Thin(module) yields only Shared, Application and Api (:117-:121), which is the shape of a module with no aggregate and no persistence. Ref(module, layer) (:123-:124) builds each entry.
  • Why it's built this way - a real map cannot be made to declare a module that is missing a layer without deleting an assembly, which is not something a test can arrange. Constructing the map from literal layer lists turns "module Notification declares no Domain assembly" into a one-line fixture.
  • Where it's used - constructed once per fact in LayerDependencyOverrideTests (:26,:39,:59,:80,:99,:109).

FitnessPrincipal

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:57 · Level 4 · class

  • What it is - a throwaway "principal" entity used only as test data for the specification-navigation fitness function. It is the entity that a dependent record points at, so a specification can be written that tries to reach across the navigation into it.
  • Depends on - AuditableBaseEntity<TIdentifierType> (it is a public sealed class FitnessPrincipal : AuditableBaseEntity<int>, SpecificationFitnessTests.cs:57).
  • Concept introduced - test fixture entities for a fitness function. A fitness function is an executable architecture rule (see ArchitectureRules); to prove such a rule actually fires you feed it a deliberately-crafted model rather than the real domain. FitnessPrincipal is one half of that crafted model: a bare entity carrying a single scalar (IsActive, SpecificationFitnessTests.cs:59) so a specification can navigate to it. [Rubric §14 - Testability] assesses whether rules are provable with focused inputs; this fixture exists precisely so the guard is tested against a known-unsafe shape instead of hoping a real specification trips it.
  • Walkthrough - one auto-property, bool IsActive (:59). That is the only member; identity and audit fields come from the base. It is the navigation target referenced by FitnessDependent.Principal. Deriving from AuditableBaseEntity<int> is load-bearing rather than cosmetic: the rule's navigation walker only counts a property as an entity navigation when its type (or its collection element type) inherits the auditable entity base (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:121-:138).
  • Why it's built this way - the fitness test must be non-vacuous: it needs a real cross-entity navigation to flag. A minimal principal with a single scalar is the smallest thing a specification can legally navigate into.
  • Where it's used - referenced by FitnessDependent and, through it, by NavigatingSpec and NavigatingQuerySpec; the whole fixture set drives SpecificationFitnessTests.

FixtureAssemblyMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · (see per-type table) · Level 4 · class

The name is used four times in this assembly, once per IL-reading rule under proof, and all four are the same one-entry map: this test assembly registered as a single framework layer, so the rule's whole-layer scan lands on the fixtures. They are nested in different outer classes, so their full names differ and none is visible outside its own file.

  • Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
  • Concept introduced - the layer a fixture map declares is not decoration, it is the scope selector. Each of these four rules starts from a different part of the map, so each map has to name the matching layer or the rule reads nothing and passes vacuously. The cascade and hard-delete rules walk every assembly the map registers, through ScannableAssemblyLocations (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:88-:92), so their layer choice is nominal; the domain-throw rule reads map.OfLayer(Layer.Domain) and nothing else (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:107-:111), so registering Domain is mandatory there. All four use Framework(...) rather than Module(...), which is the opposite choice from FixtureModuleMap and correct for the same reason: these rules do not filter on module ownership, so the simplest entry is the honest one. Reading assemblies off disk by Assembly.Location also explains a shared limitation: a rule in this family cannot inspect a dynamic or single-file assembly, and the helper silently skips any location that does not exist on disk. [Rubric §14 - Testability] assesses whether a guardrail is provable with focused inputs, and a map that scopes the scan to fixtures is what makes each of these four rules provable at all.
  • Walkthrough - each declares RepoToken => "MMCA.Common" and a single-entry DefineLayers() naming one layer and one anchor type from the fixture namespace it serves. Anchoring by typeof(SomeFixture).Assembly rather than by Assembly.GetExecutingAssembly() is deliberate: it ties the map to a type a compiler would fail on if the fixtures moved.
Type File:Line Layer and anchor The rule it scopes
FixtureAssemblyMap (in CascadeSoftDeleteFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:97 Framework(Layer.Domain, typeof(CascadeChildFixture).Assembly) (:103) AggregatesCascadeSoftDeleteToChildren.
FixtureAssemblyMap (in SoftDeleteEnforcementFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementFitnessTests.cs:77 Framework(Layer.Infrastructure, typeof(FixtureEntity).Assembly) (:83) HardDeletesOnlyInAllowedTypes.
FixtureAssemblyMap (in DomainEventHandlerSaveFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:110 Framework(Layer.Application, typeof(FixtureDomainEvent).Assembly) (:116) DomainEventHandlersDoNotSave.
FixtureAssemblyMap (in DomainThrowFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DomainThrowFitnessTests.cs:138 Framework(Layer.Domain, typeof(NonThrowingFixture).Assembly) (:144) DomainThrowsOnlyArgumentGuards, which reads the Domain layer only, so this entry is load-bearing.
  • Why they're built this way - four nested six-line classes rather than one shared fixture map, so each test file is readable end to end and no test can change another's scope by editing a shared file. It is the same trade-off FixtureModuleMap makes.
  • Caveats / not-in-source - because each map registers the whole test assembly, a rule in this family sees every type in it, not just the fixtures in its own namespace. Three of the four are unaffected because their fixtures are the only matching shapes present; the domain-throw rule is not, which is why it carries an explicit allowlist of the assembly's non-fixture throws (DomainThrowFitnessTests.cs:29-:33).
  • Where they're used - constructed once as a readonly field per test class (CascadeSoftDeleteFitnessTests.cs:21, SoftDeleteEnforcementFitnessTests.cs:19, DomainEventHandlerSaveFitnessTests.cs:21, DomainThrowFitnessTests.cs:35) and passed to every rule call in each.

FixtureEntity

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.HardDeleteFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:11 · Level 4 · class

  • What it is - the entity type the hard-delete fixtures erase, keep, and soft-delete. It is the T of the DbSet<T>, IQueryable<T> and List<T> those fixtures operate on.
  • Depends on - AuditableBaseEntity<TIdentifierType> closed over int (HardDeleteFixtures.cs:11, import at :2).
  • Concept introduced - soft-delete as the framework's deletion model, and the EF members that bypass it. An entity sets IsDeleted = true and the global query filter hides it, so the row survives for audit, restore and erasure accounting; a hard delete bypasses all of that (rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:26-:30). HardDeletesOnlyInAllowedTypes (:62) reads every scannable assembly's IL and reports any call to one of four EF Core member names, Remove, RemoveRange, ExecuteDelete and ExecuteDeleteAsync (:10-:11), from a type outside the repo's allowlist. The interesting half is the disambiguation. Remove is an ordinary collection method name, so it only counts when the declaring type is one of four EF entity-set types, DbContext, DbSet<T>, InternalDbSet<T,T> or LocalView<T> (:18-:24,:138-:139); ExecuteDelete and ExecuteDeleteAsync are unambiguous wherever EF declares them, so any declaring type under the Microsoft.EntityFrameworkCore prefix counts (:130,:138). Without that scoping the rule would fire on every dictionary and cache in the repo, which is the version of this idea that gets deleted after a week. [Rubric §8 - Data Architecture] assesses whether the storage model's deletion semantics hold everywhere; [Rubric §30 - Compliance/Privacy/Data Governance] is why it matters, since an erased row silently drops out of audit and erasure accounting (ADR-005); [Rubric §14 - Testability] covers the fixtures.
  • Walkthrough - one member, public string Name { get; set; } = string.Empty; (:13). It is public (unlike the fixtures around it) because it appears in the public signatures of the static fixture methods and is the anchor type the self-test's map resolves the assembly from (SoftDeleteEnforcementFitnessTests.cs:83).
  • Where it's used - the entity parameter of all five members across DbSetRemovingFixture and family, and the map anchor of SoftDeleteEnforcementFitnessTests.

FixtureModuleMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts and MMCA.Common.Architecture.Tests.Cqrs · (see per-type table) · Level 4 · class

The name is used twice in this assembly, once per catalog-style rule under proof, and both are the same one-entry map: this test assembly registered as a module Application layer so a consumer-facing rule sees the fixtures. They are nested in different outer classes, so their full names differ.

  • Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
  • Concept introduced - why a self-test sometimes has to pretend to be a consumer. Several rules deliberately read only the per-module assemblies and skip framework layers, because a framework's own types are not the consumer's catalog: the error-catalog rules take ModuleDomain() plus ModuleApplication() (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.ErrorCatalog.cs:38-:44), and the validator-coverage rule takes ModuleApplication() alone, mirroring where the framework's AddValidatorsFromAssembly scan looks (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:88-:89). A map built from Framework(...) entries has Module set to the empty string (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:94-:95) and the module-layer lookup keeps only entries with l.Module.Length > 0 (:101-:102), so ModuleApplication() returns nothing and both rules would pass having read nothing at all. Using Module("Tickets", ...) is therefore not decoration: it is the one thing that puts the fixtures in scope. Compare FixtureAssemblyMap, whose rules read whole layers and which uses Framework(...) for the opposite reason, and FakeConsumerMap, which flips the same switch to prove a rule excludes framework types.
  • Walkthrough - both declare RepoToken => "MMCA.Common" and a single-entry DefineLayers() returning Module("Tickets", Layer.Application, <anchor>.Assembly). The module name "Tickets" is arbitrary except that the error-catalog self-test's prefix delegate closes over the same word (ErrorCatalogFitnessTests.cs:100), which is how a consumer's real prefix convention is modelled.
Type File:Line The rule it scopes
FixtureModuleMap (in CommandValidatorCoverageFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:115 Anchored on typeof(CreateTicketCommand).Assembly (:121). Its doc records the mirroring intent: the rule reads commands and validators from the per-module Application assemblies, exactly where the framework's validator scan looks (:110-:114).
FixtureModuleMap (in ErrorCatalogFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ErrorCatalogFitnessTests.cs:106 Anchored on typeof(TicketErrors).Assembly (:112). Its doc notes that the catalog rules read the per-module Domain and Application assemblies and deliberately skip framework layers (:102-:105).
  • Why they're built this way - each is nested private sealed inside the test class that uses it, so neither is visible to the other and neither can be mistaken for a repo map. Duplicating eight lines is cheaper than a shared fixture map whose meaning would have to be read from another file.
  • Where they're used - constructed once as a readonly field per test class (CommandValidatorCoverageFitnessTests.cs:20, ErrorCatalogFitnessTests.cs:19) and passed to every rule call in each.

StaleAllowListTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:108 · Level 4 · class

  • What it is - the adversarial fixture for the stale-entry half of the anonymous-endpoint gate: a subclass whose allow-list names an endpoint that does not exist, so the base's AllowList_HasNoStaleEntries fact must report it rather than shrug.
  • Depends on - AnonymousEndpointTestsBase (private sealed class StaleAllowListTests : AnonymousEndpointTestsBase, AnonymousEndpointTestsBaseTests.cs:108).
  • Concept introduced - an allow-list rots in two directions, and only one of them is obvious. A missing entry fails loudly the moment a new [AllowAnonymous] lands. A stale entry fails silently and permanently: the endpoint gets renamed, re-gated with [Authorize], or deleted, and the list keeps granting a permission that is no longer being requested. Nothing breaks, so nobody looks, and the next endpoint that happens to match that identifier inherits an approval nobody granted it. The base therefore runs the comparison both ways, computing stale as the allow-list entries with no match in the scanned set (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:83-:89), with a because that spells out the consequence: an entry that no longer matches hides a renamed or re-gated endpoint behind a permission that is no longer being granted (:88). [Rubric §11 - Security] is the property; [Rubric §15 - Best Practices & Code Quality] is the mechanism, since a self-pruning list is one that stays readable.
  • Walkthrough - TargetAssemblies is this test assembly (:110-:111), so the fixture controllers are discovered normally. AllowedAnonymousEndpoints holds exactly one entry, "MMCA.Common.Architecture.Tests.NoLongerAnonymousController.ReadAsync" (:113-:114), naming a controller that has never existed. That makes the fixture fail both facts at once (every real fixture endpoint is now an unlisted offender as well), which is harmless because the fact under proof invokes only AllowList_HasNoStaleEntries as a delegate.
  • Where it's used - the subject of Base_Fails_WhenTheAllowListHasAStaleEntry (AnonymousEndpointTestsBaseTests.cs:26-:35), which asserts the message contains "NoLongerAnonymous" (:33), the distinctive fragment of the phantom identifier.

ModuleConformanceTestsBaseTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:86 · Level 5 · class

  • What it is - the meta-test for the shipped per-module conformance base: five facts that prove each of the base's assertions actually fails on the drift it claims to catch, that the base reaches IModule's default implementations for a leaf module, and that its disabled-stub hook is harmlessly vacuous when a subclass does not override it.
  • Depends on - ModuleConformanceTestsBase<TModule> (the type under test), its own fixtures DriftedTests, FakeLeafModuleConformanceTests, FakeLeafModule and FakeDependentModule, plus xUnit [Fact] and AwesomeAssertions' Should().Throw<Exception>() / NotThrow() delegate assertions.
  • Concept introduced - testing a shipped test base, from the outside, by invoking its facts as delegates. The base lives in the MMCA.Common.Testing.Architecture package and is consumed by five subclasses across MMCA.ADC and MMCA.Store, so a regression in it would surface as green consumer suites that assert nothing. This class converts each inherited [Fact] into a method group (var assert = new DriftedTests().Module_ShouldDeclare_ExpectedName;, :91) and asserts on the delegate's behavior, which is what lets one xUnit test observe another test method failing. [Rubric §14 - Testability] assesses whether guardrails are themselves trustworthy; [Rubric §34 - Architecture Governance & Documentation] is the reason the guardrail exists at all (ADR-015 makes module conformance build-gating); and [Rubric §33 - Developer Experience] covers the failure mode being prevented, a silent break that would only appear in a downstream repo after a release (ADR-058).
  • Walkthrough
    • Three adversarial facts, one per assertion the base makes, each pointing at DriftedTests and asserting the delegate throws: Base_Fails_WhenTheNameDrifts (:88-:94), Base_Fails_WhenADependencyIsMissing (:96-:102), and Base_Fails_WhenRequiresDependenciesDrifts (:104-:110).
    • Base_ReadsDefaultInterfaceImplementations_ForALeafModule (:112-:121): instantiates FakeLeafModuleConformanceTests and calls the two inherited assertions directly (:119-:120). Passing proves the base reached IModule's defaults, because the leaf fixture declares neither member (inline comment, :115-:116).
    • Base_DisabledStubHook_IsVacuous_ByDefault (:123-:129): asserts the fourth inherited fact does not throw for a module that exports nothing, so a leaf subclass never has to override AssertDisabledStubs (the base's default is an empty body, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleConformanceTestsBase.cs:75-:77).
    • The nested private sealed class DriftedTests (:131-:138) closes the file; being private is what keeps xUnit from collecting its three deliberately-failing inherited facts as tests of their own (class doc, :81-:85).
  • Why it's built this way - the three module members the base asserts on are the entire contract ModuleLoader registers on: it topologically orders on Dependencies, matches ModulesSettings entries by Name, and chooses between a hard start failure and stub registration from RequiresDependencies (the base's own because clauses spell each one out, ModuleConformanceTestsBase.cs:40,:52,:61). None of those drifts throws at build time, so the fitness base is the only guard, and this class is the guard on the guard (ADR-059).
  • Where it's used - an independent class in the Common architecture suite. The base it protects is subclassed by five real module tests: MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.API.Tests/NotificationModuleTests.cs:8, MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.API.Tests/IdentityModuleTests.cs:5, MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.API.Tests/SalesModuleTests.cs:5, MMCA.Store/Tests/Modules/Identity/MMCA.Store.Identity.API.Tests/IdentityModuleTests.cs:5, and MMCA.Store/Tests/Modules/Catalog/MMCA.Store.Catalog.API.Tests/CatalogModuleTests.cs:5.
  • Caveats / not-in-source - the three adversarial facts assert only that an exception is thrown (Should().Throw<Exception>(), :93,:101,:109), not which assertion produced it or what its message says. They prove the base is not vacuous; they do not pin its failure text.

AnonymousEndpointTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTests.cs:14 · Level 5 · class

  • What it is - the framework's own anonymous-endpoint allow-list: the live, reviewed statement of every controller action MMCA.Common ships without an authorization gate. Six entries today, all of them credential-exchange or credential-recovery endpoints.
  • Depends on - AnonymousEndpointTestsBase (public sealed class AnonymousEndpointTests : AnonymousEndpointTestsBase, AnonymousEndpointTests.cs:14), ApiControllerBase as the anchor for the API assembly (:18), and UISharedAssemblyReference as the anchor for the shared UI assembly (:19).
  • Concept introduced - an allow-list as a review artifact, not a suppression list. The gate's value is not that it blocks [AllowAnonymous]; it is that adding one becomes a line in a test file with a comment next to it, landing in a diff that a human reads. Every entry here carries its justification inline, and each justification is the same argument: requiring a token would be circular, because minting or recovering the token is what the endpoint does. Login, register and refresh mint or rotate the token pair (:24-:28); the OAuth completion code is the caller's only credential at that point and is single-use, burned on first exchange (:29-:31); forgot-password and reset-password are for a caller who has lost the credential, are throttled by the same auth-ip rate-limit policy, and forgot-password always answers 202 so it reveals nothing about which addresses hold accounts (:32-:34). [Rubric §11 - Security] assesses the deliberateness of the authorization posture; note that the compensating control for every entry is rate limiting rather than authentication (ADR-029). [Rubric §9 - API & Contract Design] applies because the anonymous surface is part of the published contract, and [Rubric §34 - Architecture Governance & Documentation] because the reasoning lives in compiled code rather than a wiki page.
  • Walkthrough
    • TargetAssemblies (:16-:20) names two assemblies by anchor type, so the scan covers both the controllers and the routable Blazor pages the framework ships.
    • AllowedAnonymousEndpoints (:22-:39) is the six-entry list. Two of them are worth reading closely: PasswordResetAuthControllerBase`2.ForgotPasswordAsync and its reset sibling (:37-:38) carry a backtick-2 arity suffix, because PasswordResetAuthControllerBase<TForgotPasswordCommand, TResetPasswordCommand> is generic over the app's command records and reflection renders a generic type's FullName that way. The comment above them says exactly that (:35-:36), which is the kind of note that saves the next person an hour.
    • MinimumScannedTypes => 21 (:43) raises the base's default floor of 1 to this repo's known count, described in the comment as a floor and not an equality: 12 API controller types plus the routable UI pages (:41-:42). Removing a scanned type is therefore a failure rather than a quietly smaller scan.
  • Why it's built this way - the endpoints that cannot require a token are exactly the ones an attacker reaches first, so the framework's position is that they must be enumerated, justified, and rate-limited rather than merely working. The base's own doc records the one thing this gate cannot see: minimal-API endpoints opt out through the .AllowAnonymous() builder call, which is endpoint metadata produced at map time and invisible to static reflection (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:18-:24). The framework's minimal-API anonymous surface (JWKS, OIDC discovery, app-association, session-cookie refresh, health) is deliberate and small, but it is not covered here.
  • Where it's used - collected and run by xUnit in the Common architecture suite; its three facts come entirely from the base. Consumers write their own subclass with their own list.
  • Caveats / not-in-source - the minimal-API blind spot above is stated in the base's documentation, and closing it would need an endpoint-metadata check over a built host. Nothing in this class compensates for it.

CancellationTestMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CancellationTokenFitnessTests.cs:63 · Level 5 · class

  • What it is - a one-layer architecture map used only by CancellationTokenFitnessTests: it registers this test assembly as the map's single Application layer so the cancellation-token rule scans the fixture services and nothing else.
  • Depends on - ArchitectureMapBase (private sealed class CancellationTestMap : ArchitectureMapBase, CancellationTokenFitnessTests.cs:63), the Layer enum, and LayerRef.
  • Concept - cross-references the map concept from CommonArchitectureMap. The narrowing matters twice over here. First, the cancellation rule only visits the Application and Infrastructure layers (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CancellationTokens.cs:45-:47), so registering the test assembly as Application is what puts the fixtures in scope at all. Second, the rule's automatic exemption compares against map.Layers.Select(l => l.Assembly) (:42), so a map holding only this assembly is what makes IAsyncEnumerator<T> (declared in the BCL, outside the map) count as an externally-fixed contract. A wider map would not change that result here, but the single entry keeps the fixture's meaning unambiguous.
  • Walkthrough - RepoToken => "MMCA.Common" (:65) and a one-entry DefineLayers() returning Framework(Layer.Application, typeof(CancellationTokenFitnessTests).Assembly) (:67-:68).
  • Where it's used - constructed on every call of the test class's private RunRule helper (:56).

CancellationTokenFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CancellationTokenFitnessTests.cs:12 · Level 5 · class

  • What it is - the meta-test for the trailing-CancellationToken rule: three facts that prove it flags a missing, misplaced, or misnamed token, leaves compliant and out-of-scope members alone, honors the caller-supplied exemption list, and never flags a signature the repo does not own.
  • Depends on - ArchitectureRules (the rule under test, CancellationTokenFitnessTests.cs:55), its own CancellationTestMap (:56), the six fixture services in MMCA.Common.Architecture.Tests.CancellationFixtures (:1), and externals xUnit [Fact] plus AwesomeAssertions' Should().Throw<Exception>().Which (:59).
  • Concept introduced - asserting on a rule's failure message rather than on whether it threw. Every fixture in this file is deliberately in scope at once, so the rule always throws; a bare "it threw" assertion would prove nothing. Instead the private RunRule helper (:53-:60) captures the exception's message once and each fact makes positive and negative Contain assertions against that single string. The result is one rule execution that proves six independent behaviors, and every assertion carries a because clause spelling out the ruling it pins (:19-:24). [Rubric §14 - Testability] assesses whether guardrails are themselves trustworthy; this is the technique that makes a multi-branch classifier provable in three facts. [Rubric §12 - Performance & Scalability] is the property being defended, as taught in the fixture family section.
  • Walkthrough
    • Rule_FlagsMissingToken_ButNotCompliantOrNonAsyncMembers (:14-:25): asserts both MissingTokenFixtureService.RunAsync and .PingAsync appear (:19-:22), and that neither CompliantFixtureService nor the internal HiddenAsync does (:23-:24).
    • Rule_FlagsMisplacedAndMisnamedTokens (:27-:36): asserts the two offender method names appear together with the rule's exact classifier strings, "must be the LAST parameter" (:33) and "must be named 'cancellationToken'" (:35), so the message a developer will actually read is pinned, not just the fact of a failure.
    • Rule_HonorsExemptions_AndSignaturesTheRepoDoesNotOwn (:38-:51): runs the rule twice. Without exemptions, ExemptableFixtureService.LegacyAsync is reported (:41-:43); with that one entry passed in (:45), it disappears (:47). The same run also asserts MoveNextAsync never appears (:48-:50), which is the automatic exemption rather than the explicit one.
  • Why it's built this way - the rule is one of the framework's broadest (it judges every public awaitable method in two layers), so its false-positive behavior matters as much as its true-positive behavior. Pinning the message text also protects the developer experience: the classifier's wording is what tells someone how to fix the build. See ADR-015.
  • Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of the same rule is CancellationTokenConventionTests.

CommandValidatorCoverageFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:16 · Level 5 · class

  • What it is - the meta-test for the command-validation coverage rule: eight facts that pin each coverage shape, both escape hatches, and the inventory count that keeps the whole gate from passing vacuously.
  • Depends on - ArchitectureRules (CommandsHaveValidators and HandledCommandCount, CommandValidatorCoverageFitnessTests.cs:25,:103), its nested FixtureModuleMap, the thirteen types in MMCA.Common.Architecture.Tests.CommandValidatorFixtures (:1), and Xunit.Sdk.XunitException (:3), which every fact asserts on by exact type rather than as a bare Exception.
  • Concept introduced - proving a rule and proving it is not vacuous, in the same class. Seven of the eight facts read the failure message of a rule that always throws, the same technique CancellationTokenFitnessTests uses. The eighth is different in kind: CommandInventory_CountsTheDataCarryingCommands calls HandledCommandCount and pins it at exactly 5 (:100-:108). That number exists because a map that resolved to no module Application assemblies would make every other fact in the class pass while reading nothing, and the same counter is what CommandValidatorCoverageTestsBase exposes to consumers as their own non-vacuity guard (rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:73-:77). Pinning an exact count rather than a floor is affordable here because the fixture set is closed; a consumer uses a floor. [Rubric §14 - Testability] and [Rubric §24 - Forms/Validation/UX Safety] apply.
  • Walkthrough - one private const string FixtureNamespace (:18) and one readonly FixtureModuleMap (:20) are shared by every fact.
    • CommandWithNoValidation_IsFlagged (:22-:31) and CommandWithItsOwnValidator_IsCovered (:33-:42): the offender and the direct-coverage case, asserted by Contain and NotContain on the one message.
    • CommandCoveredThroughTheRequestBridge_IsCovered (:44-:53) and BridgeWithNoRequestValidator_IsNotCoverage (:55-:64): the two halves of the bridge, with the second because stating exactly why a registered CommandRequestValidator is not coverage on its own (:63).
    • PayloadFreeCommand_IsSkipped (:66-:75): the marker command, absent from the report because it has no settable property to validate.
    • AllowlistedCommand_IsExempt (:77-:89): passes one fully-qualified command name and asserts that command disappears while the other offender is still reported (:87-:88). That second assertion is what distinguishes a working exemption from a rule that stopped firing.
    • AllowlistedNamespace_SilencesTheRule (:91-:98): passes the fixture namespace and asserts NotThrow, the only fact in the class where the rule is quiet.
    • CommandInventory_CountsTheDataCarryingCommands (:100-:108): the count, with the because enumerating the five commands by name.
  • Why it's built this way - the escape hatch has two granularities (a type and a namespace) and both need proof, because a repo records "this command genuinely has nothing to check" at whichever level fits. Asserting on XunitException specifically rather than Exception means an unexpected failure inside the rule (a null reference, a bad cast) fails the fact instead of being mistaken for the rule firing. See ADR-015.
  • Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of the same rule is CommandValidatorCoverageTestsBase, which MMCA.Common itself does not subclass because it declares no modules.

CycleTestMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/NamespaceCycleFitnessTests.cs:52 · Level 5 · class

  • What it is - a one-layer map for the namespace-cycle fitness test, rooted at the CycleFixtures namespace so the rule sees only the three fixture namespaces and none of the real test code.
  • Depends on - ArchitectureMapBase (private sealed class CycleTestMap : ArchitectureMapBase, NamespaceCycleFitnessTests.cs:52), LayerRef, and the Layer enum.
  • Concept introduced - constructing a LayerRef directly to override the derived root namespace. Every other map in this chapter uses the Framework(layer, assembly) helper, which computes the layer's root namespace as MMCA.Common.{segment} (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:94-:95). That is wrong here, because the cycle rule groups types by the namespace segment directly beneath LayerRef.RootNamespace (ArchitectureRules.Cycles.cs:12-:16) and skips types outside it entirely: with a derived root the fixtures would not be grouped into Left / Right / Acyclic nodes at all. So this map builds the LayerRef by hand with FixtureRoot as the fourth argument (:58-:62), which is the one place in the suite where the record's positional constructor (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:31) is used directly. [Rubric §14 - Testability] is the theme: scoping the map is what keeps the assertion about the fixtures instead of about whatever else the test assembly happens to contain.
  • Walkthrough - RepoToken => "MMCA.Common" (:54) and a DefineLayers() returning a single new LayerRef(string.Empty, Layer.Application, typeof(NamespaceCycleFitnessTests).Assembly, FixtureRoot) (:56-:63), where FixtureRoot is the class constant "MMCA.Common.Architecture.Tests.CycleFixtures" (:13). The empty first argument is the module name, which is what marks the layer as framework-owned rather than module-owned.
  • Where it's used - constructed once per fact in NamespaceCycleFitnessTests (:18,:32,:42).

DataSubjectSample

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiErasureContractFitnessTests.cs:79 · Level 5 · class

  • What it is - a representative "data subject" fixture: a single object that carries [Pii] members alongside non-PII fields and implements an in-place erasure path, so the framework's privacy machinery can be exercised end to end against a realistic shape.
  • Depends on - IAnonymizable (it is private sealed class DataSubjectSample : IAnonymizable, PiiErasureContractFitnessTests.cs:79), PiiAttribute (marks Email and FullName, :91,:94), and Result (returned from Anonymize, :99).
  • Concept introduced - closing a vacuous fitness function with a stand-in data subject. The framework's [Pii] => IAnonymizable scan (PiiConventionTests) has nothing to assert because MMCA.Common ships no PII-bearing domain entity of its own. Rather than invent a fake aggregate in the Domain layer, this fixture models the exact contract a consumer PII aggregate (for example MMCA.ADC's User) must satisfy, and lets PiiErasureContractFitnessTests prove the three §30 mechanisms compose. [Rubric §30 - Compliance/Privacy/Data Governance] assesses whether erasure, redaction, and masking actually work together; this sample is the vehicle that keeps the framework's proof of that non-vacuous.
  • Walkthrough - public constants publish the expected before and after values so the tests can assert without magic literals: SampleId = 7, PublicCity = "Atlanta", OriginalEmail, OriginalFullName (:81-:84), plus private anonymized placeholders (:86-:87). Id (:89) and City (:97) are non-PII init pass-through fields; Email and FullName are [Pii] with private setters (:91-:95), which is what lets Anonymize rewrite them in place while no caller outside the type can. Anonymize() (:99-:105) overwrites both PII fields with the fixed placeholders and returns Result.Success(); because it re-applies constants, calling it twice is idempotent by construction (:101).
  • Why it's built this way - ADR-005 (soft-delete versus erasure) requires that a right-to-erasure path be idempotent and leave no clear text behind. The fixture encodes that contract in the smallest object that can be pushed through PiiRedactor and Anonymize together.
  • Where it's used - the sole fixture for PiiErasureContractFitnessTests.

DbSetRemovingFixture, ExecuteDeletingFixture, SoftDeletingFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.HardDeleteFixtures · (see per-type table) · Level 5 · class

Three internal static classes holding five one-line methods between them: three that erase rows through EF Core and two that do not. They are the compiled call sites the hard-delete rule has to tell apart.

  • Depends on - Microsoft.EntityFrameworkCore for DbSet<T> and the ExecuteDeleteAsync queryable extension (HardDeleteFixtures.cs:1) and FixtureEntity as the element type.
  • Concept - cross-references the hard-delete concept introduced by FixtureEntity. What this set proves is the disambiguation, which is the whole engineering content of the rule. SoftDeletingFixture.Forget calls List<FixtureEntity>.Remove (:41), which is the same method name the rule bans, on a type the rule must ignore. Its own doc says why that matters (:32-:36): Remove on a List<T> is what makes the naive "ban the name Remove" version of this rule useless. Having a genuine List<T>.Remove compiled next to a genuine DbSet<T>.Remove is what turns the declaring-type check from an implementation detail into a proven behavior.
  • Walkthrough - the rule reports each site as {type.FullName}.{method.Name} calls {DeclaringType.Name}.{callee.Name} (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:115), so the failure text names both ends. The allowlist is checked at the type level before any method is walked (:99-:102), which is why an entry silences a whole type rather than one call.
Type File:Line What it models
DbSetRemovingFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:17 Two erasing members in one type: Purge calls DbSet<T>.Remove (:19) and PurgeMany calls DbSet<T>.RemoveRange (:21-:22). Both must be reported, which is how the test proves the rule walks every method rather than stopping at the first hit.
ExecuteDeletingFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:26 The server-side delete: PurgeAsync calls IQueryable<T>.ExecuteDeleteAsync (:28-:29). Kept in a separate type so the type-level allowlist can silence the DbSet fixture while this one still fails.
SoftDeletingFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:37 Two near-misses: Deactivate calls the framework's own entity.Delete() (:39), and Forget calls List<T>.Remove (:41). Neither is a hard delete, so the whole type name must be absent from the report.
  • Why they're built this way - two erasing types rather than one is what makes the type-level allowlist fact meaningful: exempting DbSetRemovingFixture must leave ExecuteDeletingFixture reported. Keeping all five methods to a single expression means the compiled IL contains the call under test and essentially nothing else.
  • Where they're used - scanned by SoftDeleteEnforcementFitnessTests through its FixtureAssemblyMap, which registers this assembly as the map's single Infrastructure layer.

ErrorCatalogFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ErrorCatalogFitnessTests.cs:15 · Level 5 · class

  • What it is - the meta-test for the two error-catalog rules: seven facts that pin the cross-type collision, the two-branch near-miss, the shared-code exemption on both rules, the unprefixed offender, the UNVERIFIABLE report for a dynamically-built code, and the distinct-code inventory.
  • Depends on - ArchitectureRules (ErrorCodesAreUnique, ErrorCodesUseAnAllowedPrefix and DistinctErrorCodeCount, ErrorCatalogFitnessTests.cs:24,:58,:92), its nested FixtureModuleMap, the six fixture catalogs (:1), and Xunit.Sdk.XunitException (:3).
  • Concept introduced - the third verdict: UNVERIFIABLE, and why a static analyser should have one. Most fitness functions are binary. These are not, because a code assembled at run time genuinely cannot be read out of the instruction stream, and both available binary answers are wrong: passing it hides a possible collision, failing it bans a legitimate pattern the framework never decided to ban. The rules take the third option and surface those sites in the failure message under an UNVERIFIABLE heading (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.ErrorCatalog.cs:151-:159), so the reader learns the catalog has a blind spot and where it is. DomainThrowFitnessTests pins the same verdict for the same reason on a different rule. [Rubric §14 - Testability] assesses whether a guardrail is honest about its own limits, which is exactly what a named third verdict is; [Rubric §9 - API & Contract Design] is the property.
  • Walkthrough - a private static readonly string[] SharedCodes = ["Error.NotFound"] (:17) stands in for a repo's allowed-shared-codes list, and IsTicketsCode (:100) is the prefix delegate, closing over the fixture module's name exactly as a consumer's would close over its real module names.
    • DuplicateCode_AcrossTwoTypes_IsFlagged (:21-:31): asserts the message names the code and both owning types (:28-:30), which is what makes a collision actionable.
    • CodeReusedAcrossBranchesOfOneType_IsNotADuplicate (:33-:42): the near-miss, absent from the report because uniqueness is measured per declaring type.
    • SharedCode_OnTheAllowList_IsExemptFromUniqueness (:44-:53) and PrefixedCodes_AndAllowedSharedCodes_PassThePrefixRule (:66-:76): the shared-code exemption proven against both rules, not just the one it was written for.
    • UnprefixedCode_IsFlagged (:55-:64): asserts both the code and the owning type appear (:62-:63).
    • DynamicCode_IsReportedAsUnverifiable (:78-:87): asserts the literal string "UNVERIFIABLE" and the owning type name are both present (:85-:86).
    • DistinctCodeCount_CountsTheLiteralCatalog (:89-:97): the non-vacuity guard, asserted as a floor of 5 with the five literal codes enumerated in the because (:94-:96). A floor rather than an equality, because the count is over literals the scan can read and a future fixture addition should not be a test failure.
  • Why it's built this way - both rules read the same collected sites, so proving them in one class against one fixture catalog keeps the two verdicts comparable, and each fact makes exactly one claim about the shared message. See ADR-015.
  • Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of both rules is ErrorCatalogTestsBase, whose default prefix delegate closes over the repo's own module names.

FitnessDependent

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:48 · Level 5 · class

  • What it is - the other half of the specification-navigation fixture: an entity that holds a foreign-key scalar and a navigation to a FitnessPrincipal, so both a safe (scalar-only) and an unsafe (navigating) specification can be written over it.
  • Depends on - AuditableBaseEntity<TIdentifierType> (public sealed class FitnessDependent : AuditableBaseEntity<int>, SpecificationFitnessTests.cs:48) and FitnessPrincipal (the Principal? navigation, :52).
  • Concept introduced - cross-references the fixture concept introduced by FitnessPrincipal. This type adds the parts a specification can filter on: PrincipalId (scalar FK, :50), a nullable Principal navigation (:52), and a Flag scalar (:54). The scalar-versus-navigation split is the whole point: it lets one fixture support both the pattern the rule must flag and the pattern it must leave alone.
  • Walkthrough - three auto-properties: int PrincipalId (:50), FitnessPrincipal? Principal (:52), bool Flag (:54). ScalarOnlySpec filters on PrincipalId and Flag; NavigatingSpec reaches through Principal.
  • Where it's used - the entity type parameter for all four fixture specifications in SpecificationFitnessTests: NavigatingSpec, ScalarOnlySpec, and the two query variants (NavigatingQuerySpec, ScalarOnlyQuerySpec).

FixtureAssemblyMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · (see per-type table) · Level 5 · class

The name is used four times in this assembly, once per IL-reading rule under proof, and all four are the same one-entry map: this test assembly registered as a single framework layer, so the rule's whole-layer scan lands on the fixtures. They are nested in different outer classes, so their full names differ and none is visible outside its own file.

  • Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
  • Concept introduced - the layer a fixture map declares is not decoration, it is the scope selector. Each of these four rules starts from a different part of the map, so each map has to name the matching layer or the rule reads nothing and passes vacuously. The cascade and hard-delete rules walk every assembly the map registers, through ScannableAssemblyLocations (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:87-:91), so their layer choice is nominal; the domain-throw rule reads map.OfLayer(Layer.Domain) and nothing else (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:106-:110), so registering Domain is mandatory there. All four use Framework(...) rather than Module(...), which is the opposite choice from FixtureModuleMap and correct for the same reason: these rules do not filter on module ownership, so the simplest entry is the honest one. Reading assemblies off disk by Assembly.Location also explains a shared limitation: a rule in this family cannot inspect a dynamic or single-file assembly, and the helper silently skips any location that does not exist on disk.
  • Walkthrough - each declares RepoToken => "MMCA.Common" and a single-entry DefineLayers() naming one layer and one anchor type from the fixture namespace it serves. Anchoring by typeof(SomeFixture).Assembly rather than by Assembly.GetExecutingAssembly() is deliberate: it ties the map to a type a compiler would fail on if the fixtures moved.
Type File:Line Layer and anchor The rule it scopes
FixtureAssemblyMap (in CascadeSoftDeleteFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:97 Framework(Layer.Domain, typeof(CascadeChildFixture).Assembly) (:103) AggregatesCascadeSoftDeleteToChildren.
FixtureAssemblyMap (in SoftDeleteEnforcementFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementFitnessTests.cs:77 Framework(Layer.Infrastructure, typeof(FixtureEntity).Assembly) (:83) HardDeletesOnlyInAllowedTypes.
FixtureAssemblyMap (in DomainEventHandlerSaveFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:110 Framework(Layer.Application, typeof(FixtureDomainEvent).Assembly) (:116) DomainEventHandlersDoNotSave.
FixtureAssemblyMap (in DomainThrowFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DomainThrowFitnessTests.cs:138 Framework(Layer.Domain, typeof(NonThrowingFixture).Assembly) (:144) DomainThrowsOnlyArgumentGuards, which reads the Domain layer only, so this entry is load-bearing.
  • Why they're built this way - four nested six-line classes rather than one shared fixture map, so each test file is readable end to end and no test can change another's scope by editing a shared file. It is the same trade-off FixtureModuleMap makes.
  • Caveats / not-in-source - because each map registers the whole test assembly, a rule in this family sees every type in it, not just the fixtures in its own namespace. Three of the four are unaffected because their fixtures are the only matching shapes present; the domain-throw rule is not, which is why it carries an explicit allowlist of the assembly's non-fixture throws (DomainThrowFitnessTests.cs:29-:33).
  • Where they're used - constructed once as a readonly field per test class (CascadeSoftDeleteFitnessTests.cs:21, SoftDeleteEnforcementFitnessTests.cs:19, DomainEventHandlerSaveFitnessTests.cs:21, DomainThrowFitnessTests.cs:35) and passed to every rule call in each.

IdempotencyFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:12 · Level 5 · class

  • What it is - the meta-test for the POST idempotency-intent gate: three facts that prove the rule names the undeclared controller, stays silent about the three declared ones, and skips abstract controllers entirely.
  • Depends on - ArchitectureRules (ArchitectureRules.PostActionsDeclareIdempotencyIntent, IdempotencyFitnessTests.cs:17), its nested IdempotencyTestMap and four controller fixtures, IdempotentAttribute and NonIdempotentAttribute (:2), plus Microsoft.AspNetCore.Mvc (:1), xUnit, and AwesomeAssertions.
  • Concept introduced - nameof instead of string literals in message assertions. Every assertion here is built from nameof(UndeclaredFitnessController) and friends (:21,:32,:35,:38,:48), so renaming a fixture is a compile-time event rather than a silently-passing test. Combined with the always-throwing design (CancellationTokenFitnessTests uses the same shape), it means the suite fails loudly on refactors instead of drifting. [Rubric §14 - Testability] and [Rubric §15 - Best Practices & Code Quality] both apply.
  • Walkthrough
    • Rule_FlagsThePostThatDeclaresNothing (:14-:23): calls the rule through an act delegate, captures the thrown exception, and asserts the message contains UndeclaredFitnessController.CreateThingAsync (:20-:22). Note it asserts the qualified member, so the fact fails if the rule reports a type without its offending action.
    • Rule_AcceptsDirectAndInheritedAndOptedOutDeclarations (:25-:40): three NotContain assertions covering the direct declaration (:31-:33), the inherited one (:34-:36), and the justified opt-out (:37-:39), each with a because clause stating the ruling.
    • Rule_SkipsAbstractControllers (:42-:50): asserts AbstractFitnessControllerBase is absent, which is the ConcreteClasses filter's doing, not an attribute's.
  • Why it's built this way - one deliberately-offending fixture means the rule always throws, so all three facts can read the same message shape without any of them having to construct a passing map. The gate itself exists because an undeclared POST is a duplicate-write incident waiting to happen (ADR-015 for the enforcement approach).
  • Where it's used - an independent class in the Common architecture suite; the shipped binding of the same rule over real code is IdempotencyConventionTests.

IdempotencyTestMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:52 · Level 5 · class

  • What it is - a one-layer map that registers this test assembly as the Api layer, which is the only layer the POST idempotency rule scans.
  • Depends on - ArchitectureMapBase (private sealed class IdempotencyTestMap : ArchitectureMapBase, IdempotencyFitnessTests.cs:52), the Layer enum, and LayerRef.
  • Concept - cross-references the map concept from CommonArchitectureMap. The layer choice is the whole configuration: PostActionsDeclareIdempotencyIntent starts from map.OfLayer(Layer.Api) (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:48), so pointing the Api layer at the test assembly is what substitutes the fixture controllers for the framework's real ones.
  • Walkthrough - RepoToken => "MMCA.Common" (:54) and a one-entry DefineLayers() returning Framework(Layer.Api, typeof(IdempotencyFitnessTests).Assembly) (:56-:57). The root namespace the helper derives (MMCA.Common.API) is unused by this rule, which scans whole assemblies rather than namespace-scoped subsets.
  • Where it's used - constructed once per fact in IdempotencyFitnessTests (:17,:28,:45).

LayerDependencyOverrideTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/LayerDependencyOverrideTests.cs:18 · Level 5 · class

  • What it is - six facts pinning the per-module escape on the map-completeness rule ModulesDeclareLayers: a thin module fails under the strict default, passes when the override names its own layers, and every other module stays strict while one is overridden.
  • Depends on - ArchitectureRules (ModulesDeclareLayers in both overloads, LayerDependencyOverrideTests.cs:30,:43), its nested FakeArchitectureMap, LayerRef and the Layer enum.
  • Concept introduced - why an escape hatch is per-module rather than a trimmed default. ModulesDeclareLayers is the rule that stops every other per-module layer rule from passing vacuously: it fails when a repo forgets to register a module's assembly in its map (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Layers.cs:82-:88). That creates a real problem for a deliberately thin module, one that owns no aggregate and no persistence, so no Domain and no Infrastructure assembly exists to register. Before the override existed there was exactly one way to express that: trim the default list, which silently stops enforcing those layers for every other module in the repo. One thin module would buy blanket permission to forget an assembly anywhere (rule doc, :96-:104). The three-argument overload (:113) instead takes a per-module dictionary and replaces the required list for the modules it names, leaving everything else at the strict default (:131-:137). Naming the exception keeps the weakening exactly where it is true. [Rubric §3 - Clean Architecture] assesses whether layer boundaries are declared and enforced; [Rubric §34 - Architecture Governance & Documentation] is the mechanism, since the exception ends up written in code with a reason next to it rather than silently absent; [Rubric §14 - Testability] is why an override needs its own test at all, since an escape hatch that is too wide is indistinguishable from a disabled rule.
  • Walkthrough - a static FiveLayers list (:20-:21) is the required set every fact starts from, and two helpers build maps from it: Full(module) yields all five layers, Thin(module) yields only Shared, Application and Api (:117-:121).
    • ModulesDeclareLayers_FailsForAThinModule_WhenEveryModuleIsHeldToTheFullList (:23-:34): the baseline, asserting the exact message "*module 'Notification' declares no Layer.Domain*" (:33).
    • ModulesDeclareLayers_PassesForAThinModule_WhenTheOverrideNamesItsOwnLayers (:36-:52): the same map with an override naming Notification's three real layers, asserted NotThrow.
    • ModulesDeclareLayers_KeepsEveryOtherModuleStrict_WhileOneIsOverridden (:54-:73): the load-bearing fact. Both modules are thin and only one is overridden, so the other must still fail (:71-:72). Its inline comment states the whole reason the override exists rather than a trimmed default list (:57-:58).
    • ModulesDeclareLayers_OverrideCanAlsoDemandMore_NotJustLess (:75-:92): overrides a full module with the five layers plus Layer.Ui and asserts it now fails (:87,:91). Pins that the override replaces rather than subtracts, so a repo can hold one module to a stricter bar than the rest.
    • ModulesDeclareLayers_TwoArgumentOverloadIsUnchanged (:94-:104) and ModulesDeclareLayers_NullOverrides_HoldEveryModuleToTheDefaultList (:106-:115): the two backward-compatibility facts, since the two-argument overload is what every current consumer calls (comment, :96-:98) and it forwards overrides: null (ArchitectureRules.Layers.cs:89-:90).
  • Why it's built this way - the map is a fixture rather than any real repo's, because the point under test is the rule's arithmetic over module names and layers, not any particular assembly (class doc, :14-:16). Asserting on the exact message with WithMessage rather than on a bare throw is what pins which module and which layer got reported, which is the only part of the output a developer acts on.
  • Where it's used - an independent class in the Common architecture suite. The rule it guards is called by LayerDependencyTestsBase (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/LayerDependencyTestsBase.cs:57), which passes the subclass's ModuleRequiredLayerOverrides; MMCA.Common's own LayerDependencyTests declares no modules, so the rule is vacuous there and the consumers are the real users.

NamespaceCycleFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/NamespaceCycleFitnessTests.cs:11 · Level 5 · class

  • What it is - the meta-test for the namespace-acyclicity rule: three facts that prove it reports the two-namespace fixture cycle, leaves the acyclic fixture namespace out of the report, accepts the cycle when the allowance covers every namespace on it, and still fails when the allowance covers only part of it.
  • Depends on - ArchitectureRules (ArchitectureRules.NamespacesHaveNoDependencyCycles, NamespaceCycleFitnessTests.cs:18), its nested CycleTestMap, the three fixture namespaces under CycleFixtures (LeftModelBase, LeftService, RightModel, AcyclicConsumer), and externals xUnit plus AwesomeAssertions.
  • Concept introduced - why a cycle allowance must cover the whole component, not just a name on the path. The rule takes an optional allowedCycleNamespaces list and skips a cycle only when every namespace on it is listed (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:39-:44,:61). The reason is subtle and worth internalizing: if a partial allowance sufficed, a fourth namespace joining an already-accepted tangle would slip in silently, because the reported shortest path might not even mention it (:57-:60). The third fact in this class is the executable proof of exactly that policy. [Rubric §15 - Best Practices & Code Quality] and [Rubric §7 - Microservices Readiness] are the properties defended; [Rubric §34 - Architecture Governance & Documentation] covers the allowance mechanism, which forces an accepted tangle to be written down in code rather than tolerated in review.
  • Walkthrough - a class constant FixtureRoot = "MMCA.Common.Architecture.Tests.CycleFixtures" (:13) is shared by the map and every assertion, so the namespace strings cannot drift apart.
    • Rule_FlagsTwoNamespaceCycle_ButNotAcyclicNamespaces (:15-:26): asserts the message contains both ...CycleFixtures.Left and ...CycleFixtures.Right (:21-:22) and not ...CycleFixtures.Acyclic (:23-:25), each with a because naming the edge that put it there.
    • Rule_Passes_WhenTheWholeCycleIsAllowed (:28-:36): passes both namespaces as the allowance and asserts NotThrow() (:31-:35). This is the only fact in the class where the rule does not throw.
    • Rule_StillFails_WhenOnlyPartOfTheCycleIsAllowed (:38-:46): passes only Left and asserts it still throws, with the reason stated inline as "a partial allowance must never hide a cycle" (:45).
  • Why it's built this way - the allowance is the rule's only escape hatch, and an escape hatch that can be misused silently is worse than none. Proving both its accepting and its refusing behavior is what lets the framework's own NamespaceCycleTests carry a real three-namespace allowance without weakening the gate.
  • Where it's used - an independent class in the Common architecture suite.

ProtoContractFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ProtoContractFitnessTests.cs:14 · Level 5 · class

  • What it is - the meta-test for the frozen-.proto contract gate: five facts that prove a pinned proto matches its committed snapshot, that a renumbered field and an added rpc are both reported, that a missing file fails loudly, and that the parser ignores the lines that are not part of the wire contract.
  • Depends on - ArchitectureRules (ProtoContractsMatchFrozenList, ProtoContractFitnessTests.cs:47, and BuildProtoContract, :93), ArchitectureMapBase (FindRepoRoot, :92), two fixture .proto files under TestData/, and externals xUnit plus AwesomeAssertions.
  • Concept introduced - a wire contract as a committed, sorted string snapshot. A .proto file is a published contract: a renumbered field silently breaks every deployed peer, because protobuf keys on the number and not the name. The rule renders each file into a canonical, ordered list of lines (one per rpc with its streaming flag, one per message field with its label, type and NUMBER, one per enum value) and compares that list to a frozen array checked into the test (:28-:42). Regenerating the snapshot is a deliberate act: you print BuildProtoContract and paste the result, which is what makes a contract change a reviewable diff rather than a silent one. [Rubric §9 - API & Contract Design] assesses whether published contracts are versioned and change-controlled; this is the gRPC half of that discipline, and it is the same technique the integration-event contract snapshot uses (IntegrationEventContractTestsBaseTests proves that side). [Rubric §7 - Microservices Readiness] applies because these are exactly the contracts that survive extraction (ADR-007).
  • Walkthrough - three constants locate the fixtures relative to the repo root: SolutionFileName = "MMCA.Common.slnx" (:16), the TestData directory (:18), and the pinned and drifted file arrays (:20,:22). The 12-line FrozenContract (:28-:42) is the snapshot, sorted so the comparison is order-independent for the author and deterministic for the diff.
    • Rule_PassesWhenTheProtoMatchesItsFrozenSnapshot (:44-:50): the positive case.
    • Rule_FlagsARenumberedField (:52-:64): asserts both sides of the change are reported, the new product_id = 7 line as unexpected (:58-:60) and the frozen product_id = 1 line as missing (:61-:63), which is what makes a renumbering legible in the failure output.
    • Rule_FlagsAnAddedRpc (:66-:74): asserts the added DeleteProduct rpc is named, because an addition peers have not been told about is still a contract change.
    • Rule_ReportsAMissingProtoFileExplicitly (:76-:87): a path typo must produce <missing proto file> does-not-exist.proto rather than silently pinning an empty contract, the classic way a snapshot gate rots into a no-op.
    • BuildProtoContract_IgnoresSyntaxImportAndOptionLines (:89-:98): resolves the repo root, builds the contract from the pinned file, and asserts it equals FrozenContract exactly and contains neither the csharp_namespace option nor the timestamp.proto import (:95-:97). Those lines are code-generation details, not wire contract, so churning them must not fail anyone's build.
  • Why it's built this way - MMCA.Common ships no protos of its own, so this is the framework's only exercise of a consumer-facing rule (class doc, :9-:12); consumers subclass ProtoContractTestsBase. The two fixture files differ by exactly the two breaking changes under test (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/TestData/fitness-catalog.proto versus fitness-catalog-drifted.proto), and the pinned one deliberately includes a nested message, a nested enum, a repeated field, an optional field, and a streaming rpc so the parser is exercised on every shape it claims to handle.
  • Where it's used - an independent class in the Common architecture suite. Neither fixture proto is compiled by any project.

SortableColumnFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/SortableColumnFitnessTests.cs:15 · Level 5 · class

  • What it is - the meta-test for the MudDataGrid sortable-column rule: four facts proving it reports a sortable TemplateColumn with its file and line, catches the same defect written as a Razor expression, stays silent on every near-miss, and fails loudly on a missing scan root.
  • Depends on - ArchitectureRules (SortableGridColumnsUsePropertyColumn, SortableColumnFitnessTests.cs:31), ArchitectureMapBase (FindRepoRoot, :18), four real .razor fixture files under RazorFixtures/, and Xunit.Sdk.XunitException (:2).
  • Concept introduced - scanning markup text, and the near-misses that decide whether a text scan is usable. MudBlazor's server-side sort reads the bound property off a PropertyColumn; a TemplateColumn has no bound property, so marking one Sortable="true" produces a clickable header that sorts nothing, or sorts only the page's local slice. The failure is silent: the grid renders, the arrow toggles, and the order is wrong (rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.Markup.cs:10-:15). The defect lives in markup that compiles perfectly, so this is the one rule in the suite that reads .razor text rather than IL or reflection (:16-:19). That choice imports every classic text-scanning hazard, and the rule addresses each one: the tag is read to its closing > through quoted attribute values, so attribute order, wrapped lines and a generic T="Foo<Bar>" argument all work (:22-:29); the Sortable value is matched quoted or unquoted, with or without an @ expression prefix and parentheses, while a value bound to a field is left alone because its value is not knowable from markup; and @* *@ comments are blanked with spaces before the scan, newlines preserved, so a commented example neither fails the gate nor shifts the reported line numbers (:31-:34,:82-:108). [Rubric §18 - UI Architecture] and [Rubric §23 - Front-End Performance] both apply, since server-side sorting is what keeps a grid from paging in memory; [Rubric §14 - Testability] is the technique.
  • Walkthrough - FixtureRoot is built from ArchitectureMapBase.FindRepoRoot("MMCA.Common.slnx") plus the four path segments down to RazorFixtures (:17-:22), with Offending and Clean as expression-bodied properties over it (:24,:26).
    • SortableTemplateColumn_IsFlaggedWithFileAndLine (:28-:37): asserts the message contains SortableTemplateColumnGrid.razor:7. That exact number is the point: in the fixture the element opens on line 7 and the Sortable="true" attribute sits on line 8 (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/RazorFixtures/Offending/SortableTemplateColumnGrid.razor:7-:9), and the rule must report where the element starts, not where the attribute happens to land.
    • ExpressionBoundLiteralTrue_IsAlsoFlagged (:39-:48): pins ExpressionSortableTemplateColumnGrid.razor:5, a one-line element with Sortable="@(true)" written first (RazorFixtures/Offending/ExpressionSortableTemplateColumnGrid.razor:5).
    • ConformingMarkup_Passes (:50-:57): the discrimination fact, and the one carrying the most fixture. ConformingGrid.razor packs seven near-misses into one file: a sortable PropertyColumn (:6), a TemplateColumn with no Sortable at all (:9), one explicitly Sortable="false" (:16), one bound to a field (:19), a longer attribute name IsSortable="true" (:22), a generic argument carrying an angle bracket inside a quoted value with a real sortable PropertyColumn on the line below it (:26-:27), and a longer element name TemplateColumnGroup (:30). CommentedOutGrid.razor holds the eighth: the whole defect inside a @* *@ block (:3-:9).
    • MissingRoot_FailsRatherThanScanningNothing (:59-:70): points the rule at a folder that does not exist and asserts the message contains "markup root not found" (:67), which is this rule's version of the non-vacuity floor. A path typo must fail the gate rather than quietly making it pass.
  • Why it's built this way - the fixtures are real .razor files rather than inline strings because the rule takes directory roots and enumerates files itself, excluding bin and obj (ArchitectureRules.Markup.cs:71-:76); testing it through anything but the filesystem would be testing a different method. The generic-argument case (ConformingGrid.razor:24-:27) is the sharpest of the seven: its own comment records the failure being prevented, a tag scan that runs past the element and swallows the sortable PropertyColumn below it.
  • Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of the rule is SortableColumnConventionTestsBase, which supplies the repo's real markup roots.
  • Caveats / not-in-source - a Sortable value bound to a field or property is deliberately not judged, so a TemplateColumn whose binding evaluates to true at run time passes this gate. ConformingGrid.razor:19 is that shape, listed as conforming.

CascadeSoftDeleteFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:17 · Level 6 · class

  • What it is - the meta-test for the cascade-soft-delete rule: six facts that pin both accepted cascade forms, both violation reasons with their named child collection, the out-of-scope aggregate, and both granularities of the exemption list.
  • Depends on - ArchitectureRules (AggregatesCascadeSoftDeleteToChildren, CascadeSoftDeleteFitnessTests.cs:26), its nested FixtureAssemblyMap, the seven types in MMCA.Common.Architecture.Tests.CascadeFixtures (:1), and Xunit.Sdk.XunitException (:3).
  • Concept introduced - a rule that reads IL cannot be verified by reading it. The class doc states the principle in those words (:7-:16), and it is the organizing idea behind this whole family of self-tests. AggregatesCascadeSoftDeleteToChildren decides its verdict from the instruction stream of a Delete() override, so no amount of reviewing the rule's source proves it distinguishes base.Delete() from child.Delete(). The only proof is to compile both shapes side by side and point a map at them. [Rubric §14 - Testability] is the technique; [Rubric §8 - Data Architecture] and [Rubric §30 - Compliance/Privacy/Data Governance] are the properties, taught at CascadeChildFixture.
  • Walkthrough - a private const string FixtureNamespace (:19) and a readonly FixtureAssemblyMap (:21) are shared by every fact, and five of the six read the message of a rule call that always throws.
    • AggregateWithoutDeleteOverride_IsFlagged_WithItsChildCollection (:23-:33): asserts three things about one line, the type name, the reason "no Delete() override", and the collection name "_orphans" (:30-:32). Pinning the collection name is what makes the failure actionable rather than merely true.
    • DeleteOverrideThatOnlyDeletesItself_IsFlagged_WithItsChildCollection (:35-:45): the same three assertions for the other reason, "Delete() never deletes a child" and "_ignored". The two distinct reason strings are the rule distinguishing "you forgot" from "you tried and it does not work".
    • BothCascadeForms_AreAccepted (:47-:60): asserts both the helper form and the hand-rolled loop are absent, each with a because naming the form.
    • AggregateWithNoChildEntities_IsIgnored (:62-:71): the scope fact, with the ruling stated inline, a List<string> is not a child-entity collection.
    • AllowlistedType_SilencesOnlyThatType (:73-:85) and AllowlistedNamespace_SilencesTheRule (:87-:94): the two exemption granularities. The type-level fact carries the control assertion, that the other offender is still reported (:83-:84).
  • Why it's built this way - two structurally identical offenders is what makes the type-level exemption fact meaningful; two distinct violation reasons is what keeps a regression in the body reader from hiding behind the simpler missing-override check. See ADR-005 and ADR-015.
  • Where it's used - an independent class in the Common architecture suite; the shipped binding of the same rule over real code is CascadeSoftDeleteConventionTestsBase, activated here by CascadeSoftDeleteConventionTests and in both consumer repos over their own modules.

DependencyVersionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/DependencyVersionTests.cs:9 · Level 6 · class

  • What it is - the MMCA.Common binding of the shared dependency-pin rule: a one-line sealed subclass that turns on the checks enforcing this repo's two commercial-license package ceilings, MassTransit below major 9 and SixLabors.ImageSharp below major 4.
  • Depends on - DependencyVersionTestsBase (public sealed class DependencyVersionTests : DependencyVersionTestsBase;, DependencyVersionTests.cs:9), which in turn calls ArchitectureRules.PinnedPackageMajorBelow against this repo's Directory.Packages.props.
  • Concept introduced - the thin-subclass fitness pattern. Almost every rule in this project lives once in the reusable MMCA.Common.Testing.Architecture package as an abstract *TestsBase, and each repo activates it with a near-empty subclass. This is the first of many such subclasses; the body-less form here is the extreme case (no configuration at all: the declaration ends at its semicolon, DependencyVersionTests.cs:9). [Rubric §32 - Dependency & Supply-Chain] assesses guarding against risky dependency drift; the base parses the pinned versions and fails the build before either ceiling is crossed, so a "just bump the version" edit cannot slip through.
  • Walkthrough - no members; the entire behavior is inherited. The base contributes two [Fact]s. MassTransit_MustNotExceed_MajorVersion8 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DependencyVersionTestsBase.cs:24-:37) walks the three MassTransit package ids MassTransit, MassTransit.RabbitMQ, and MassTransit.Azure.ServiceBus.Core (:17-:22) with an exclusiveMajorCeiling of 9, because v9 requires a commercial license (MT_LICENSE) and every broker-enabled host fails its startup license check without one, while CI never starts a broker so the build would otherwise stay green (:32-:35). ImageSharp_MustNotExceed_MajorVersion3 (:47-:60) does the same for SixLabors.ImageSharp (:45) with a ceiling of 4, because ImageSharp v4's MSBuild targets fail at build time without a $(SixLaborsLicenseKey) (:55-:58). Both id lists are protected virtual, so a repo that pins neither package can override them to an empty list and make the rule vacuous.
  • Why it's built this way - the pins are real only in MMCA.Common (where these packages are actually declared); ADC and Store inherit MassTransit transitively through MMCA.Common.Infrastructure and deliberately do not subclass the base, because the default list would assert a Directory.Packages.props entry they do not declare (DependencyVersionTestsBase.cs:8-:13). See ADR-016, which records the lockstep-versioning policy and the license-ceiling pattern these two facts implement.
  • Where it's used - run by the MMCA.Common.Architecture.Tests suite in CI's build-and-test job.
  • Caveats / not-in-source - the class's own doc comment (DependencyVersionTests.cs:5-:8) still describes only the MassTransit trap; the ImageSharp ceiling arrived later on the base and the subclass doc was not extended. The behavior is the base's two facts, not the comment.

FixtureMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventContractTestsBaseTests.cs:124 · Level 6 · class

  • What it is - a consumer-shaped map over this test assembly, declaring one module Shared layer, so the integration-event contract scan covers the fixture events here and none of the framework's own.
  • Depends on - ArchitectureMapBase (private sealed class FixtureMap : ArchitectureMapBase, IntegrationEventContractTestsBaseTests.cs:124), LayerRef and the Layer enum.
  • Concept - cross-references the module-versus-framework distinction taught at FixtureModuleMap and FakeConsumerMap. The single Module("Fixture", Layer.Shared, ...) entry (:130) does two things at once. It makes the map module-bearing, so IntegrationEvents excludes framework layers (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Events.cs:70-:73) and no framework-shipped event can leak into the contract the probes compare; and it names Shared, which is where a consumer's integration events are required to live, so the fixture events do not simultaneously trip the residency rule. RepoToken => "MMCA.FixtureRepo" (:126) is deliberately unlike MMCA.Common, so nothing derived from the token can collide with the real framework namespaces.
  • Walkthrough - DefineLayers() is a yield-based iterator with one entry (:128-:131). The events it brings into scope are the eight upcaster fixture contracts, which are the only IIntegrationEvent implementations this assembly declares. That reuse is what makes the class doc's promise hold, that the mutation cases stay honest as the fixture types change (:9-:10): the baseline is rebuilt from live types on every call rather than hard-coded.
  • Where it's used - built fresh by LiveContract() (:86-:87) on every mutation, and supplied as the Map of every ProbeTests instance (:136).

FolderWidthTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FolderWidthTests.cs:18 · Level 6 · class

  • What it is - the MMCA.Common binding of the folder-width rule: a sealed subclass that points the shared rule at this repo's root and declares the three folders this repo keeps deliberately flat.
  • Depends on - FolderWidthTestsBase (public sealed class FolderWidthTests : FolderWidthTestsBase, FolderWidthTests.cs:18) and ArchitectureMapBase for FindRepoRoot("MMCA.Common.slnx") (FolderWidthTests.cs:20), which walks upward until it finds the solution file so the test does not depend on the working directory.
  • Concept - the same thin-subclass fitness pattern taught at DependencyVersionTests, here with configuration rather than none: the repo supplies RepoRoot and, because it has documented exceptions, ExemptFolderSuffixes. [Rubric §5 - Modularity & Structure] is the property. The rule enforces "feature by folder, use case by leaf": no folder under Source/ or Tests/ may hold more than MaxDirectFiles direct code units, defaulting to 12 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FolderWidthTestsBase.cs:23), so a folder keeps naming a feature instead of drifting into a technical bucket (base doc, FolderWidthTestsBase.cs:3-:13).
  • Walkthrough - two overrides and no facts of its own.
    • RepoRoot (FolderWidthTests.cs:20) is an initialized get-only property, so the upward search for MMCA.Common.slnx runs once per test instance.
    • ExemptFolderSuffixes (FolderWidthTests.cs:28-:33) returns three repo-relative suffixes: MMCA.Common.Application/UseCases/Decorators, MMCA.Common.Application.Tests/Decorators, and MMCA.Common.Domain/Interfaces. The doc above it (:22-:27) gives the reason for the first pair: the decorator pipeline is nine cross-cutting concerns times command and query, so splitting it by concern would yield nine two-file folders, and the test twin mirrors it. The third is the entity marker interfaces.
    • The single inherited [Fact], Folders_stay_narrow (FolderWidthTestsBase.cs:31-:33), delegates to ArchitectureRules.FoldersStayNarrow (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.FolderWidth.cs:34), which walks Source and Tests recursively (:42-:50), skips the tool-owned and build-output segments bin, obj, Migrations, Platforms, Resources, node_modules, wwwroot and .git (:82), and counts a .razor component and its co-located X.razor.cs code-behind as one unit (:105-:119).
  • Why it's built this way - counting code units rather than files is what keeps the cap honest for a Blazor project, where a component is three files that describe one thing; skipping Migrations/ and Platforms/ is what keeps generated and SDK-owned trees from failing a rule about human-authored layout. Exemptions are folder-path suffixes rather than a blanket switch, so each accepted flat namespace is a line someone had to write and can be read back. See ADR-109 for the convention itself and ADR-015 for the fitness-function approach.
  • Where it's used - run by the MMCA.Common.Architecture.Tests suite in CI. The same base is activated once per repo; the ADC binding is the identical shape with FindRepoRoot("MMCA.ADC.slnx") and no exemptions at all (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/FolderWidthTests.cs:10-:12).
  • Caveats / not-in-source - the class-level doc comment (FolderWidthTests.cs:5-:17) describes a wider exemption set than the class configures, naming the application contracts, the shared auth contracts, the API startup extensions and the integration-test package root. The behavior is the three suffixes actually listed at :30-:32; the property's own doc (:22-:27) records the current rationale and points at UPGRADING.md.

IntegrationEventContractTestsBaseTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventContractTestsBaseTests.cs:13 · Level 6 · class

  • What it is - the meta-test for the frozen integration-event contract base: six facts proving that a reordered member list is not a break, and that everything a consumer can actually observe (a missing member, an extra member, a retyped member, a missing event) still is.
  • Depends on - IntegrationEventContractTestsBase (the type under test, through the nested ProbeTests), ArchitectureRules (BuildIntegrationEventContract, :87), its nested FixtureMap, and externals xUnit plus AwesomeAssertions.
  • Concept introduced - a snapshot gate has to be insensitive to everything the wire is insensitive to, or it trains people to update it by rote. The base compares each event's member list as a SET rather than a sequence, and the reason is stated plainly in its own doc (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventContractTestsBase.cs:10-:16): JSON carries no member order, so reordering two properties in a record declaration changes nothing a consumer can observe, and failing the build for it would teach a team to regenerate the one gate that guards real breakage without reading it. The comparison therefore reports four kinds of difference and no others: MISSING EVENT, NEW EVENT (:52-:56), and per event MISSING member, changed type and EXTRA member (:78-:96). The retype is reported as a retype rather than as an unrelated add and remove, because that is the failure a consumer actually sees: the property still deserializes, into the wrong shape (:64-:68). [Rubric §6 - CQRS & Event-Driven] and [Rubric §9 - API & Contract Design] are the properties; [Rubric §33 - Developer Experience] is the reason for the set comparison, since a gate that cries wolf stops being read.
  • Walkthrough - the class never hard-codes an expected contract. It builds the live one from FixtureMap through LiveContract() (:86-:87) and then mutates it, so every case stays honest as the fixture types change (class doc, :8-:10).
    • Base_Passes_WhenTheCommittedLiteralsAreAlphabetical (:15-:23): the unmutated baseline, which is the shape every consumer's committed literal is written in today.
    • Base_Passes_WhenMembersAreListedInADifferentOrder (:25-:34): reverses each line's member list through ReverseMembers (:90-:94) and asserts NotThrow, with the because stating why order is not observable.
    • Base_Fails_WhenTheCodeDeclaresAMemberTheContractDoesNot (:36-:47) and Base_Fails_WhenTheContractDeclaresAMemberTheCodeDropped (:49-:60): drop the first member and append a GhostProperty:String, asserting "EXTRA member" and "MISSING member GhostProperty:String" respectively.
    • Base_Fails_WhenAMembersTypeChanges (:62-:74): rewrites the first member's type to Guid and asserts "changed type".
    • Base_Fails_WhenAnEventIsMissingFromTheSnapshot (:76-:83): drops the first line entirely and asserts "NEW EVENT", which is the direction that matters most, an event shipped without a consumer rollout.
    • MutateFirstMultiMemberEvent (:100-:109) is the helper that keeps all four failure cases robust: it finds the first event declaring more than one member and asserts that such an event exists (:104), so a fixture reshaped down to single-member events fails loudly instead of making the cases vacuous. Split (:111-:118) parses one line back into a name and a member array.
  • Why it's built this way - deriving the expectations from the live contract and mutating them is what stops this meta-test from becoming a second snapshot that also needs maintaining. The probe subclasses are private so xUnit does not collect their inherited facts as tests of their own (class doc, :10-:11), the same discipline DriftedTests follows.
  • Where it's used - an independent class in the Common architecture suite. The base it protects is subclassed per repo by the consumers, whose committed ExpectedContract literals are the real subject of the gate.
  • Caveats / not-in-source - the base's Parse keeps a malformed committed line whole as an event with no members (IntegrationEventContractTestsBase.cs:99-:122) so it surfaces as a mismatch rather than being silently dropped. No fact here exercises that path.

LocalizationResourceTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/LocalizationResourceTests.cs:12 · Level 6 · class

  • What it is - the MMCA.Common binding of the resource-completeness rule: it asserts the framework's own .resx files fully translate every supported non-default culture, deriving the required-culture set from the live allowlist rather than restating it.
  • Depends on - LocalizationResourceTestsBase (base rule, LocalizationResourceTests.cs:12) and SupportedCultures (source of the required cultures, :15).
  • Concept introduced - deriving a gate's expectations from the same allowlist production uses. Instead of hardcoding "translate Spanish," the override computes RequiredCultures as SupportedCultures.All minus SupportedCultures.Default (:14-:17), so adding a locale to the app automatically extends the coverage requirement. [Rubric §27 - i18n] assesses whether localization is complete and enforced; this gate makes a missing translation a build failure and self-updates when the supported set grows.
  • Walkthrough - two members. RequiredCultures (:14-:17) filters SupportedCultures.All to the non-default entries; the allowlist is [Default, "es"] with Default = "en-US" (MMCA.Common/Source/Core/MMCA.Common.Shared/Globalization/SupportedCultures.cs:12,:18), so the required set is today exactly es. MinimumBaseResources => 3 (:21) sets a non-vacuous floor: the scan must find at least three base resources (ErrorResources for the API, plus SharedResource and MudTranslations for the UI), so a wrong scan root or repo re-layout cannot let the gate pass having checked nothing. The base's default for that floor is zero, which skips the guard entirely (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizationResourceTestsBase.cs:21), so overriding it here is what makes the gate honest. The single inherited [Fact] is Translations_AreComplete_ForEveryRequiredCulture (LocalizationResourceTestsBase.cs:23-:25).
  • Why it's built this way - ADR-027 (localization) requires supported cultures to be fully translated; the derived allowlist plus the minimum-count floor turn that into a self-maintaining CI gate. The pseudo-locale qps-Ploc is deliberately kept out of SupportedCultures.All (SupportedCultures.cs:20-:28) precisely so this gate does not demand a .qps-Ploc.resx sibling.
  • Where it's used - run by the MMCA.Common.Architecture.Tests suite.

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · (see per-type table) · Level 6 · class

The same unsafe and safe pair as NavigatingSpec and ScalarOnlySpec, rebuilt one level further down the specification hierarchy as QuerySpecification<TEntity, TIdentifierType> subclasses. They exist to pin a single ruling: adding includes, ordering, and paging to a specification must not take it out of the navigation rule's reach.

  • Depends on - QuerySpecification<TEntity, TIdentifierType> (both, SpecificationFitnessTests.cs:76,:89), FitnessDependent and FitnessPrincipal (the entities), and System.Linq.Expressions (:1).
  • Concept introduced - walking the whole base chain instead of matching one type. The rule selects candidates with HasBaseTypeStartingWith("MMCA.Common.Domain.Specifications.Specification") (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:7,:33), and that helper climbs BaseType all the way up (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/RuleHelpers.cs:80-:91). Because QuerySpecification itself derives from Specification, the prefix still matches two levels down, and the string prefix match means both Specification and QuerySpecification are caught by the one condition. That is the kind of thing that is true today and could quietly stop being true after a refactor of the specification hierarchy, which is exactly why it gets its own fact rather than being left as an assumption. [Rubric §7 - Microservices Readiness] and [Rubric §8 - Data Architecture] are the underlying properties (a navigating predicate cannot cross a data-source boundary, ADR-006); [Rubric §14 - Testability] is why the pair exists.
  • Walkthrough - both declare an explicit parameterless constructor, which is load-bearing: the rule instantiates candidates to read Criteria and skips any type without one (ArchitectureRules.Specifications.cs:37-:41). Each constructor also exercises a different part of the query surface, so the fixtures prove that the extra machinery does not interfere with the analysis.
Type File:Line What differs
NavigatingQuerySpec MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:76 The offender. Its constructor adds ordering and paging, AddOrderBy(d => d.PrincipalId) and ApplyPaging(skip: 0, take: 10) (:79-:83), and its Criteria dereferences the navigation, d => d.Principal!.IsActive (:86). Must be reported.
ScalarOnlyQuerySpec MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:89 The safe counterpart, and a deliberately awkward one: its constructor calls AddInclude(nameof(FitnessDependent.Principal)) (:92), so it does pull in the related entity, while its Criteria filters only on own columns (:95). Must not be reported. Includes are an eager-loading instruction, not a filter predicate, and the rule analyzes the predicate only.
  • Why they're built this way - ScalarOnlyQuerySpec is the sharper of the two. Without it, a future rule implementation that (reasonably but wrongly) also inspected Includes would still pass every other fixture in the file. Both are declared public while their plain-Specification siblings are private, which makes no difference to the rule (it scans ConcreteClasses across the assembly, not by visibility).
  • Where they're used - the two inputs to Rule_AlsoAnalyzesQuerySpecifications in SpecificationFitnessTests (:26-:38).

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:63 · Level 6 · class

  • What it is - the deliberately-unsafe fixture specification: its Criteria navigates from the dependent into a related entity, the exact pattern the fitness function must flag.
  • Depends on - Specification<TEntity, TIdentifierType> (private sealed class NavigatingSpec : Specification<FitnessDependent, int>, SpecificationFitnessTests.cs:63) and FitnessDependent / FitnessPrincipal (the entities it filters over).
  • Concept introduced - why cross-entity navigation in a specification is unsafe across data sources. Under database-per-service (ADR-006), a d => d.Principal!.IsActive predicate (:65) assumes the related entity lives in the same queryable model; once the principal is extracted to another physical source, that navigation cannot translate to SQL (and on Cosmos the cross-source navigation is degraded out of the model entirely, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:9-:15). The fitness function SpecificationsDoNotNavigateToOtherEntities treats it as a violation and points at the fix: resolve the related keys first and filter on the foreign-key column instead. [Rubric §7 - Microservices Readiness] assesses whether the code stays extractable; this fixture is the negative example that proves the readiness guard fires.
  • Walkthrough - one member, an overridden Criteria expression that dereferences the Principal navigation (:65). Being parameterless matters: the rule only instantiates and inspects specifications that expose a parameterless constructor (ArchitectureRules.Specifications.cs:37-:41), so a fixture with constructor dependencies would be skipped and prove nothing. The test asserts the rule's exception message contains this type's name.
  • Where it's used - the "should be flagged" input to SpecificationFitnessTests; paired with ScalarOnlySpec.

PiiErasureContractFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiErasureContractFitnessTests.cs:19 · Level 6 · class

  • What it is - a non-vacuous §30 fitness test that pushes a representative [Pii] data subject through the framework's own privacy machinery, proving that PII detection, redaction and masking, and in-place erasure compose end to end rather than each being verified in isolation.
  • Depends on - DataSubjectSample (the fixture), PiiRedactor, IAnonymizable, and Result. Externals: xUnit [Fact], AwesomeAssertions.
  • Concept introduced - a contract-composition fitness function. Where the plain [Pii] => IAnonymizable scan is structural (does every marked type also expose an erasure path), this test exercises the behavior the scan presumes. It is the pattern for proving a cross-cutting compliance contract actually holds when its parts run together. [Rubric §30 - Compliance/Privacy/Data Governance] assesses that erasure and log-masking genuinely protect subject data; this test is the framework's executable evidence.
  • Walkthrough - four [Fact]s, each isolating one link in the contract. DataSubject_DeclaresPii_SoTheContractIsNotVacuous (:21-:24) asserts PiiRedactor.HasPii recognizes the sample, so the later guards assert against something real. PiiRedactor_MasksEveryPiiMember_AndPassesThroughNonPii (:26-:35) redacts an instance and checks Email and FullName become PiiRedactor.RedactedToken while Id and City pass through unchanged (:31-:34). PiiRedactor_LeaksNoClearTextPii_ToLogsOrTelemetry (:37-:50) verifies neither the redacted dictionary values (:42-:44) nor RedactToString output (:46-:49) contain the original email or name, covering both the structured-log and the flat-string rendering paths. The fourth fact (:52-:72) asserts the sample is IAnonymizable (:56), that Anonymize() succeeds and changes the PII fields (:59-:62), that a second call also succeeds and leaves the fields erased (idempotence, :64-:66, ADR-005), and finally that an anonymized subject still leaks no original clear text when redacted (:69-:71), proving erasure and redaction compose.
  • Why it's built this way - the plain [Pii] => IAnonymizable scan is vacuous in the framework (no PII entity in Common's Domain); this test closes that gap by forcing the machinery through a stand-in subject, so the §30 guarantee is proven, not merely assumed. Consumers (MMCA.ADC's User) run the same contract against their real aggregates.
  • Where it's used - an independent test class in the Common architecture suite; its structural counterpart is PiiConventionTests, whose own doc names this class as the non-vacuous half of the pair (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiConventionTests.cs:9-:11).

ProbeTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventContractTestsBaseTests.cs:134 · Level 6 · class

  • What it is - a parameterized negative fixture: a private sealed subclass of the frozen-contract base whose ExpectedContract is whatever the enclosing test hands its constructor.
  • Depends on - IntegrationEventContractTestsBase (private sealed class ProbeTests(IReadOnlyList<string> expected) : IntegrationEventContractTestsBase, IntegrationEventContractTestsBaseTests.cs:134) and FixtureMap (:136).
  • Concept introduced - parameterizing the drift instead of hard-coding it. DriftedTests is the same technique with fixed wrong values, which works when a base has three assertions and each needs one wrong constant. Here the base has one assertion with six interesting failure modes, so a fixed drifted subclass would mean six near-identical nested classes. A primary-constructor parameter collapses them into one type instantiated six times with different data (:20,:30,:41,:54,:68,:79). The trade-off is worth naming: the parameterized form is only safe because the expected contract is derived from the live one and then mutated, so a probe can never assert against a stale hand-written literal.
  • Walkthrough - two overrides, both initialized rather than computed: Map { get; } = new FixtureMap() (:136) and ExpectedContract { get; } = expected (:138). Being private keeps xUnit from collecting the inherited IntegrationEventContracts_ShouldMatch_TheFrozenSnapshot as a test of its own, and the enclosing facts invoke it as a delegate instead.
  • Where it's used - constructed six times across the facts of IntegrationEventContractTestsBaseTests, once per contract mutation.

ScalarOnlySpec

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:69 · Level 6 · class

  • What it is - the deliberately-safe counterpart to NavigatingSpec: its Criteria filters only on the entity's own scalar columns, the pattern the fitness function must leave alone.
  • Depends on - Specification<TEntity, TIdentifierType> (private sealed class ScalarOnlySpec : Specification<FitnessDependent, int>, SpecificationFitnessTests.cs:69) and FitnessDependent.
  • Concept introduced - cross-references the navigation-safety concept from NavigatingSpec; this is the positive example. A predicate over the entity's own scalars (d => d.PrincipalId == 1 && d.Flag, :71) translates to SQL on any engine and survives extraction, so the rule must not flag it. Having both a flagged and an un-flagged fixture is what makes the test prove the rule discriminates rather than just always throwing.
  • Walkthrough - one overridden Criteria filtering on PrincipalId and Flag (:71). The test asserts the rule's exception message does not contain this type's name.
  • Where it's used - the "should not be flagged" input to SpecificationFitnessTests.

SoftDeleteEnforcementFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementFitnessTests.cs:15 · Level 6 · class

  • What it is - the meta-test for the hard-delete rule: five facts proving EF's erasing members are caught by name and declaring type, that every erasing member in a type is reported rather than just the first, that an ordinary collection Remove is never mistaken for one, and that both allowlist granularities work.
  • Depends on - ArchitectureRules (HardDeletesOnlyInAllowedTypes, SoftDeleteEnforcementFitnessTests.cs:24), its nested FixtureAssemblyMap, the four types in MMCA.Common.Architecture.Tests.HardDeleteFixtures (:1), and Xunit.Sdk.XunitException (:3).
  • Concept - cross-references the hard-delete concept taught at FixtureEntity. The fact worth dwelling on is HardDelete_ReportsEveryErasingMember (:30-:40), which asserts all three method names Purge, PurgeMany and PurgeAsync appear in one message. A rule that stopped at the first hit per type would still pass every other fact in the class, and a partial report is worse than none in a codebase-wide sweep: a developer fixes the one named site, the build goes green on the next run only because the next offender is now first, and the sweep takes as many rounds as there are call sites. The rule's collector is a nested yield return over every method and every instruction (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:104-:118), and this fact is what pins that.
  • Walkthrough - a private const string FixtureNamespace (:17) and a readonly FixtureAssemblyMap (:19) are shared by every fact.
    • HardDelete_OutsideTheAllowlist_IsFlagged (:21-:28): the baseline, asserted with WithMessage("*DbSetRemovingFixture*").
    • HardDelete_ReportsEveryErasingMember (:30-:40): the completeness fact described above, with a because naming which EF member each one maps to.
    • OrdinaryCollectionRemove_IsNotAHardDelete (:42-:51): the discrimination fact, asserting the whole SoftDeletingFixture type name is absent even though it contains a literal Remove call.
    • AllowlistedNamespace_SilencesTheRule (:53-:60) and AllowlistedType_SilencesOnlyThatType (:62-:74): the two granularities, the second carrying the control assertion that ExecuteDeletingFixture is still reported (:72-:73).
  • Why it's built this way - the class doc names the principle again (:7-:14): a rule that reads IL cannot be verified by reading it, so the offending and the innocent call sites are compiled side by side and a map is pointed at them. See ADR-005 and ADR-015.
  • Where it's used - an independent class in the Common architecture suite; the shipped binding of the same rule over real code is SoftDeleteEnforcementTestsBase, activated here by SoftDeleteEnforcementTests with the framework's four reviewed eraser types.

SpecificationFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:13 · Level 7 · class

  • What it is - the test that verifies the SpecificationsDoNotNavigateToOtherEntities fitness function actually discriminates: it must flag a specification that navigates into another entity, must leave a scalar-only specification alone, and must reach both shapes through the QuerySpecification subclass as well.
  • Depends on - ArchitectureRules (the rule under test), and its own nested fixtures SpecTestMap, FitnessDependent, FitnessPrincipal, NavigatingSpec, ScalarOnlySpec, and the two query variants (NavigatingQuerySpec, ScalarOnlyQuerySpec).
  • Concept introduced - testing the test: verifying a fitness function is neither vacuous nor over-broad. A rule that never fires is useless; a rule that flags everything is worse. This class proves the specification-navigation guard does exactly one thing by feeding it both a positive and a negative fixture in a single run. [Rubric §14 - Testability] assesses whether the guardrails themselves are trustworthy; this is the meta-test that earns that trust, and ADR-015 is the decision that makes such guardrails build-gating in the first place. Compare ModuleConformanceTestsBaseTests, which applies the same discipline to the module-conformance base.
  • Walkthrough - two [Fact]s, both driving the same single rule execution shape. Rule_FlagsNavigatingSpecification_ButNotScalarSpecification (:15-:24) wraps the rule call in an act delegate, ArchitectureRules.SpecificationsDoNotNavigateToOtherEntities(new SpecTestMap()) (:18), captures the thrown exception through Should().Throw<Exception>().Which (:20), and asserts its message contains NavigatingSpec and the word "navigates" while not containing ScalarOnlySpec (:21-:23). Both halves matter: the first two assertions prove the rule fires, the third proves it does not over-report. Rule_AlsoAnalyzesQuerySpecifications (:26-:38) repeats the pair one level down the hierarchy, asserting NavigatingQuerySpec is reported and ScalarOnlyQuerySpec is not (:32-:37). The nested types below the facts supply the model: SpecTestMap (:40), the two entities (:48,:57), and the four specifications (:63,:69,:76,:89).
  • Why it's built this way - the navigation rule protects future extraction (a navigating specification cannot cross a data-source boundary, ADR-006); a discriminating test keeps the rule honest as it evolves, and the second fact keeps it honest as the specification hierarchy evolves.
  • Where it's used - an independent class in the Common architecture suite.

SpecTestMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:40 · Level 7 · class

  • What it is - a minimal architecture map used only by SpecificationFitnessTests: it registers this test assembly as the single Application layer so the specification-navigation rule has a model to scan.
  • Depends on - ArchitectureMapBase (private sealed class SpecTestMap : ArchitectureMapBase, SpecificationFitnessTests.cs:40), the Layer enum, and LayerRef.
  • Concept introduced - cross-references the map concept from CommonArchitectureMap. Where the real map spans seven packages, this one collapses to a single self-referential Application layer (Framework(Layer.Application, typeof(SpecificationFitnessTests).Assembly), :45) because the fixtures (the four specifications and two entities) live in the test assembly itself. It is the smallest map that lets a fitness function run against hand-crafted types; the rule scans Application and Domain layers for specification subclasses (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:30-:33), so one Application entry is enough.
  • Walkthrough - RepoToken => "MMCA.Common" (:42) and a one-entry DefineLayers() (:44-:45) pointing at this assembly.
  • Where it's used - instantiated once per fact inside SpecificationFitnessTests (:18,:29).

BadgeGranter, DirectSavingHandler, PointsWriter

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainEventSaveFixtures · (see per-type table) · Level 8 · class

The three fixture types that actually call IUnitOfWork.SaveChangesAsync. Each is an internal sealed class with one injected IUnitOfWork, one constructor, and one method whose whole body is the save. They are the terminals of every call chain the domain-event handler save rule has to find.

  • Depends on - IUnitOfWork (DomainEventSaveFixtures.cs:2), plus the handler interface and event for the one that is itself a handler.
  • Concept - cross-references the handler-save concept from IBadgeGranter. What this trio pins is how the rule recognizes a save at all, which is broader than a single interface. IsSaveCall accepts a callee named SaveChanges or SaveChangesAsync (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:12) whose declaring type is either under the Microsoft.EntityFrameworkCore namespace prefix, or whose simple name ends with one of four suffixes: DbContext, DbContextFactory, UnitOfWork or Repository (:20-:21,:235-:255). Matching by suffix rather than by a full type name is what lets the rule see EF's own contexts, the framework's DbContextFactory save surface, a consumer's SQLServerDbContext, and any IRepository that forwards to one, without the rule package taking a compile dependency on any of them. These three fixtures all reach it through IUnitOfWork, the suffix case.
  • Walkthrough - all three share the same shape: a private readonly IUnitOfWork _unitOfWork, an internal constructor assigning it, and one method returning or awaiting _unitOfWork.SaveChangesAsync(cancellationToken). What differs is where they sit in a chain.
Type File:Line Its position in a chain
DirectSavingHandler MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:16 Depth zero: the handler saves in its own HandleAsync (:22-:23). Caught at the call site with no traversal needed, which is what makes it the control for the depth-bound and allowlist facts, since neither can hide it.
PointsWriter MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:53 Depth two: the second hop of the transitive chain, reached only through PointsAwarder. Its WriteAsync is async and awaits with ConfigureAwait(false) (:59-:60), so the call the walk must find lives inside a compiler-generated state machine rather than in the method body.
BadgeGranter MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:70 Behind an abstraction: the sole implementation of IBadgeGranter, reached from InterfaceDispatchSavingHandler only after the walk expands the interface call to its targets. Also async (:76-:77).
  • Why they're built this way - three different routes to the same terminal is the design. If all three saves were direct, the walk's traversal, its state-machine handling and its interface resolution would all be untested; if the direct one did not exist, nothing would prove that a save at depth zero is caught independently of the walk at all, which is the assertion the allowlist and depth-bound facts rest on.
  • Where they're used - DirectSavingHandler is named by three facts of DomainEventHandlerSaveFitnessTests (:30,:92,:103); PointsWriter is asserted to appear in the reported chain (:43); BadgeGranter is reached transitively and is the reason InterfaceDispatchSavingHandler is reported at all.

PointsAwarder

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainEventSaveFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:42 · Level 9 · class

  • What it is - hop one of the transitive save chain: a service that saves nothing itself and delegates to PointsWriter, which does.
  • Depends on - PointsWriter (constructor-injected, DomainEventSaveFixtures.cs:44,:46).
  • Concept - cross-references IBadgeGranter. Its distinctive job is to be the middle of a chain, which is what makes it the natural subject of the allowlist-pruning fact. Allowlist entries in this rule do two things at once: a matching type is neither reported nor walked into (rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:72-:74), because EnqueueTargets filters targets by the same allowlist before queueing them (:170-:172). Allowlisting a collaborator therefore stops the walk at that node and the handler behind it disappears from the report, while a handler that saves directly is unaffected because its save is found at the call site rather than through a target expansion. That distinction is exactly what one fact pins.
  • Walkthrough - one field, one internal constructor, and internal async Task AwardAsync(CancellationToken cancellationToken) => await _writer.WriteAsync(cancellationToken).ConfigureAwait(false); (:48-:49). Being internal rather than public is irrelevant to the rule, which reads Cecil type definitions rather than reflected public surface.
  • Where it's used - asserted to appear in the reported chain as the first hop (DomainEventHandlerSaveFitnessTests.cs:42), and used as the allowlist entry in AllowlistedCollaborator_PrunesTheWalkWithoutHidingDirectSaves (:81).

TransitiveSavingHandler

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainEventSaveFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:31 · Level 10 · class

  • What it is - the real-world offender shape: a handler that delegates to a service, which delegates to a writer, which saves. Two hops, both through async methods.
  • Depends on - IDomainEventHandler<in TDomainEvent> closed over FixtureDomainEvent, and PointsAwarder (DomainEventSaveFixtures.cs:31,:33).
  • Concept - cross-references IBadgeGranter. This is the fixture that forces the two hardest parts of the walk. First, both hops are async, so the calls the walk needs are not in the declared method bodies at all: the C# compiler moves them into generated state-machine types. The rule handles that by resolving each method's AsyncStateMachineAttribute to its generated type and reading that type's methods too (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:200-:231), then collapsing those generated types back onto their declaring type for the report so the printed chain reads like source (:265-:277). The fixture doc says exactly this (:26-:29): the walk must follow the compiler-generated state machines to see anything at all. Second, the walk is breadth-first with a visited set and a depth bound (:120-:158), which is what makes it cycle-safe and what makes the depth-bound fact possible.
  • Walkthrough - one field, one internal constructor, and public async Task HandleAsync(...) => await _awarder.AwardAsync(cancellationToken).ConfigureAwait(false); (:37-:38). The reported chain for it names three nodes and the save: the handler, PointsAwarder, PointsWriter, and SaveChangesAsync.
  • Where it's used - the subject of TransitiveSave_TwoHopsAway_IsFlagged (DomainEventHandlerSaveFitnessTests.cs:34-:45), which asserts all four names appear, and the control in both AllowlistedCollaborator_PrunesTheWalkWithoutHidingDirectSaves (:88-:90) and DepthBound_StopsTheWalk (:104-:106), where it must not appear.

DomainEventHandlerSaveFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:17 · Level 11 · class

  • What it is - the meta-test for the domain-event-handler save rule: seven facts pinning a direct save, a two-hop transitive save with its full chain, an interface call resolved to its implementation, a clean handler, both allowlist behaviors, and the depth bound.
  • Depends on - ArchitectureRules (DomainEventHandlersDoNotSave, DomainEventHandlerSaveFitnessTests.cs:26), its nested FixtureAssemblyMap, the eight types in MMCA.Common.Architecture.Tests.DomainEventSaveFixtures (:1), and Xunit.Sdk.XunitException (:3).
  • Concept introduced - a call-graph walk, and the three things a test of one has to pin. The rule is the most machinery-heavy in the suite: it reads every scannable assembly into Mono.Cecil, builds a CallGraphIndex over them, finds every type implementing the domain-event-handler interface, and breadth-first searches outward from each of its methods for a save, reporting the shortest chain it finds (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:93-:109,:125-:158). Three properties of such a walk need proof and none is visible by reading it. That it reaches far enough: the transitive fact. That it resolves indirection: the interface fact. That it stops: the depth-bound fact. The last is the one teams usually skip, and it matters twice over here, because maxCallDepth defaults to 6 (:79) and a walk with no bound over a real repo's assemblies would not terminate usefully. The reported chain is itself part of the contract, which is why the transitive fact asserts on every node rather than on the handler alone: a report that named only the handler would leave a developer to rediscover the path by hand. [Rubric §6 - CQRS & Event-Driven] is the property; [Rubric §12 - Performance & Scalability] is why the bound exists; [Rubric §14 - Testability] is the technique.
  • Walkthrough - a private const string FixtureNamespace (:19) and a readonly FixtureAssemblyMap (:21) are shared by every fact.
    • DirectSave_InTheHandler_IsFlagged (:23-:32) and TransitiveSave_TwoHopsAway_IsFlagged (:34-:45): the two reach cases, the second asserting the handler, both hops and SaveChangesAsync all appear, each with a because naming what that node proves.
    • SaveBehindAnInterface_IsResolvedToItsImplementation (:47-:56): the indirection case.
    • HandlerThatOnlyMutates_IsNotFlagged (:58-:67): the discrimination case.
    • AllowlistedNamespace_SilencesTheRule (:69-:76): the whole fixture namespace exempted, asserted NotThrow, with the because naming the real use, a cascade a repo accepts while migrating.
    • AllowlistedCollaborator_PrunesTheWalkWithoutHidingDirectSaves (:78-:94): the sharpest fact in the class. Allowlisting PointsAwarder removes TransitiveSavingHandler from the report because the walk stops at the pruned node, and leaves DirectSavingHandler reported because a direct save is detected at the call site. Both halves are asserted, which is what distinguishes a working prune from a broken rule.
    • DepthBound_StopsTheWalk (:96-:107): calls the rule with maxCallDepth: 1 and asserts the direct save is still found while the two-hop save is not, with the because calling the bound the documented limit of the walk.
  • Why it's built this way - every fixture shape maps to one branch of the walk, and the allowlist and depth facts each carry a control assertion so a rule that simply stopped working could not pass them. The rule exists because a handler that saves opens a second write inside the first: it re-enters the change tracker, can raise a fresh event cascade, and persists work the outer transaction may still roll back (rule doc, :23-:29). Handlers mutate state and let the owning unit of work flush it; anything needing an independent write belongs on the outbox (ADR-003).
  • Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of the rule is DomainEventHandlerSaveTestsBase.

CommonArchitectureMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommonArchitectureMap.cs:15 · Level 12 · class

  • What it is - the architecture map for the MMCA.Common framework: it names each package's layer and pins the layer to a concrete assembly, so the shared rule library knows which assembly is Shared, Domain, Application, and so on for this repo.
  • Depends on - ArchitectureMapBase (internal sealed class CommonArchitectureMap : ArchitectureMapBase, CommonArchitectureMap.cs:15), the Layer enum, LayerRef, and one anchor type per package (Result, BaseEntity<>, DomainEventDispatcher, ApplicationDbContext, ApiControllerBase, ResultGrpcExtensions, UISharedAssemblyReference, :21-:27).
  • Concept introduced - the map as the single point of repo-specific truth for architecture rules. The rule bodies live once in MMCA.Common.Testing.Architecture and are parameterized by an IArchitectureMap; each repo supplies exactly one map so the same rules run identically across Common, Store, and ADC. Because Common is a module-less framework, every layer is registered as a framework layer via the Framework(...) helper rather than a module layer, and that distinction is load-bearing well beyond bookkeeping: several rules branch on map.ModuleNames.Count to decide whether they are judging a framework or a consumer (see EventScopeFitnessTests), and several others read only the per-module accessors, which is why the self-tests for those use FixtureModuleMap instead of this one. [Rubric §3 - Clean Architecture] assesses whether layer boundaries are explicit and enforced; this map is the machine-readable statement of those boundaries.
  • Walkthrough - RepoToken => "MMCA.Common" (:17) identifies the repo and is what the source-scanning rules use to locate the repo root (they look for {RepoToken}.slnx). DefineLayers() (:19-:28) returns one Framework(Layer.X, anchorType.Assembly) entry per package, using a single anchor type to resolve each assembly (mirrors the old PackageAssemblies helper): Shared, Domain, Application, Infrastructure, Api, Grpc, and Ui (:21-:27). The doc comment (:8-:13) records a deliberate omission: MMCA.Common.UI.Maui (ADR-042) is absent because its four MAUI TFM assemblies cannot load in the ubuntu net10.0 test process, so its UI-plus-Shared boundary is enforced at compile time by EnforceUIMauiLayerBoundary in Source/Build/MMCA.Common.LayerEnforcement.targets and the windows build-maui CI job instead.
  • Why it's built this way - one map per repo keeps the rule bodies DRY and identical everywhere (see the "Architecture Enforcement" section in MMCA.Common/CLAUDE.md); anchoring by type keeps the assembly reference refactor-safe.
  • Where it's used - supplied as Map by every thin *ConventionTests subclass in this unit, used directly by EventScopeFitnessTests as the module-less contrast case (EventScopeFitnessTests.cs:39), by EventUpcasterFitnessTests as the map that owns no upcasters (EventUpcasterFitnessTests.cs:51) and by DomainThrowFitnessTests as the framework's own ADR-013 reference run (DomainThrowFitnessTests.cs:131). It is also the pattern the single-layer fitness maps (SpecTestMap, IdempotencyTestMap, CancellationTestMap, CycleTestMap, FixtureAssemblyMap) collapse.

FrameworkSanityTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FrameworkSanityTests.cs:13 · Level 12 · class

  • What it is - the home for the few architecture checks that are Common-only and do not generalize into the shared rule library: the MMCA.Common.Grpc transport boundary and the placement of the IMessageBus, IJwksProvider, and ILiveChannelPublisher abstractions.
  • Depends on - IMessageBus, IJwksProvider, ILiveChannelPublisher, and the NetArchTest Types query API routed through ArchitectureAssert.
  • Concept introduced - repo-specific sanity next to the shared library. Not every rule fits the parameterized base classes; some assert facts true only of the framework repo. Keeping them in one explicitly-named class documents the boundary between "shared rule applied here" and "Common-only invariant." [Rubric §7 - Microservices Readiness] (transport isolation) and [Rubric §3 - Clean Architecture] (abstraction placement) both apply: gRPC is pure transport and must not couple to Domain, Application, or Infrastructure, and the cross-cutting abstractions must sit in the layer their consumers depend on.
  • Walkthrough - three private static Assembly accessors anchor the Grpc, Application, and Infrastructure assemblies by an anchor type each (:15-:19). Three [Fact]s assert MMCA.Common.Grpc has no dependency on Domain, Application, or Infrastructure (:21-:34) via the AssertNoDependency helper (:51-:59), which runs a Types.InAssembly(...).ShouldNot().HaveDependencyOnAny(...) NetArchTest query and routes the result through ArchitectureAssert.NoViolations (:58). Three more [Fact]s assert placement by comparing the abstraction's declaring assembly against the anchored layer assembly: IMessageBus lives in Application (:36-:39), IJwksProvider in Infrastructure because it handles crypto and PEM material (:41-:44), and ILiveChannelPublisher in Application beside IPushNotificationSender (:46-:49).
  • Why it's built this way - the message-bus abstraction must stay in Application so application code depends on transport through it (extraction boundary, ADR-007); the JWKS provider is crypto and belongs in Infrastructure (ADR-004). These are load-bearing placements, so they get their own asserted facts.
  • Where it's used - an independent class in the Common architecture suite; it has no counterpart in Store or ADC because only Common owns the Grpc package and defines these abstractions.

ModuleIsolationTestsBaseTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleIsolationTestsBaseTests.cs:16 · Level 12 · class

  • What it is - coverage of the module-isolation rule set itself, and in particular of the one rule that closes a gap the six named rules leave open: a module's Domain reaching another module's Application.
  • Depends on - ArchitectureRules (seven isolation rules called by name, ModuleIsolationTestsBaseTests.cs:30,:42-:47,:65), its nested StubMap, and the framework's Infrastructure and Domain assemblies as the two real subjects (:25,:63).
  • Concept introduced - a cross product beats a hand-written pair list, and a test can document why. The six named isolation rules cover Domain, Application, Infrastructure and Api each against their own layer in another module, plus Domain and Application against another module's Infrastructure (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Modules.cs:7-:29). That leaves ten pairs unchecked, and one of them matters: a Sales.Domain -> Catalog.Application project reference passed every gate while compile-coupling two modules that ADR-007 and ADR-008 promise can be extracted separately. The per-module layer rules do not catch it because they forbid only the same module's higher layers, and the compile-time layer guard does not because it only knows MMCA.Common.* references (rule doc, :34-:41; class doc, :8-:14). ModuleInternalLayersAreIsolated (:47) closes it by iterating the full four-by-four product, with Layer.Ui deliberately excluded because a module's UI composing another module's UI is a real, intended arrangement in the shipped apps (:42-:45). MMCA.Common declares no modules, so every one of these rules is vacuous against the real map, which is exactly why a stub map is what makes the pairs assertable at all. [Rubric §7 - Microservices Readiness] is the property (ADR-059); [Rubric §34 - Architecture Governance & Documentation] is the second fact's contribution; [Rubric §14 - Testability] is the technique.
  • Walkthrough - a static readonly StubMap named CrossLayerViolation (:21-:25) declares module "Beta" owning one Domain assembly, and names MMCA.Common.Application as another module's Application layer. The assembly it registers is the framework's Infrastructure assembly, which really does depend on that namespace, so the violation is genuine rather than simulated (comment, :18-:20).
    • ModuleInternalLayersAreIsolated_CatchesADomainReachingAnotherModulesApplication (:27-:33): asserts the cross-product rule throws.
    • TheSixNamedRules_DoNotCoverThatPair (:35-:54): the unusual one. It calls all six named rules against the same violating map and asserts each one does not throw. Its inline comment states the purpose (:38-:39): it documents exactly why the cross-product rule exists, and if one of the six ever does start covering the pair, this test says so and the new rule can be re-scoped. That is a test whose failure is a prompt to think rather than a defect report.
    • ModuleInternalLayersAreIsolated_IsSatisfiedWhenNothingCrossesAModuleBoundary (:56-:68): a second stub map naming a namespace nothing references, over the framework's Domain assembly, asserted NotThrow. The discrimination half.
  • Why it's built this way - proving a gap requires asserting six negatives, which is only legible because they are listed as an array of Action and looped (:40-:53). Writing them as six separate facts would have buried the point. See ADR-015.
  • Where it's used - an independent class in the Common architecture suite. The rules it covers are bound for real by ModuleIsolationTestsBase in the two module-bearing consumer repos; MMCA.Common has no modules, so it runs none of them outside this file.

DomainThrowFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DomainThrowFitnessTests.cs:17 · Level 13 · class

  • What it is - the meta-test for the domain-throw rule: nine facts pinning both offender kinds, the three permitted argument guards, the bare rethrow, the non-throwing control, the UNVERIFIABLE verdict, both allowlist granularities, and a final run of the rule against MMCA.Common's own Domain.
  • Depends on - ArchitectureRules (DomainThrowsOnlyArgumentGuards, DomainThrowFitnessTests.cs:40), its nested FixtureAssemblyMap, CommonArchitectureMap (:131), the six types in MMCA.Common.Architecture.Tests.DomainThrowFixtures (:1), and Xunit.Sdk.XunitException (:3).
  • Concept introduced - the allowlist doing its day job, in the test's own setup. Every other self-test in this assembly points its map at a namespace whose only inhabitants are its fixtures. This one cannot: the rule reads the whole Domain-layer assembly, and this assembly contains two throws that are not fixtures. NavigationContractTests guards its embedded resource with an InvalidOperationException, and the OpenAPI XML-comment source generator emits a transformer into every assembly that references the API layer. Both are allowlisted in a named static array with a doc comment explaining each (:21-:33), and the comment makes the wider point: generated plumbing is exactly what an allowlist is for. The first entry is a fully qualified type name, MMCA.Common.Architecture.Tests.Ui.NavigationContractTests (:31), so the allowlist tracks the test's folder: moving that class under Ui/ moved the string with it. Reading that array teaches more about how the escape hatch is meant to be used than any of the escape-hatch facts do. The final fact is a different kind of assertion again: MMCACommonDomain_HoldsOnlyArgumentGuards (:128-:135) runs the rule against the real CommonArchitectureMap with an empty allowlist and asserts NotThrow, with the because recording the fact that makes it interesting, that MMCA.Common's own Domain is the reference implementation of ADR-013 and its single throw is the ArgumentNullException guard in Specification.cs. That is the framework holding itself to the rule it exports, with no exemptions at all. [Rubric §4 - DDD] and [Rubric §15 - Best Practices & Code Quality] are the properties; [Rubric §34 - Architecture Governance & Documentation] covers the allowlist-with-reasons discipline; [Rubric §14 - Testability] is the technique.
  • Walkthrough - a private const string FixtureNamespace (:19), the NonFixtureThrows array (:29-:33) and a readonly FixtureAssemblyMap (:35) are shared; every fixture fact passes NonFixtureThrows so the fixtures are the only subject.
    • BusinessException_IsFlagged (:37-:46): asserts both the owning type and the exception's full name System.InvalidOperationException appear (:44-:45). Naming the exception is what tells a developer which throw to convert.
    • CustomDomainException_IsAlsoFlagged (:48-:57): the custom-exception case.
    • ArgumentGuards_AreNotFlagged (:59-:68): the permitted-guard case, with a because listing all three exception types and stating why they are exempt, a caller bug rather than a business outcome.
    • BareRethrow_IsNotFlagged (:70-:79) and NonThrowingCode_IsNotFlagged (:81-:90): the two silent cases.
    • ThrowOfAValueBuiltElsewhere_IsReportedAsUnverifiable (:92-:101): asserts the literal "UNVERIFIABLE" and the owning method's type name, the same third-verdict discipline ErrorCatalogFitnessTests pins for the catalog rules.
    • AllowlistedNamespace_SilencesTheRule (:103-:112) and AllowlistedType_SilencesOnlyThatType (:114-:126): the two granularities, each built by spreading NonFixtureThrows and appending one entry, with the type-level fact carrying the control assertion that the other offender is still reported (:124-:125).
    • MMCACommonDomain_HoldsOnlyArgumentGuards (:128-:135): the self-application described above.
  • Why it's built this way - nine facts for one rule is the highest count in the suite, and the reason is that this rule has the widest blast radius: it judges every throw in every Domain assembly of every repo that adopts it, so both its false positives and its blind spots are expensive. Asserting the exception's full name rather than just the type's is what makes the report usable. See ADR-013 and ADR-015.
  • Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of the rule is DomainThrowTestsBase.
  • Caveats / not-in-source - the Microsoft.AspNetCore.OpenApi.Generated allowlist entry (:32) silences a whole generated namespace. If the generator ever emitted a type a developer also wrote into that namespace, its throws would be silenced too; nothing in the class detects that. The sibling entry is a string, not a typeof, so renaming or relocating NavigationContractTests without editing :31 would quietly re-arm the rule against it.

EventScopeFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventScopeFitnessTests.cs:13 · Level 13 · class

  • What it is - the ownership-scoping guard for the integration-event rules: three facts pinning that a consumer-shaped map neither snapshots nor polices the framework's own events, while the framework's module-less map still covers them at the source.
  • Depends on - ArchitectureRules (BuildIntegrationEventContract and IntegrationEventsResideInSharedIntegrationEventsNamespace, EventScopeFitnessTests.cs:18,:30), CommonArchitectureMap (:39), its own FakeConsumerMap, and BaseIntegrationEvent as the Domain-assembly anchor (:1,:56).
  • Concept introduced - a rule's scope is part of its contract, and ownership decides it. Both event rules ask the same question of a map, and both answer differently depending on whether the map declares modules. IntegrationEvents includes framework layers only when map.ModuleNames.Count == 0 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Events.cs:68-:73), and the residency rule enforces the Shared-layer requirement only when the map is module-bearing (:31,:33). The reasoning is ownership: a framework-shipped event is the framework's contract, gated by the framework's own conventions and public-API baseline, so a consumer's frozen snapshot must neither churn on it nor claim jurisdiction over where it lives. This class is a regression guard, and the class doc names the incident that motivated it (:6-:12): when the framework shipped its first concrete integration event, OutputCacheEvictionRequested, the Helpdesk canary broke. [Rubric §6 - CQRS & Event-Driven] assesses whether event contracts are governed; [Rubric §9 - API & Contract Design] covers the frozen-snapshot mechanism; [Rubric §33 - Developer Experience] is the failure mode being prevented, a framework release that reds every consumer's architecture suite for a change they did not make.
  • Walkthrough
    • ModuleBearingMap_ExcludesFrameworkEvents_FromContractSnapshot (:15-:23): builds the contract from FakeConsumerMap and asserts no line mentions OutputCacheEvictionRequested.
    • ModuleBearingMap_PassesResidencyRule_DespiteFrameworkEvents (:26-:34): asserts the residency rule does not throw for the consumer map, even though the framework's Domain assembly hosts that event outside any Shared assembly (inline comment, :28-:29).
    • ModuleLessMap_StillCoversFrameworkEvents (:36-:44): the other half of the proof, using the real CommonArchitectureMap and asserting the same event is in the contract. Without this third fact the scoping change would be indistinguishable from simply having disabled the rule.
  • Why it's built this way - a scoping fix is exactly the kind of change that silently over-corrects. Pinning both the exclusion and the retention, against a real framework event rather than a fixture one, is what keeps the fix from becoming a hole.
  • Where it's used - an independent class in the Common architecture suite; the rules it scopes are bound for real by EventVersioningConventionTests here and by the per-repo IntegrationEventContractTestsBase subclasses in the consumers.

EventUpcasterFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventUpcasterFitnessTests.cs:12 · Level 13 · class

  • What it is - the meta-test for the two event-upcaster fitness rules: four facts proving the unique-source rule reports a contract claimed twice, that it leaves a legitimate chain alone, that the version rule reports an upcaster pointing at a lower SchemaVersion, and that both rules pass on a map owning no upcasters at all.
  • Depends on - ArchitectureRules (EventUpcastersHaveUniqueSourceTypes and EventUpcastersIncreaseSchemaVersion, EventUpcasterFitnessTests.cs:53,:55,:61,:68), the fixture contracts and fixture upcasters (:1), its nested UpcasterTestMap, CommonArchitectureMap (:51), plus xUnit and AwesomeAssertions.
  • Concept introduced - proving a rule is not vacuous when the framework itself has nothing to judge. MMCA.Common ships no upcaster of its own, so running either rule over the real CommonArchitectureMap proves only that it does not crash. That is a real property worth pinning (a rule that threw on an empty set would red every consumer that has not adopted upcasting yet), and the fourth fact pins exactly it. But the other three facts have to manufacture their subjects, which is what the UpcasterFixtures namespace and the private map are for. The pattern to take away: a fitness function for a consumer-facing convention is proven in the framework repo against fixtures, and only smoke-tested against the framework's own code. ProtoContractFitnessTests, CommandValidatorCoverageFitnessTests and ServiceContractPurityTests sit in the same position for their rules. [Rubric §6 - CQRS & Event-Driven] and [Rubric §9 - API & Contract Design] are the properties defended; [Rubric §14 - Testability] is the technique.
  • Walkthrough - two private helpers each run one rule against a fresh UpcasterTestMap and return the thrown message: RunUniqueSourceRule (:59-:64) and RunSchemaVersionRule (:66-:71). Because the fixtures always contain an offender for each rule, both helpers can assert Should().Throw<Exception>().Which.Message unconditionally and let the facts make positive and negative Contain assertions against the one string.
    • UniqueSourceRule_FlagsTheContestedContract_ButNotTheCompliantLadder (:14-:25): asserts the message names the contested contract and both rival upcasters (:19-:21), which is what makes the failure actionable, and that the compliant first rung is absent (:22-:24).
    • UniqueSourceRule_DoesNotFlag_AnUpcasterWhoseSourceIsAnotherUpcastersTarget (:31-:33): the sharp one, given its own fact and its own doc comment (:27-:30). The compliant ladder's middle contract is both a target and a source; that is a chain, not a duplicate claim, and the rule must leave it alone.
    • SchemaVersionRule_FlagsTheBackwardsUpcaster_ButNotTheCompliantLadder (:35-:46): names the offender (:40) and, notably, pins the message text "must declare a HIGHER SchemaVersion" (:41-:43), because the wording is what tells a developer which direction is allowed. Both clean rungs are asserted absent (:44-:45).
    • BothRules_Pass_OnAMapThatOwnsNoUpcasters (:48-:57): runs both rules against the real framework map and asserts NotThrow, with the because stating that the framework ships no upcaster so the rule passes vacuously (:54).
  • Why it's built this way - the two rules exist because an upcast chain that is not a function, or that runs backwards, produces a bug that only appears when an old outbox row is replayed, potentially long after the change that caused it (ADR-090, building on ADR-010). Catching it at build time is worth a fixture namespace.
  • Where it's used - an independent class in the Common architecture suite. The shipped binding of both rules is EventConventionTestsBase, whose two facts call them for every repo (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/EventConventionTestsBase.cs:23,:26), and which MMCA.Common activates through EventVersioningConventionTests.

FakeConsumerMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventScopeFitnessTests.cs:50 · Level 13 · class

  • What it is - a consumer-shaped architecture map: the framework's Domain assembly registered as a framework layer plus one module layer, which is the minimum that makes a map module-bearing.
  • Depends on - ArchitectureMapBase (private sealed class FakeConsumerMap : ArchitectureMapBase, EventScopeFitnessTests.cs:50), LayerRef, the Layer enum, and BaseIntegrationEvent as the anchor for the framework Domain assembly (:56).
  • Concept introduced - the Module(...) factory, and what a single module entry changes. Every other map in this chapter uses only Framework(...), whose LayerRef.Module is the empty string (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:94-:95). Module(name, layer, assembly) sets it to the module name and derives the root namespace as {RepoToken}.{module}.{segment} (:98-:99), and ModuleNames is computed from exactly those non-empty entries (:27-:32). One module layer is therefore all it takes to flip a map from "framework" to "consumer" for every rule that branches on ownership. Building that shape from the framework's own assemblies, rather than referencing a real consumer, keeps the regression guard inside MMCA.Common's CI where it can run before a release.
  • Walkthrough - RepoToken => "MMCA.FakeConsumer" (:52), deliberately not MMCA.Common, so the derived module namespace looks like a consumer's. DefineLayers() (:54-:58) is a yield-based iterator returning two entries: Framework(Layer.Domain, typeof(BaseIntegrationEvent).Assembly) (:56), which brings the framework's real integration event into the map, and Module("Fake", Layer.Shared, typeof(EventScopeFitnessTests).Assembly) (:57), a stand-in module Shared layer that ships no integration events of its own (class doc, :46-:49). That combination is the exact situation the guard exists for: a consumer whose map can see a framework event but does not own it.
  • Where it's used - the input to the first two facts of EventScopeFitnessTests (:18,:31).
  • Caveats / not-in-source - the class doc says the stand-in module Shared layer ships no integration events (:46-:49), but the assembly it points at is this whole test assembly, which does declare the eight upcaster fixture contracts. Those fixtures live in a *.IntegrationEvents namespace precisely so the residency fact still passes (EventUpcasterFixtures.cs:9-:13), and the contract fact only asserts the absence of the framework's event, so neither fact is affected. The doc comment is narrower than the code.

NamespaceCycleTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/NamespaceCycleTests.cs:9 · Level 13 · class

  • What it is - the MMCA.Common binding of the namespace-acyclicity rule: a thin subclass that supplies the repo's map and then spends twenty-nine lines arguing, edge by edge, for the one namespace cycle the framework accepts.
  • Depends on - NamespaceCycleTestsBase (public sealed class NamespaceCycleTests : NamespaceCycleTestsBase, NamespaceCycleTests.cs:9) and CommonArchitectureMap (:11); the rule body and its fact counts live in the base, and the rule's behavior is proven against fixtures by NamespaceCycleFitnessTests.
  • Concept introduced - an allowlist that must name a whole strongly connected component, not an edge. A cycle-breaking exemption is the easiest place in an architecture suite to hide a design failure, so this override is written to make hiding expensive. AllowedCycleNamespaces lists three namespaces, not three edges, because the allowance covers the entire SCC: a fourth namespace joining the tangle changes the component and still fails the test (:37-:38). And the justification sits in the compiled file next to the list, so a diff that adds a fourth entry is a diff that has to argue against three paragraphs. [Rubric §15 - Best Practices & Code Quality] is the property being defended; [Rubric §34 - Architecture Governance & Documentation] is the escape-hatch-with-reasons discipline (ADR-015).
  • Walkthrough - two members. Map (:11) is the usual override. AllowedCycleNamespaces (:42-:47) returns MMCA.Common.Infrastructure, MMCA.Common.Infrastructure.Persistence and MMCA.Common.Infrastructure.Messaging (:44-:46), the accepted root -> Messaging -> Persistence -> root tangle inside a single assembly and a single package (:14-:16). Its doc comment (:13-:41) takes the three edges one at a time:
    • root -> Messaging (:19-:20): DependencyInjection lives in the root namespace and binds the buses and their settings, which is what a composition root is for.
    • Messaging -> Persistence (:23-:27): the buses are the outbox transport. InProcessEventBus and BrokerEventBus enqueue OutboxMessage rows and wake the OutboxProcessor through IOutboxSignal, the consumers resolve the physical source through IDataSourceResolver, and the outbox reports through BrokerMetrics and reads MessageBusSettings. Splitting the two would put half of one delivery guarantee on each side of a package boundary.
    • Persistence -> root (:30-:34): the EntityTypeConfiguration* shims carry [UseDataSource] and [UseDatabase], marker attributes that stay in the root namespace because consumers annotate their own configurations with them. Pushing them down into Persistence would deepen the public annotation surface for every consumer in order to fix an internal graph edge.
  • Why it's built this way - a comment closes with the history of the exemption itself (:38-:40): before the 2026-09 feature-by-folder reorganization the third node was MMCA.Common.Infrastructure.Settings, that folder dissolved into the features it configured, and TenancySettingsValidator, which carried the old Settings -> Persistence edge, now lives under Persistence/Tenancy. The list moved because the code moved, which is the argument for keeping the allowlist in a compiled file rather than a wiki page nobody re-reads.
  • Where it's used - run by the MMCA.Common.Architecture.Tests suite in CI's build-and-test job (fast, no database). Store and ADC subclass the same base with their own maps and their own allowlists.

ServiceContractPurityTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ServiceContractPurityTests.cs:11 · Level 13 · class

  • What it is - the MMCA.Common binding of the [ServiceContract] purity rule: a two-line subclass that supplies the repo's map so any type marked as part of a published wire surface is checked for dependencies on the producing service's Domain, Application or Infrastructure.
  • Depends on - ServiceContractPurityTestsBase (public sealed class ServiceContractPurityTests : ServiceContractPurityTestsBase, ServiceContractPurityTests.cs:11), IArchitectureMap and CommonArchitectureMap (the single override, :13), and indirectly ServiceContractAttribute, which the rule matches by full name rather than by reference.
  • Concept introduced - a rule that is deliberately vacuous today, kept as a ratchet. Most gates in this chapter earn their place by failing on real code. This one asserts nothing in MMCA.Common, because the framework marks no type with [ServiceContract], and both the subclass doc (:9-:10) and the base's remarks (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ServiceContractPurityTestsBase.cs:12-:17) say so plainly. The value is in the timing, the same argument the three ratchet subclasses rest on: the invariant is enforced from the first marked type onward, with no test for anyone to remember to write, at the moment when a contract package's shape is still cheap to change. Two design choices follow from that. It is attribute-driven rather than Layer.Contracts-driven, because no repo registers that layer today and a layer-iterating rule would pass vacuously forever (base remarks, :9-:11); and it scans every assembly the map registers, so a marked type is judged wherever it lives. A marked type sitting inside a Domain, Application or Infrastructure assembly then fails by construction, which the rule's own remarks call the intent: a published contract belongs in a *.Contracts or Shared assembly, not inside the service it describes (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:26-:29). [Rubric §7 - Microservices Readiness] is the property: a contract that leaks a domain entity, a handler abstraction or a persistence type forces every consumer to take the producer's internals as a package dependency, which is what makes an extraction irreversible (:16-:18). [Rubric §9 - API & Contract Design] covers the wire surface itself, and [Rubric §34 - Architecture Governance & Documentation] the ratchet.
  • Walkthrough - one member, protected override IArchitectureMap Map { get; } = new CommonArchitectureMap(); (:13). Everything else is inherited: the base contributes a single [Fact], ServiceContracts_ShouldNotDependOn_ServiceInternals (ServiceContractPurityTestsBase.cs:24-:26), which calls ArchitectureRules.ServiceContractsDoNotDependOnServiceInternals(Map). The rule first derives the forbidden namespace set from the map's Domain, Application and Infrastructure layers (ArchitectureRules.Contracts.cs:57-:66) and returns immediately when that set is empty (:35-:38), then walks every layer, selects marked types through the Mono.Cecil custom rule CarriesServiceContractAttribute (:44,:69-:74), and asserts ShouldNot().HaveDependencyOnAny(forbidden) per layer with the layer's root namespace in the message (:42-:52).
  • Why it's built this way - matching the marker by its full-name string, "MMCA.Common.Shared.Abstractions.ServiceContractAttribute" (ArchitectureRules.Contracts.cs:10-:11), is the same zero-reference idiom the rest of the rule library uses: the testing package deliberately takes no compile dependency on the framework assemblies it inspects. The rule complements, and does not replace, the transport- and layer-purity rules that guard the same boundary from the layer side (ADR-007, ADR-015).
  • Where it's used - run by the MMCA.Common.Architecture.Tests suite in CI's build-and-test job. Its siblings in this chapter are ProtoContractFitnessTests and ContractImplementationTests, the other consumer-facing contract gates the framework exercises without owning any subject of its own.
  • Caveats / not-in-source - because the framework marks no type, this class asserts nothing today; there is no fixture proving the rule fires. That proof exists only in whichever repo first marks a type.

SoftDeleteEnforcementTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:19 · Level 13 · class

  • What it is - the MMCA.Common binding of the hard-delete rule, and the one thin subclass in this unit whose override is a substantial, individually-argued document: the four framework types that are allowed to erase rows, each with the reason written next to it.
  • Depends on - SoftDeleteEnforcementTestsBase (public sealed class SoftDeleteEnforcementTests : SoftDeleteEnforcementTestsBase, SoftDeleteEnforcementTests.cs:19) and CommonArchitectureMap (:21).
  • Concept introduced - the rule turned back on its own author, and why that keeps a downstream allowlist honest. Store and ADC subclass the same base and allowlist exactly these four framework types, because no module of theirs erases a row of its own. Running the rule here is what keeps that list correct: a NEW framework eraser would otherwise land in the packages and fail downstream, in a repo whose maintainers did not write it and cannot judge it, rather than in the repo where the change was made (class doc, :10-:17). The second decision in that doc is finer: each entry is named individually rather than exempting the MMCA.Common.Infrastructure.Persistence namespace, so a fifth eraser under that namespace still fails here and gets reviewed. A namespace exemption would have been two characters shorter and would have silently pre-approved every future eraser in the framework's persistence layer. [Rubric §8 - Data Architecture] and [Rubric §30 - Compliance/Privacy/Data Governance] are the properties (ADR-005); [Rubric §34 - Architecture Governance & Documentation] is the allowlist-with-reasons discipline; [Rubric §33 - Developer Experience] is the failure-location argument.
  • Walkthrough - two members. Map (:21) is the usual override. AllowedHardDeleteTypes (:24-:47) is the substance, four fully-qualified entries each preceded by its justification:
    • EFRepository<TEntity, TIdentifierType> (:31, listed by its arity-suffixed metadata name under MMCA.Common.Infrastructure.Persistence.Repositories), the framework's set-based delete escape hatch. The comment states the calling convention that makes it acceptable (:26-:30): erasing is the caller's explicit ask (the method is named ExecuteDeleteAsync), and it targets derived rows the caller is about to rewrite, never user data carrying an audit or erasure obligation. Allowlisting the implementation rather than the abstraction is what leaves IRepository free to be used everywhere else.
    • OutboxCleanupService (:36, now under ...Persistence.Outbox.Administration), for PurgeAsync, PurgeInboxAsync and SweepDeadLettersAsync. These rows are delivery plumbing with a bounded lifetime, and the sweep is the retention policy; soft-deleting them would grow the table the job exists to bound (:33-:35).
    • AuditTrailCleanupJob (:40), for audit-trail retention. The comment inverts the usual instinct (:38-:39): erasing past the retention window IS the requirement, and keeping an audit row forever is the privacy defect rather than the safeguard.
    • RefreshSessionCleanupService (:46), for refresh-session retention. A session row is framework bookkeeping rather than an aggregate: it carries no IsDeleted flag and no audit stamps, and its content is a credential digest plus the IP and user-agent of a device, so flagging it instead of erasing it would keep a growing record of a data subject's devices past any use for it (:42-:45).
  • Why it's built this way - because the entries are fully-qualified type names, a namespace move is a test failure until the list is updated: the 2026-09 folder reorganization that put the outbox cleanup job under Outbox/Administration had to be reflected here (:36). Four reasons written in a compiled file are also four reasons that get read in a diff when someone adds a fifth. That is the whole argument for an allowlist over a suppression: the list is where the erasure policy actually lives. See ADR-005 and ADR-015; the rule's own behavior is proven against compiled fixtures by SoftDeleteEnforcementFitnessTests.
  • Where it's used - run by the MMCA.Common.Architecture.Tests suite in CI's build-and-test job. The same four entries appear in the Store and ADC subclasses of the same base (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs).

UIArchitectureConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:11 · Level 13 · class

  • What it is - the framework repo's binding of the shared UI-architecture fitness rules: a three-line sealed subclass that supplies the map and inherits both [Fact]s, so MMCA.Common's own shared pages and primitives are held to the code-behind and inline-@code caps.
  • Depends on - UIArchitectureConventionTestsBase (public sealed class UIArchitectureConventionTests : UIArchitectureConventionTestsBase, UIArchitectureConventionTests.cs:11), CommonArchitectureMap and the IArchitectureMap abstraction it satisfies (UIArchitectureConventionTests.cs:13).
  • Concept - cross-references the base-plus-thin-subclass shape of the shared fitness bases (see CancellationTokenConventionTestsBase and AggregateConventionTestsBase for the same arrangement): the rule is authored once in MMCA.Common.Testing.Architecture and re-run per repo by a subclass whose only job is to name the repo. [Rubric §18] is the category the rule's own doc comment cites (UIArchitectureConventionTests.cs:6): it asks whether the container/presentational split is actually held, and this class is the mechanical answer for the framework repo. The caps are convention rather than measurement, so the base marks them virtual and the doc comment says they should move only with a recorded decision (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/UIArchitectureConventionTestsBase.cs:11-:12); this subclass overrides none of them.
  • Walkthrough - one member: protected override IArchitectureMap Map { get; } = new CommonArchitectureMap(); (UIArchitectureConventionTests.cs:13). Everything else is inherited. The map is the only thing the base needs from a repo: it takes Map.RepoToken, finds the repo root by walking up to {RepoToken}.slnx, and scans that repo's Source/ tree (UIArchitectureConventionTestsBase.cs:89, :91-:92), skipping obj/ and bin/ plus any ExcludedPathFragments (:97-:99, empty here since the subclass does not override :41). The two inherited facts then run: CodeBehinds_StayWithinTheLineCap globs *.razor.cs and fails any file over MaxCodeBehindLines = 400 (:44, :46, :22), first asserting at least MinimumCodeBehindFiles = 1 file was discovered so a glob that matched nothing cannot pass vacuously (:35); RazorFiles_KeepInlineCodeBlocksSmall globs *.razor, measures each file's inline block from the first line starting with @code to end of file, and fails anything over MaxInlineCodeLines = 120 (:63, :65, :29).
  • Why it's built this way - the caps catch the failure mode directly (a ballooning code-behind is the visible symptom of page logic that belongs in an injected UI service or an extracted sub-component, UIArchitectureConventionTestsBase.cs:5-:10), and putting them in CI makes the split enforced rather than review-enforced. Measuring the @code block as "the tail of the file" is a deliberate convention shortcut (:24-:27) rather than a Razor parse.
  • Where it's used - discovered and run by xUnit as part of MMCA.Common.Architecture.Tests; nothing constructs it. Its two sibling bindings are the identical subclasses in the consumer repos, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:10 (map: AdcArchitectureMap) and MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:9 (map: StoreArchitectureMap), neither of which overrides a cap either.

UpcasterTestMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventUpcasterFitnessTests.cs:74 · Level 13 · class

  • What it is - a one-layer architecture map used only by EventUpcasterFitnessTests: it registers this test assembly as the map's single Application layer so the two upcaster rules see the fixture upcasters and nothing else.
  • Depends on - ArchitectureMapBase (private sealed class UpcasterTestMap : ArchitectureMapBase, EventUpcasterFitnessTests.cs:74), LayerRef, and the Layer enum.
  • Concept - cross-references the map concept from CommonArchitectureMap, with one detail specific to these rules. Both of them scope by ownership exactly as the integration-event rules do: EventUpcasters includes framework layers only when the map declares no modules (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:62-:64). Because this map is built entirely from Framework(...) entries, ModuleNames is empty and the single Application layer is scanned, which is what puts the fixtures in scope. Registering a module layer instead would silently exclude them and both rules would pass vacuously, so the choice is load-bearing rather than incidental. Compare FakeConsumerMap and FixtureModuleMap, which flip exactly that switch on purpose for rules that scope the other way.
  • Walkthrough - RepoToken => "MMCA.Common" (:76) and a one-entry DefineLayers() returning Framework(Layer.Application, typeof(EventUpcasterFitnessTests).Assembly) (:78-:79). The doc comment states the intent in one line (:73). The layer's derived root namespace is unused by these rules, which enumerate ConcreteClasses across whole assemblies (ArchitectureRules.Upcasters.cs:67) rather than namespace-scoped subsets.
  • Where it's used - constructed on every call of the test class's two private rule helpers (EventUpcasterFitnessTests.cs:61,:68).

CrossServiceDataSource

MMCA.Common.Testing · MMCA.Common.Testing.Fixtures · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:15 · Level 0 · sealed record

  • What it is: a two-field record naming one logical data source a cross-service fixture routes to its own physical database: the logical name the framework's DataSources configuration section keys on (normally the module name) and the database that name resolves to on the shared SQL Server container (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:8-15).
  • Depends on: nothing. A positional sealed record of two strings, declared above CrossServiceFixtureBase in the same file.
  • Concept: it is the declarative half of database-per-service (ADR-006, taught in primer §2) expressed as test data. The doc's own examples are the shape to hold onto: LogicalName is Conference, DatabaseName is ADC_Conference (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:13-14). [Rubric §8, Data Architecture] assesses whether each service owns its own store; this record is how a test fixture states that ownership once and derives everything else from it.
  • Walkthrough: two positional members, LogicalName and DatabaseName (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:15), so it gets structural equality and immutability for free. The base consumes each instance three ways: the database name drives the pre-create loop (CrossServiceFixtureBase.cs:213), and the logical name drives both environment keys SetNamedDataSource pushes, DataSources__{LogicalName}__SQLServerConnectionString and DataSources__{LogicalName}__SQLServerMigrationsAssembly (CrossServiceFixtureBase.cs:272-275).
  • Where it's used: as the DataSources list a subclass supplies (CrossServiceFixtureBase.cs:60); ADC declares three, Identity, Conference and Engagement (MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/CrossServiceFixture.cs:62-67), and Store declares two, Catalog and Sales (MMCA.Store/Tests/Integration/MMCA.Store.CrossService.IntegrationTests/Infrastructure/CrossServiceFixture.cs:56-60).

DependencyInjectionAssert

MMCA.Common.Testing · MMCA.Common.Testing.Support · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/DependencyInjectionAssert.cs:13 · Level 0 · class (static)

  • What it is: a one-method assertion helper for the DI registration extensions every module and layer exposes. It proves a registration extension hands back the very IServiceCollection it was given, so a fluent chain stays intact.
  • Depends on: AwesomeAssertions and Microsoft.Extensions.DependencyInjection (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/DependencyInjectionAssert.cs:1-2). No first-party dependency.
  • Concept introduced, the fluent-contract guard. The framework's registration methods are fluent by convention: hosts chain AddApplication().AddInfrastructure(...).AddAPI(...). An extension that returns a new collection silently drops every registration chained after it, and no other test catches that, because the dropped services are simply absent rather than wrong (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/DependencyInjectionAssert.cs:6-11). [Rubric §14, Testability] assesses whether an invariant can be checked cheaply; this one turns an otherwise invisible composition failure into a one-line test. [Rubric §15, Best Practices & Code Quality] covers the convention itself: the return-the-same-collection contract is what lets host composition stay declarative.
  • Walkthrough
    • ReturnsSameCollection(Func<IServiceCollection, IServiceCollection> register) (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/DependencyInjectionAssert.cs:21-32): null-guards the delegate (:23), creates the ServiceCollection itself so the call site stays one line (:25, the doc shows the shape at :16-18), invokes the registration under test (:27), and asserts result.Should().BeSameAs(services, ...) with a because-reason that spells out the consequence of failing (:29-31).
    • Reference equality is the whole assertion. It deliberately says nothing about what was registered; the per-module tests that call it assert their own service descriptors separately.
  • Why it's built this way: creating the collection inside the helper is what keeps adoption free. A module's DI test adds one line per registration extension rather than three lines of arrange plus an assertion nobody remembers to write.
  • Where it's used: seven module DI test classes across the two apps, for example MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.Infrastructure.Tests/DependencyInjectionTests.cs:29, MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.API.Tests/DependencyInjectionTests.cs:63,68, MMCA.Store/Tests/Modules/Catalog/MMCA.Store.Catalog.API.Tests/DependencyInjectionTests.cs:68,73, MMCA.Store/Tests/Modules/Identity/MMCA.Store.Identity.API.Tests/DependencyInjectionTests.cs:49,54, MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.API.Tests/DependencyInjectionTests.cs:26,31, and MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.Application.Tests/DependencyInjectionTests.cs:35. MMCA.Common self-tests the helper, including that it fails for an extension returning a different collection (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/DependencyInjectionAssertTests.cs:13).

EntityBuilderBase<TBuilder, TEntity>

MMCA.Common.Testing · MMCA.Common.Testing.Builders · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Builders/EntityBuilderBase.cs:9 · Level 0 · class (abstract)

  • What it is: the tiny root of the framework's fluent test-data builders. A subclass fixes sensible defaults for one entity type so a test only has to state the properties it actually cares about, then calls Build() to materialize the entity through its real domain factory.
  • Depends on: nothing first-party, and no BCL surface beyond object. Two type parameters and one abstract method is the whole type (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Builders/EntityBuilderBase.cs:9-18).
  • Concept introduced, the Test Data Builder plus the self-referencing generic (CRTP). [Rubric §14, Testability] assesses how easily the code can be exercised in isolation; a builder base is a textbook §14 affordance, it removes the copy-pasted setup that otherwise bloats every arrange step. The signature EntityBuilderBase<TBuilder, TEntity> where TBuilder : EntityBuilderBase<TBuilder, TEntity> (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Builders/EntityBuilderBase.cs:9-10) is the curiously-recurring template pattern: a concrete builder passes itself as TBuilder, so the WithX(...) methods a subclass adds can return the concrete builder type and keep a fluent chain strongly typed without a cast.
  • Walkthrough
    • Build() (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Builders/EntityBuilderBase.cs:17): the single abstract member. The XML doc (EntityBuilderBase.cs:12-15) records the contract, the subclass calls the entity's Result-returning factory (ADR-013) and throws if it failed, so a builder never yields a domain object that violated its invariants. The base deliberately owns no state and no default WithX helpers, those live on each concrete builder because defaults are per-entity.
  • Why it's built this way: keeping the base to one abstract method means it adds zero coupling and zero opinions beyond "a builder produces a TEntity". The CRTP is the only structural rule it enforces, and it exists purely so fluent chaining stays type-safe down in the subclasses.
  • Where it's used: the domain-test builders in both apps subclass it, ten today: MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Domain.Tests/Builders/ActivityBuilder.cs:10, .../Builders/EventBuilder.cs:10, .../Builders/SessionBuilder.cs:10, .../Builders/SpeakerBuilder.cs:10, .../Builders/SponsorBuilder.cs:11, MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Domain.Tests/Builders/UserBuilder.cs:10, MMCA.Store/Tests/Modules/Catalog/MMCA.Store.Catalog.Domain.Tests/Builders/CategoryBuilder.cs:10, .../Builders/ProductBuilder.cs:10, MMCA.Store/Tests/Modules/Identity/MMCA.Store.Identity.Domain.Tests/Builders/CustomerBuilder.cs:11, and MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.Domain.Tests/Builders/OrderBuilder.cs:11.
  • Caveats / not-in-source: the "throws on failure" and "sensible defaults" behavior is documented on the base but implemented only in those subclasses, which live outside this unit.

FeatureManagementTestExtensions

MMCA.Common.Testing · MMCA.Common.Testing.Support · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:10 · Level 0 · class (static)

  • What it is: a one-method helper that lets an integration-test host force feature-flag values, overriding whatever appsettings.json would otherwise resolve, so a test can pin a flag on or off and assert both branches of a feature-gated command or query.
  • Depends on: BCL and NuGet only, IServiceCollection and IConfiguration from Microsoft.Extensions.* plus AddFeatureManagement from Microsoft.FeatureManagement (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:1-3). No first-party dependency.
  • Concept: this is the test-side counterpart to the framework's FeatureGateCommandDecorator<TCommand, TResult>, the outermost link in the CQRS pipeline (taught in primer §2). [Rubric §14, Testability] again: a gated handler is only meaningfully testable if a test can flip its flag deterministically. [Rubric §17, DevOps & Deployment] applies too, feature management (ADR-031) is a cross-cutting concern, and this helper keeps its test-time configuration in one reusable place.
  • Walkthrough
    • The whole class body is a single C# preview extension(IServiceCollection services) block (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:12), the same extension-member style the framework uses for DI registration (see primer §4), not a classic this-parameter extension method.
    • ConfigureTestFeatureFlags(Dictionary<string, bool> features) (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:35-62): null-guards the dictionary (:38), then layers the flags on top of the configuration the host already registered rather than replacing it. It finds the last IConfiguration descriptor and reads its ImplementationInstance (:40-42), seeds a ConfigurationBuilder from it when one was found (:44-48), appends the flags as in-memory FeatureManagement:{Name} keys so they win over any FeatureManagement section the host configured (:50-56), registers the composed root as the IConfiguration singleton (:58), calls AddFeatureManagement against its FeatureManagement section (:59), and returns the collection for chaining (:61).
    • The layering is the load-bearing detail, and the doc says why (:18-25): .NET DI hands a non-collection dependency the last registration, so building a flags-only root here would give every component constructed afterwards a configuration containing nothing but FeatureManagement, silently losing connection strings, authentication settings and the data-source section.
    • The pick-up is instance-only by design (:26-31): a configuration registered behind a factory cannot be read without building the provider, so in that case the flags stand alone.
  • Why it's built this way: pushing overrides through the real IConfiguration plus AddFeatureManagement path (rather than mocking an IFeatureManager) means the test exercises the same feature-evaluation code the production host runs, only the source of the flag value changes.
  • Where it's used: it is intended for a test WebApplicationFactory's ConfigureServices, and the XML doc says exactly that (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:14-17). MMCA.Common covers all three behaviors directly: that the flags layer on top of a host configuration (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/FeatureManagementTestExtensionsTests.cs:20), that they drive a resolved IFeatureManager (:43), and that a collection with no configuration at all still gets the flags (:62).
  • Caveats / not-in-source: as of this pass no application test project calls it. A workspace-wide search finds ConfigureTestFeatureFlags only at its definition (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:35) and in MMCA.Common's own unit tests; neither ADC, Store nor Helpdesk uses it. It ships in the package as available capability, not as a technique any application suite currently uses.

IIntegrationTestFixture

MMCA.Common.Testing · MMCA.Common.Testing.Fixtures · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IIntegrationTestFixture.cs:8 · Level 0 · interface

  • What it is: the contract every integration-test fixture implements, the two capabilities a test base needs from a booted host: hand me an HttpClient, and reset the database to clean between tests.
  • Depends on: BCL only (HttpClient, Task). No first-party dependency, which is what lets it sit at Level 0 and be referenced by everything above it.
  • Concept introduced, the test fixture as an abstraction boundary. [Rubric §14, Testability]: by depending on this interface rather than a concrete WebApplicationFactory, the reusable IntegrationTestBase<TFixture> stays host-agnostic, and each downstream app supplies its own concrete fixture with its own Program, JWT keys, and data sources. This is the boundary that keeps the shared test scaffolding in MMCA.Common.Testing and the app-specific wiring in each repo, which is the whole premise of ADR-058.
  • Walkthrough
    • CreateClient() (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IIntegrationTestFixture.cs:11): returns an HttpClient configured for the in-process test server.
    • ResetDatabaseAsync() (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IIntegrationTestFixture.cs:19): resets the database between tests (the doc names Respawn as the typical mechanism). The doc comment (IIntegrationTestFixture.cs:13-18) records a load-bearing rule for the database-per-service topology (ADR-006): a host with multiple physical data sources must reset every relational source, and can enumerate them by resolving IEntityDataSourceRegistry and IDataSourceResolver from the host's services.
  • Why it's built this way: two members, no state, no host coupling. The interface is deliberately minimal so the reset strategy (single database versus multi-source) is the fixture's problem, not the base's.
  • Where it's used: implemented by SqlServerIntegrationTestFixtureBase<TEntryPoint> (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/SqlServerIntegrationTestFixtureBase.cs:27) and through it by every per-service fixture in both apps; consumed as the TFixture constraint on IntegrationTestBase<TFixture> (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:14) and therefore by all three contract bases in this unit.

JwtTokenGenerator

MMCA.Common.Testing · MMCA.Common.Testing.Support · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/JwtTokenGenerator.cs:30 · Level 0 · class (static)

  • What it is: a static factory that mints signed JWT bearer tokens for integration tests, plus the matching switch that re-points a test host's Bearer scheme at the same committed key. Together they let a test call an authorized endpoint as any role or user without standing up the real login flow or a reachable JWKS endpoint. Each downstream project wraps the generator with role-specific convenience methods (AdminToken, OrganizerToken, and so on, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/JwtTokenGenerator.cs:11-12).
  • Depends on: BCL and NuGet only, System.Globalization, System.IdentityModel.Tokens.Jwt, System.Security.Claims, System.Security.Cryptography (RSA), Microsoft.AspNetCore.Authentication.JwtBearer (for the options type the second member configures), and Microsoft.IdentityModel.Tokens (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/JwtTokenGenerator.cs:1-6). The generated claim layout mirrors the framework's ITokenService so downstream auth middleware cannot tell a test token from a real one (JwtTokenGenerator.cs:99-102). The userId parameter is typed UserIdentifierType (JwtTokenGenerator.cs:114), the solution-wide identifier alias (ADR-048).
  • Concept introduced, exercising the real RS256 path in tests. [Rubric §11, Security] assesses how authentication and key handling are done; the deliberate choice here is that tests sign with RS256 (SecurityAlgorithms.RsaSha256, JwtTokenGenerator.cs:131) using an embedded RSA-2048 dev keypair, the same asymmetric algorithm production uses, so integration tests run the identical validation code path (ADR-004, taught in primer §2) rather than a weaker HMAC shortcut. [Rubric §14, Testability] covers the ergonomics: deterministic tokens with no per-run key generation, and a host that validates them with no network dependency at all.
  • Walkthrough
    • Public constants (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/JwtTokenGenerator.cs:33-96): DefaultIssuer (https://localhost:6001, line 33), DefaultKeyId (mmca-test-key, line 41, the kid the host advertises on its JWKS document), and the paired DefaultPublicKeyPem (line 49) and DefaultPrivateKeyPem (line 68). The class doc records the wiring contract: test host appsettings set Jwt:SigningAlgorithm=RS256, Jwt:RsaPublicKeyPem, and Jwks:KeyId (JwtTokenGenerator.cs:18-20) so RsaJwksProvider publishes a JWKS entry with the matching kid.
    • GenerateToken(...) (JwtTokenGenerator.cs:112-156): imports the PEM private key into RSAParameters inside a using so the RSA instance can be disposed without invalidating the key held by SigningCredentials (:121-131), assembles the claim set (:137-141) and appends any caller-supplied extras (:143-146), then writes a one-hour token (:148-155). Defaulted parameters mean a caller normally passes only audience, user id, and role (:112-119).
    • The claim set is exactly two claims, ClaimTypes.NameIdentifier carrying the culture-invariant user id and ClaimTypes.Role (:137-141). NameIdentifier is what the JWT bearer handler produces for a real token's sub under its default inbound mapping, so a test token carrying it reaches every reader the way a production one does; the comment above it (:133-136) records that the duplicate custom claim that used to ride alongside it is gone, matching ITokenService, because two claims for one identity can disagree.
    • ConfigureInProcessTokenValidation(JwtBearerOptions options, string audience) (JwtTokenGenerator.cs:170-192): the validation half. It nulls Authority and ConfigurationManager and clears RequireHttpsMetadata (:174-176) to stop OIDC/JWKS discovery outright, imports the public key (:178-183), and pins the token-validation parameters to that static key plus the fixed issuer and the caller's audience (:185-191). The doc explains why it has to exist (:158-167): AddForwardedJwtBearer otherwise fetches the issuer and signing keys from the Identity service's JWKS document through the gateway, which no in-process test topology serves.
  • Why it's built this way: the whole point is fidelity. Tokens are indistinguishable in shape and signing algorithm from production, so auth middleware and role checks are under test rather than stubbed. Splitting the key material into a mint side and a validate side is what lets a single-host fixture and a multi-host cross-service fixture share one committed keypair.
  • Where it's used: tokens are applied to a client through IntegrationTestBase<TFixture>'s SetBearerToken(...) (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:42-44) and wrapped by each app's role-specific token helpers. ConfigureInProcessTokenValidation is called from a PostConfigure<JwtBearerOptions> in the test factories of the non-Identity hosts, in ADC (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Infrastructure/ConferenceTestWebApplicationFactory.cs:49,77, .../MMCA.ADC.Engagement.IntegrationTests/Infrastructure/EngagementTestWebApplicationFactory.cs:56,86, .../MMCA.ADC.Notification.IntegrationTests/Infrastructure/NotificationTestWebApplicationFactory.cs:50,69) and in Store (MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Infrastructure/CatalogTestWebApplicationFactory.cs:40,43, .../MMCA.Store.Sales.IntegrationTests/Infrastructure/SalesTestWebApplicationFactory.cs:47,57), plus the cross-service factories (MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/ConferenceCrossServiceFactory.cs:45, .../EngagementCrossServiceFactory.cs:54, MMCA.Store/Tests/Integration/MMCA.Store.CrossService.IntegrationTests/Infrastructure/CatalogCrossServiceFactory.cs:50, .../SalesCrossServiceFactory.cs:52). It is covered directly by MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/JwtTokenGeneratorTests.cs:39-95.
  • Caveats / not-in-source: the class doc (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/JwtTokenGenerator.cs:22-28) carries an explicit security warning, the embedded keypair is committed to the public git repo and is insecure by design, it exists only to make integration tests deterministic. Production keys are provisioned via user-secrets or Azure Key Vault per JwtSettings.RsaPrivateKeyPem and must never be this keypair.

MmcaGatewayHardeningTestsBase<TEntryPoint>

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MmcaGatewayHardeningTestsBase.cs:39 · Level 0 · class (abstract)

  • What it is: the eight edge-hardening gates a gateway host must pass, authored once and re-run as a thin sealed subclass per gateway. It covers what a host adopts from the shared gateway kit: the per-client-IP rate limiter and its bypass list, an optional tighter named policy on the credential route, the correlation ID stamped and echoed on every response, the per-downstream readiness checks, and the active destination health probe on every cluster (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MmcaGatewayHardeningTestsBase.cs:15-22). The subclass supplies only its own route-table facts.
  • Depends on: RecordingHttpForwarder (not referenced in code, but the registration the base's doc expects a subclass to make, :30-34), plus Microsoft.AspNetCore.TestHost's TestServer, WebApplicationFactory, Microsoft.Extensions.Diagnostics.HealthChecks' HealthCheckServiceOptions, YARP's IProxyConfigProvider / IProxyConfigFilter, AwesomeAssertions and Xunit (:1-10). On the host side the behaviors it asserts come from the gateway kit: GatewayRateLimitingExtensions, GatewayCorrelationMiddleware, GatewayHealthCheckExtensions and GatewayHealthCheckDefaultsConfigFilter.
  • Concept introduced, driving the test server directly instead of through an HttpClient. The rate limit assertions go through TestServer.SendAsync(Action<HttpContext>, CancellationToken) rather than an HttpClient (:22-28), and the reason is a chain of two facts: the kit partitions on Connection.RemoteIpAddress, which a TestServer request leaves null, and the kit deliberately fails open on an unresolvable IP rather than collapsing every unattributable request into one shared bucket (MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Gateway/GatewayRateLimitingExtensions.cs:136-142). A client-driven test could therefore never observe a 429. Setting the connection IP per request also gives each gate its own partition, which is what lets eight gates share one class fixture without disturbing each other. [Rubric §11, Security] and [Rubric §12, Performance & Scalability] are the limiter axis (ADR-019 for the service tier, ADR-088 for the edge tier this base checks); [Rubric §13, Observability & Operability] is the correlation ID; [Rubric §29, Resilience & Business Continuity] is the readiness and active-probe pair; and [Rubric §14, Testability] plus [Rubric §34, Architecture Governance & Documentation] cover the fitness-function form (ADR-015, ADR-058).
  • Walkthrough
    • Client addresses (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MmcaGatewayHardeningTestsBase.cs:42-58): six constants, all inside the RFC 5737 TEST-NET-3 documentation range (203.0.113.0/24). Each gate burns its own address, so the fixture's shared host gives every one a fresh window (:27-28); ForwardedProxyIp (:57) stands in for the ingress proxy in the forwarded-header gate.
    • Abstract knobs, the only four a subclass must supply: Factory (:60), PermitLimit (:67, stated by the subclass rather than read from the kit so an operator can see the number), LimitedPath (:73) and DownstreamServices (:76).
    • Virtual knobs with kit defaults: BypassedPaths (:84-88, /.well-known/jwks.json and /health, exempt because probes and JWKS discovery run at high frequency by design), NamedPolicyPath (:94, null skips that gate) and NamedPolicyPermitLimit (:100), ActiveProbeInterval (:103, 30 seconds) and ActiveProbeTimeout (:106, 5 seconds), ActiveProbePath (:113, /alive and not /health, because readiness on a downstream flips during its own rolling deployment and ejecting a destination for that is the gateway treating a healthy deploy as an outage, :108-112), CorrelationHeader (:116), DownstreamCheckPrefix (:119) and ForwardedForHeader (:122).
    • Route_IsThrottledOnceTheClientExhaustsItsWindow (:124-144): sends exactly PermitLimit requests, then one more, asserting none of the first batch was a 429 and the overflow was (:139-143).
    • NamedPolicyRoute_IsThrottledAtItsOwnTighterAllowance (:146-180): returns early when the host declares no named policy (:151-154), a plain return rather than a declared dynamic skip because that would need xunit.v3.assert, which this shipped fixture library deliberately does not reference (:149-150). Otherwise it exhausts the tighter allowance, asserts the overflow is shed, and then asserts the same client on a route without the policy is still admitted (:174-179), which is what makes the rejection attributable to the route policy rather than the global limiter.
    • BypassedPaths_AreNotThrottledEvenAfterTheWindowIsExhausted (:182-207): guards against an empty bypass list (:185-186), burns the window and proves it is exhausted (:194-195), then asserts every exempt path still answers with something other than 429.
    • The two correlation gates (:209-225 and :227-242): a request with no header must come back with a generated non-blank correlation ID, and a caller-supplied ID must be echoed unchanged, so a trace that started upstream survives the hop through the edge.
    • Readiness_IncludesADownstreamCheckPerService (:244-270): reads the registered HealthCheckServiceOptions (:251-252) and, per downstream, asserts a check named {DownstreamCheckPrefix}{service} exists, is tagged ready, is not tagged live, and has FailureStatus = Unhealthy (:263-268). The comment is the reasoning: a gateway that cannot reach its services must leave the load balancer, but restarting the gateway process fixes nothing about a downstream outage, so the check must never fail liveness.
    • EveryCluster_CarriesAnActiveHealthCheckProbingAlive (:272-306): resolves the proxy config and the registered IProxyConfigFilter chain (:276-277), then runs each raw cluster through every filter (:283-291) because the provider hands out the pre-filter config and the kit's defaults only appear after the filters run, exactly as YARP's config manager does at load time. It then asserts each cluster carries an enabled active health check probing ActiveProbePath at the declared interval and timeout (:296-304).
    • RateLimiter_PartitionsByForwardedClientIp_NotByProxyIp (:308-331): every request arrives from the same connection IP while X-Forwarded-For names a different caller. It exhausts caller A's window, confirms the exhaustion, then asserts caller B behind the same proxy IP is still admitted, which only holds if the forwarded-headers middleware runs before the limiter.
    • Helpers: SendAsync(path, clientIp) (:340-353) and SendForwardedAsync(path, proxyIp, forwardedFor) (:363-378), each shaping an HttpContext on the test server (method, HTTPS scheme, path, connection IP, and for the second one the forwarded header) and returning the response status code.
  • Why it's built this way: the gates are identical across gateways because the kit is; the only genuinely per-host facts are the route table and the configured numbers, so those are the four abstract members and everything else is a virtual with a kit default. Asserting the effective YARP config (post-filter) rather than the raw file is what keeps the health-probe gate honest about what the host will actually do (ADR-089).
  • Where it's used: both gateways, each a sealed subclass over its own Program. ADC (MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GatewayHardeningTests.cs:29-30) states a 120-request allowance (:38), /Events/1 as the limited path (:41), four downstreams (:47-53), /hubs added to the bypass list because a SignalR connection is long-lived (:62-67), and /Auth/login at 30 requests as its named policy (:70, :80). Store (MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/GatewayHardeningTests.cs:32-33) reads its PermitLimit straight out of the bound GatewayRateLimitingSettings options so the suite cannot drift from the section it exercises (:43-44), uses /Products/1 (:47), three downstreams (:53), and adds /Payments/webhook to the bypass list because a throttled Stripe webhook becomes a retry storm and an unreconciled payment (:63-68).
  • Caveats / not-in-source: the gates need a booted gateway host, which MMCA.Common's own test project does not have, so the framework can only guard the base's shape headlessly (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/MmcaGatewayHardeningTestsBaseTests.cs:21): that all eight gate names are still declared as [Fact] (:22-32, :42-53) and that exactly the four route-table members are still abstract (:34-40, :55-66), with an abstract SampleGatewayHardeningTests (:74) as compile coverage for the subclass surface.

ProductionHostApplicationFactory<TEntryPoint>

MMCA.Common.Testing · MMCA.Common.Testing.Fixtures · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:23 · Level 0 · class

  • What it is: the database-free boot path for host-level tests. It is a WebApplicationFactory that pins the hosting environment to Production and hangs on to the started IHost, so a test can both exercise production-only middleware branches and drive the host's own lifetime.
  • Depends on: Microsoft.AspNetCore.Mvc.Testing's WebApplicationFactory<TEntryPoint> (extended, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:23) and Microsoft.Extensions.Hosting's IHost / IHostBuilder (ProductionHostApplicationFactory.cs:1-2). No first-party dependency.
  • Concept introduced, the second boot path. The integration tier has two ways to get a running host: SqlServerIntegrationTestFixtureBase<TEntryPoint> for hosts that need a real database, and this one for hosts that do not (a YARP reverse-proxy gateway is the usual case, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:17-20). Both are named as the two paths in ADR-058. [Rubric §11, Security] is the reason Production is pinned: the restrictive CORS policy, HSTS emission, and other production-only middleware are branches a default Development boot skips entirely, which is exactly where host misconfiguration hides (ProductionHostApplicationFactory.cs:10-13). [Rubric §14, Testability] covers the second half, capturing the host is what makes a lifetime test possible at all.
  • Walkthrough
    • StartedHost (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:30): a public property with a private setter, nullable because WebApplicationFactory builds its host lazily, so it stays null until the first client is created (:25-28).
    • CreateHost(IHostBuilder builder) (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:33-40): null-guards the builder (:34), calls builder.UseEnvironment("Production") (:36), then assigns and returns base.CreateHost(builder) (:37-38). Three lines of override, and the assignment is the entire reason the class exists.
  • Why it's built this way: IHost.StopAsync is not reachable through the WebApplicationFactory surface alone (ProductionHostApplicationFactory.cs:13-15), so a graceful-shutdown test has no handle to pull without this capture. The class is deliberately left unsealed and non-abstract so it can be used directly as an xUnit IClassFixture<...> with no subclass.
  • Where it's used: as the default factory of GracefulShutdownTestsBase<TEntryPoint> (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:32), directly as the class fixture of both gateway security-header tests (MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/SecurityHeadersTests.cs:11-12, MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/SecurityHeadersTests.cs:12-13), and as the base of ADC's gateway-hardening fixture, which subclasses it only to swap in the recording forwarder (MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GatewayHardeningTests.cs:96-105).
  • Caveats / not-in-source: the doc is explicit that a host which migrates or seeds on startup needs its own fixture (ProductionHostApplicationFactory.cs:17-20); this factory does nothing about a database.

RateLimiterTestExtensions

MMCA.Common.Testing · MMCA.Common.Testing.Support · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RateLimiterTestExtensions.cs:11 · Level 0 · class (static)

  • What it is: a one-member helper that replaces a test host's global rate limiter with an unlimited partition, so a suite that drives dozens of requests through one host in a few seconds is not throttled by the production budget (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RateLimiterTestExtensions.cs:15-19).
  • Depends on: BCL and ASP.NET Core only, System.Threading.RateLimiting, Microsoft.AspNetCore.RateLimiting's RateLimiterOptions, Microsoft.AspNetCore.Http and the DI abstractions (:1-4). No first-party dependency. On the host side, the limiter it neutralizes is the one configured by RateLimitingSettings (ADR-019).
  • Concept introduced, neutralize the budget without removing the middleware. Two design points make this safe to apply everywhere (:20-25). First it runs as a PostConfigure, so it wins over whatever the host registered no matter when the host's own AddRateLimiter ran, which matters because a test factory's ConfigureTestServices can execute before or after the host's registration. Second only the global limiter is replaced: the limiter middleware stays in the pipeline, so MiddlewarePipelineOrderTestsBase still sees the RateLimiting step, and per-endpoint named policies are untouched, which is what lets a suite like MmcaGatewayHardeningTestsBase<TEntryPoint> still assert a named policy engages. [Rubric §14, Testability] assesses whether a production safety control can be stood down for a test without deleting it; [Rubric §12, Performance & Scalability] is the control itself.
  • Walkthrough
    • The class body is one C# preview extension(IServiceCollection services) block (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RateLimiterTestExtensions.cs:13), the same extension-member style the framework uses for DI registration (see primer §4).
    • NeutralizeGlobalRateLimiter(string partitionName = "tests") (:32-39): a single PostConfigure<RateLimiterOptions> that sets GlobalLimiter to a PartitionedRateLimiter.Create<HttpContext, string> returning RateLimitPartition.GetNoLimiter(partitionName) for every request (:34-36), then returns the collection for chaining (:38).
    • partitionName only ever surfaces in limiter diagnostics (:27-30), which is why the call sites use it as a label ("integration-tests", "cross-service-tests") rather than for behavior.
  • Why it's built this way: a production per-IP or per-user budget and a test suite firing its whole arrange phase through one in-process host are simply incompatible, and the alternatives are worse: removing the middleware would break the pipeline-order fitness test, and raising the configured limit would leave the suite silently coupled to a number in appsettings.
  • Where it's used: eight call sites, all in ADC, four per-service test factories (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Infrastructure/ConferenceTestWebApplicationFactory.cs:63, .../MMCA.ADC.Engagement.IntegrationTests/Infrastructure/EngagementTestWebApplicationFactory.cs:72, .../MMCA.ADC.Identity.IntegrationTests/Infrastructure/IdentityTestWebApplicationFactory.cs:47, .../MMCA.ADC.Notification.IntegrationTests/Infrastructure/NotificationTestWebApplicationFactory.cs:57) and four cross-service factories (MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/ConferenceCrossServiceFactory.cs:50, .../EngagementCrossServiceFactory.cs:59, .../IdentityCrossServiceFactory.cs:36, .../NotificationCrossServiceFactory.cs:63). MMCA.Common covers all three behaviors: that it overrides a host limiter which would otherwise reject every request (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/RateLimiterTestExtensionsTests.cs:21), that it returns the same collection for chaining (:48), and that it still applies when the host configures its limiter afterwards (:61).
  • Caveats / not-in-source: no MMCA.Store or MMCA.Helpdesk test project calls it today; a workspace-wide search finds no NeutralizeGlobalRateLimiter reference under either repo's Tests/ tree.

SecurityHeadersTestsBase

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:16 · Level 0 · class (abstract)

  • What it is: a one-test conformance base that asserts a booted host emits the hardened set of security response headers on every response, so a later pipeline refactor cannot silently drop them. Authored once, re-run as a thin subclass per host under test.
  • Depends on: AwesomeAssertions and Xunit (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:1-2). It deliberately does not extend IntegrationTestBase<TFixture>: it needs only an HttpClient, so it takes one through an abstract factory rather than inheriting the SQL fixture machinery.
  • Concept: a runtime conformance check on the HTTP edge. [Rubric §11, Security] and [Rubric §26, Front-End Security] both assess defense in depth at the edge; this test pins the exact header values the shared AddCommonSecurityHeaders / UseCommonSecurityHeaders middleware (see SecurityHeadersMiddleware, ADR-023) is expected to emit. [Rubric §14, Testability] covers the reusable-base shape.
  • Walkthrough
    • ProbePath (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:19): overridable, defaults to /alive because the liveness endpoint always answers independent of any backend being reachable, so the header check is never flaky for the wrong reason (rationale in the class doc, :12-14).
    • AliveResponse_CarriesHardenedSecurityHeaders (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:21-36): the single [Fact]. It GETs ProbePath (:26-27, threading TestContext.Current.CancellationToken) and asserts six headers (:29-35): X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, a Permissions-Policy containing geolocation=(), a Content-Security-Policy containing frame-ancestors 'none', and (because the host under test boots in the Production environment) an HSTS Strict-Transport-Security header with a max-age=.
    • CreateClient() (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:42): abstract, the subclass supplies it from its WebApplicationFactory class fixture. Header(...) (:44-45) is the private helper that joins a header's values or returns null when the header is absent, which is what makes a missing header fail with a readable null-versus-expected message.
  • Why it's built this way: pinning literal header values (not just presence) turns "we harden responses" into an executable, per-host guarantee, and probing /alive keeps the test independent of application state. Booting the subclass fixture in Production is what makes the HSTS assertion valid, which is why the two adopters pair it with ProductionHostApplicationFactory<TEntryPoint>.
  • Where it's used: both gateway hosts subclass it with a single CreateClient override, MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/SecurityHeadersTests.cs:11-12 and MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/SecurityHeadersTests.cs:12-13, each also taking a ProductionHostApplicationFactory<Program> as its xUnit class fixture on the same declaration.

ServiceBusEmulatorFixtureBase

MMCA.Common.Testing · MMCA.Common.Testing.Fixtures · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ServiceBusEmulatorFixtureBase.cs:54 · Level 0 · class (abstract)

  • What it is: the shared scaffolding for the Azure Service Bus emulator broker-parity test tier. It is a collection fixture that starts ONE warm emulator container (plus the companion SQL Server container Testcontainers.ServiceBus provisions automatically) for the whole tier and, optionally, owns the single MassTransit bus every test in that tier publishes through (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ServiceBusEmulatorFixtureBase.cs:11-18).
  • Depends on: Azure.Messaging.ServiceBus and its .Administration admin-plane client, MassTransit (v8 by policy), Testcontainers.ServiceBus and xUnit's IAsyncLifetime (:1-7). The package trio is documented in the project file as Docker-only, alongside the same note for the Testcontainers pair the cross-service tier needs (MMCA.Common/Source/Hosting/MMCA.Common.Testing/MMCA.Common.Testing.csproj:32-38). No first-party dependency.
  • Concept introduced, broker parity as its own tier, shaped entirely by emulator quotas. The cross-service tier (CrossServiceFixtureBase) proves the outbox round-trip over RabbitMQ; production runs Azure Service Bus, and ADR-066 is where that dev/prod split and this base are recorded. Four quota facts drive the whole design:
    • The emulator allows about 10 connections per namespace and throttles the admin plane to roughly one operation per second, so one warm container and serial tests are the stable shape (:12-18).
    • The image is pinned to 2.0.1 because the HTTP management plane MassTransit provisions topology through shipped in 2.0.0; a silent downgrade to a 1.x tag would leave the broker unusable rather than merely older (:20-23, :56-61). [Rubric §32, Dependency & Supply-Chain] is exactly this kind of pin.
    • The emulator rejects entities whose TTL or auto-delete exceed its one-hour quota, and MassTransit v8's defaults are far above it, so the static constructor lowers three process-global MassTransit defaults (:70-77). That override is process-wide, which is why this tier belongs in its own test process (:23-26).
    • Provisioning is expensive, so the bus lives on the fixture, never on the test class (:29-39). [Rubric §6, CQRS & Event-Driven] and [Rubric §7, Microservices Readiness] are what the tier actually proves (a real contract reaches a real subscription over the production transport); [Rubric §14, Testability] and [Rubric §33, Developer Experience] cover shipping this as a base instead of a hand-copied fixture per repo.
  • Walkthrough
    • Constants and state: DefaultEmulatorImage (:61), AdminPlanePort 5300 (:64), the container, AMQP client and bus fields (:66-68), and the static constructor lowering DefaultMessageTimeToLive, BasicMessageTimeToLive and AutoDeleteOnIdle to one hour (:70-77).
    • Public surface for the tests: Client (:80-81, the AMQP data-plane client, throwing a clear InvalidOperationException before startup), AdminClient (:87), BusControl (:95-97, named BusControl rather than Bus so it does not shadow MassTransit's static Bus.Factory, and throwing a message that names the fix when no queue was declared), the shared Consumed bag (:103, safe because each test matches on a value unique to itself), and HostAddress (:106, sb://localhost/).
    • Virtual knobs: EmulatorImage (:112), the three wall-clock budgets ContainerStartTimeout (4 minutes, :118, sized for a cold image pull on a CI runner), BusStartTimeout (3 minutes, :121) and BusStopTimeout (1 minute, :124), and ReceiveQueueName (:131, null by default for a fixture that wants the two clients and no bus).
    • ComposeAdminConnectionString(hostname, mappedAdminPort) (:145-148): public static and pure, so a fixture's connection-string composition is unit-testable with no container (:134-140). It exists because the container module's own connection string targets the mapped AMQP port, so the admin plane needs its own against the mapped 5300 port.
    • InitializeAsync (:151-209): builds the container inside the method rather than a field initializer so a subclass can be constructed on a machine with no Docker daemon and EmulatorImage is read as a virtual member (:153-158); runs PHASE 1 bounded by WaitAsync(ContainerStartTimeout) (:162) and rethrows a named TimeoutException that tells the reader it is the companion-SQL startup and not admin-plane provisioning (:164-172); constructs both clients (:174-176); returns early when no queue is declared (:178-182); builds the bus with the v8 custom-clients Host(...) overload, the only v8 path onto the emulator (:184-194), with exactly ONE receive endpoint (:193); then runs PHASE 2 bounded by WaitAsync(BusStartTimeout) (:198) with its own named TimeoutException naming the next lever (:200-208).
    • The bounded phases are not just about failing sooner (:41-46): a step killed by the CI job timeout has its output discarded, so a hang leaves no evidence of which phase hung. A bounded phase throws, the step completes, and its log survives. WaitAsync rather than a CancellationToken because the observed hang does not honor cancellation. [Rubric §13, Observability & Operability] is this diagnosability argument applied to CI itself.
    • DisposeAsync (:212-239): best effort in reverse order of creation, swallowing a bus-stop timeout because the container teardown takes the whole namespace with it anyway (:220-228), and null-guarding each member because either timeout path leaves the later ones unassigned.
    • ConfigureReceiveEndpoint(IServiceBusReceiveEndpointConfigurator endpoint) (:247-249): the empty virtual hook where a subclass binds one endpoint.Handler<TContract> per contract. Defaulting to no handler provisions the queue and nothing else.
  • Why it's built this way: the class doc records the incident behind the shape (:29-39). A test class implementing IAsyncLifetime is re-instantiated per [Fact], so a bus created there starts once per test and every start re-provisions the whole topology through the throttled admin plane; with two contracts and two tests that was four provisioning cycles per run, and MMCA.ADC's job hung and was killed at its timeout on 7 of 7 scheduled runs (2026-07-21 to 2026-07-24). Hoisting the bus to the fixture makes it one cycle per run, and provisioning exactly one receive endpoint means every extra contract costs a topic and a subscription rather than another queue.
  • Where it's used: one sealed fixture per app, each supplying only its queue name and contracts. ADC (MMCA.ADC/Tests/Integration/MMCA.ADC.ServiceBusEmulator.IntegrationTests/Infrastructure/ServiceBusEmulatorFixture.cs:22) declares the adc-sb-emulator-smoke queue (:25,28) and binds two real integration-event contracts, UserRegistered and SpeakerLinkedToUser (:37-46); Store (MMCA.Store/Tests/Integration/MMCA.Store.ServiceBusEmulator.IntegrationTests/Infrastructure/ServiceBusEmulatorFixture.cs:14) declares store-sb-emulator-smoke (:17) and binds ProductVariantChanged (:20-25). Each file also carries the tier's [CollectionDefinition] class (ADC :51-56, Store :29-34), which a collection definition must be, being per test assembly by construction (:49-52 on the base). The smoke tests consume the fixture through that collection (MMCA.ADC/Tests/Integration/MMCA.ADC.ServiceBusEmulator.IntegrationTests/ServiceBusRoundTripSmokeTests.cs:25-26, MMCA.Store/Tests/Integration/MMCA.Store.ServiceBusEmulator.IntegrationTests/ServiceBusRoundTripSmokeTests.cs:25-26). MMCA.Common covers the container-free half of the base (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Fixtures/ServiceBusEmulatorFixtureBaseTests.cs:15): the image pin (:21), the admin connection-string composition (:30), and a private ProbeFixture subclass (:109).
  • Caveats / not-in-source: this tier needs a Docker daemon, and the container images are pulled at run time. Where each repo schedules the tier, and whether it gates a deploy, is a CI decision recorded outside this class.

TestPolling

MMCA.Common.Testing · MMCA.Common.Testing.Support · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/TestPolling.cs:9 · Level 0 · class (static)

  • What it is: a poll-with-timeout helper for asynchronous integration assertions. It repeatedly probes a value until a condition holds or a budget runs out, and hands back the last probed value either way.
  • Depends on: BCL only (Task, DateTime, TimeSpan). No first-party dependency and no assertion library, so the caller keeps ownership of the assertion.
  • Concept introduced, replacing the pre-assert sleep. Anything that travels the outbox to a broker and back, or any other eventually-consistent path, arrives at a time the test cannot know (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/TestPolling.cs:3-8). A fixed Task.Delay before the assertion is both slow and flaky: too short and the suite reds intermittently, too long and every green run pays the worst case. Polling returns as soon as the condition holds and bounds the wait. [Rubric §14, Testability] assesses whether the suite is deterministic; [Rubric §6, CQRS & Event-Driven] is why the problem exists at all, since the outbox (ADR-003) is asynchronous by design and offers no synchronous handle to await.
  • Walkthrough
    • PollUntilAsync<T>(Func<Task<T>> probe, Func<T, bool> isSatisfied, TimeSpan? timeout = null, TimeSpan? interval = null) (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/TestPolling.cs:22-41): null-guards both delegates (:28-29), computes a deadline from the 60-second default budget (:31) and a 500 ms default interval (:32), probes once before the loop (:33), then loops while the condition is unmet and the deadline has not passed (:34-38).
    • The return is the design decision worth noticing: it returns last unconditionally (:40) rather than throwing on timeout, so a timed-out poll still fails on the caller's real assertion message rather than on a bare timeout exception (the doc states exactly this at :11-14).
  • Why it's built this way: bounding the wait and returning the last value keeps two properties at once, a fast green path (the loop exits on the first satisfying probe) and a diagnosable red path (the failure message describes the domain expectation, not the plumbing).
  • Where it's used: the cross-service tiers of both apps route every eventual assertion through it and say so in their own docs (MMCA.Store/Tests/Integration/MMCA.Store.CrossService.IntegrationTests/Infrastructure/CrossServiceTestBase.cs:15 and the ADC equivalent at MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/CrossServiceTestBase.cs), with call sites such as MMCA.Store/Tests/Integration/MMCA.Store.CrossService.IntegrationTests/CrossService/ProductVariantChangedRoundTripTests.cs:28,48,51. MMCA.Common covers the helper itself, including the null-argument guards (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/TestPollingTests.cs:15-64).

CrossServiceFixtureBase

MMCA.Common.Testing · MMCA.Common.Testing.Fixtures · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:41 · Level 1 · class (abstract)

  • What it is: the shared scaffolding for the cross-service real-broker integration tier. It boots several service hosts in ONE process against a real Testcontainers SQL Server and a real Testcontainers RabbitMQ, so the genuine outbox to broker to consumer round-trip (and any real cross-service gRPC read) is exercised end to end rather than faked (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:17-25).
  • Depends on: CrossServiceDataSource (the per-source declaration), plus Microsoft.Data.SqlClient, Testcontainers.MsSql, Testcontainers.RabbitMq, and xUnit's IAsyncLifetime (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:1-4,41).
  • Concept introduced, the multi-host in-process topology and its configuration channel. Where SqlServerIntegrationTestFixtureBase<TEntryPoint> boots one host with the cross-service edges faked and no broker, this base owns a whole topology. Two mechanisms are load-bearing, and both are documented on the class. First, process environment variables are the only override channel these hosts honor (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:26-39): each host reads its connection string, MessageBus settings, and JWT settings from builder.Configuration at configure-time, before builder.Build(), which is before WebApplicationFactory.ConfigureAppConfiguration deltas apply, so in-memory config would arrive too late. Second, because the one genuinely per-host key is the SQL connection string, hosts must boot strictly sequentially, and that is safe precisely because a booted host has already snapshotted its connection (the data-source resolver, the context factory, the outbox processor, and the MassTransit bus are all built during StartAsync). [Rubric §7, Microservices Readiness] assesses whether extracted services really do collaborate over their declared transports; [Rubric §6, CQRS & Event-Driven] covers the outbox path (ADR-003); [Rubric §8, Data Architecture] covers database-per-service (ADR-006); and [Rubric §14, Testability] covers shipping the whole topology as a reusable base.
  • Walkthrough
    • State: the private DummyBearerAuthority constant (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:45), the original-environment snapshot map (:47), the two nullable containers (:49-50), and the public RabbitMqConnectionString (:53).
    • Subclass knobs: DataSources (:60, the logical sources in the order their databases are created), MigrationsAssemblyPrefix (:67, so each named source gets {prefix}.{LogicalName} exactly as production does), BootHostsAsync (:143) and DisposeHostsAsync (:146), plus the optional hooks OnContainersStartedAsync (:152) and ConfigureSharedEnvironment (:161).
    • SqlServerBaseConnectionString (:70-72) throws a clear InvalidOperationException when the container has not started, and the public static ComposeConnectionString(...) (:82-96) overlays a catalog, TrustServerCertificate, and an optional Application Name. It is deliberately pure and static so a fixture's connection-string composition is unit-testable with no Docker daemon (:74-81); BuildConnectionString (:168-169) is the instance shorthand over it.
    • InitializeAsync (:99-116) is the whole lifecycle in seven steps: create the containers, start both in parallel with Task.WhenAll (:102), read the broker connection string (:104), run the subclass hook (:105), pre-create the databases (:111), push the shared environment (:113), then boot the hosts (:115). The pre-create step carries the sharpest comment in the file (:107-110): CREATE DATABASE runs before EF's migration lock (sp_getapplock) is acquired, so a host booted twice (the real-Kestrel double-boot pattern) would otherwise race itself; with the databases already present EF skips the create and the migration lock serializes the actual migration run.
    • SetSharedEnvironment (:227-258) pushes ASPNETCORE_ENVIRONMENT=Testing (:229), one named data source per module (:243-246), the real broker settings MessageBus__Provider=RabbitMq and ConnectionStrings__rabbitmq (:249-250), and the dummy Bearer authority that only has to exist so AddForwardedJwtBearer's authority guard passes (:255; real validation is re-pointed at the committed test key by JwtTokenGenerator.ConfigureInProcessTokenValidation, :252-254), before handing control to the subclass (:257).
    • The named-source loop is the multi-host fix, and its comment (:231-242) is worth reading in full: EF Core caches a context type's model in a process-global cache keyed by (context type, source name). If every host let its entities collapse onto Default (as production does, one host per process), all hosts here would share ONE cached model and the first booted would win. SetNamedDataSource (:265-276) therefore composes each module's connection string with a distinct Application Name MMCA-{LogicalName} (:270) so it differs ordinally from the top-level ConnectionStrings value and the resolver keeps the named source, giving each host its own model-cache key.
    • SetHostConnectionString (:177) is the one key mutated between sequential boots, and SetEnvironmentVariable (:187-195) records only the first original value per key so re-pushing that key cannot clobber the restore point. DisposeAsync (:119-136) disposes the hosts, then RabbitMQ, then SQL, then calls RestoreEnvironment (:278-286).
    • CreateContainers (:203-204) is built inside a method rather than a field initializer so a subclass can be constructed and its non-container logic unit-tested on a machine with no Docker daemon, and it carries a documented CS0618 suppression for the parameterless Testcontainers module builders (:197-202). CreateDatabasesAsync (:207-225) issues one guarded IF DB_ID(...) IS NULL CREATE DATABASE per source with a scoped CA2100 suppression justified because the database names are the subclass's own compile-time constants (:218).
  • Why it's built this way: the environment-variable channel and the sequential boot are not style choices, they are the only way to give several hosts distinct databases in one process given configure-time configuration reads. Everything genuinely per-app (which databases, how many hosts and in what order, which extra settings) stays behind the four abstract members.
  • Where it's used: one fixture per app, both sealed subclasses, MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/CrossServiceFixture.cs:24 (three databases for three REST hosts at :61-66, migrations prefix MMCA.ADC.Migrations.SqlServer at :69) and MMCA.Store/Tests/Integration/MMCA.Store.CrossService.IntegrationTests/Infrastructure/CrossServiceFixture.cs:29 (two databases at :56-60, prefix MMCA.Store.Migrations.SqlServer at :63). MMCA.Common covers the container-free half of the base through its own private FakeCrossServiceFixture (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Fixtures/CrossServiceFixtureBaseTests.cs:107).
  • Caveats / not-in-source: this tier needs a Docker daemon. Where each repo schedules it is a CI decision recorded outside this class; the base itself says nothing about scheduling. It also proves the round-trip over RabbitMQ, not the production transport, which is what ServiceBusEmulatorFixtureBase exists to cover.

DecoratorPipelineOrderTestsBase<TCommand, TCommandResult, TQuery, TQueryResult>

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:38 · Level 1 · class (abstract)

  • What it is: an opt-in fitness function that builds a real ServiceCollection through a repo's own registration sequence, resolves the decorated command and query handlers out of the built provider, and asserts the runtime object graph nests the decorators in exactly the ADR-014 order.
  • Depends on: ICommandHandler<in TCommand, TResult> and IQueryHandler<in TQuery, TResult> from MMCA.Common.Application.UseCases (MMCA.Common/Source/Hosting/MMCA.Common.Testing/DecoratorPipelineOrderTestsBase.cs:4), plus System.Reflection, Microsoft.Extensions.DependencyInjection, AwesomeAssertions, and Xunit (:1-5).
  • Concept introduced, verifying a decorator chain by unwrapping the constructed graph. The decorator pipeline itself is taught in group-05; what is new here is how you prove it. Scrutor's TryDecorate applies decorators in reverse registration order, so the outermost decorator is the last one registered, and an innocent-looking reorder of the AddApplicationDecorators() lines (or a module scan that runs after it) silently changes runtime behavior with no compile error (class doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:16-19). Rather than inspecting the registration list, this base resolves the service and walks the real chain by reflection (:29-32). [Rubric §6, CQRS & Event-Driven] assesses whether the command/query pipeline is coherent and intentional; [Rubric §2, Design Patterns] assesses correct application of the decorator pattern; [Rubric §14, Testability] covers turning an ordering convention into an executable check; and [Rubric §34, Architecture Governance & Documentation] covers the fact that a decision record is enforced here rather than merely written down (ADR-015 is the general case). It is also the one non-HTTP member of the ADR-058 conformance tier.
  • Walkthrough
  • Why it's built this way: asserting the constructed object graph is strictly stronger than asserting the registration list, it catches a decorator that was registered but never applied (for example because a module scan re-registered the handler afterwards). Comparing simple type names keeps the base free of a compile-time reference to the decorator classes, which live in MMCA.Common.Application.
  • Where it's used: four subclasses today. MMCA.Common self-tests the base against its own registration sequence with a synthetic PingCommand / PingQuery pair (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/DecoratorPipelineOrderTests.cs:24), and the three applications subclass it in their architecture tiers, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/DecoratorPipelineOrderTests.cs:30 and MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/DecoratorPipelineOrderTests.cs:28 over the real Identity ChangePreferencesCommand / GetUserPreferencesQuery pair, and MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/DecoratorPipelineOrderTests.cs:35 over the Tickets pair.
  • Caveats / not-in-source: each subclass pins one representative command/query pair, not every handler, so the guard proves the ordering is right, not that every handler is decorated.

GracefulShutdownTestsBase<TEntryPoint>

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:25 · Level 1 · class (abstract)

  • What it is: a shutdown conformance base. It boots a real host, calls a real IHost.StopAsync under a bounded cancellation token, and asserts the host drained cleanly, firing ApplicationStopping and then ApplicationStopped inside the timeout.
  • Depends on: ProductionHostApplicationFactory<TEntryPoint> (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:32), plus Microsoft.Extensions.Hosting's IHost / IHostApplicationLifetime, Microsoft.Extensions.DependencyInjection, AwesomeAssertions, and Xunit (:1-4).
  • Concept introduced, the bounded-stop drain check. [Rubric §29, Resilience & Business Continuity] (named in the class doc itself, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:10) assesses whether the system survives planned and unplanned interruption; a rolling deploy is the planned one. The failure this catches is a hosted service (a warm-up runner, service discovery, proxy infrastructure) that refuses to drain, which in production does not announce itself: it silently wedges a rolling deploy while the platform waits out its termination grace period (:13-17). [Rubric §13, Observability & Operability] is the operational half, lifetime events firing in order are what a platform's shutdown handling depends on. The recovery-objective framing is ADR-009; the base itself is one of the suites recorded in ADR-058.
  • Walkthrough
    • ShutdownTimeoutSeconds (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:29): virtual, defaults to 20 seconds. This number is the test: a host that drains slower than this fails.
    • CreateFactory() (:31): virtual, returns a plain ProductionHostApplicationFactory<TEntryPoint>. The doc says to override it only when the host needs a fixture beyond a Production-pinned boot (:18-21).
    • Host_StopsGracefully_FiringLifetimeEventsWithinTimeout (:33-62): the single [Fact]. It creates the factory (:36) and holds the disposal as a separate ConfiguredAsyncDisposable (:40), with an inline comment explaining why (:38-39): the shorter await using var factory = ....ConfigureAwait(false) form would retype factory and lose access to CreateClient and StartedHost.
    • It then creates and immediately disposes a client (:42-45), which is what forces the lazy host to build and start, asserts StartedHost is non-null (:47-48), resolves IHostApplicationLifetime (:49), and confirms ApplicationStarted already fired (:50).
    • The stop itself (:55-56): a CancellationTokenSource for ShutdownTimeoutSeconds and await host.StopAsync(timeout.Token). Reaching the next line already means the stop returned cleanly inside the budget, because a wedged hosted service makes the token cancel and StopAsync throw (:52-54).
    • The two closing assertions (:58-61): ApplicationStopping must have fired during the shutdown, and ApplicationStopped must signal once the host has fully stopped, each with its own because-reason.
  • Why it's built this way: the bounded token is the mechanism, not decoration. Without it a host that never drains would hang the test run instead of failing it, which is exactly the production symptom the test exists to surface. Driving a real IHost.StopAsync (rather than asserting registrations) is what makes this a runtime check.
  • Where it's used: both gateways, each a body-less one-line subclass, MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/GracefulShutdownTests.cs:9 and MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GracefulShutdownTests.cs:9. Both run in the no-database host-test tier, so they are in each repo's CI.slnf.

IntegrationTestBase<TFixture>

MMCA.Common.Testing · MMCA.Common.Testing.Fixtures · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:13 · Level 1 · class (abstract)

  • What it is: the workhorse base class every integration test inherits. It owns the per-test HTTP client and lifecycle, typed request helpers, bearer-token management, and a thread-safe id counter, so a concrete test class is left with just its arrange/act/assert.
  • Depends on: IIntegrationTestFixture (the TFixture constraint, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:14), plus Xunit's IAsyncLifetime, System.Net.Http.Headers, and System.Net.Http.Json (:1-3).
  • Concept introduced, the xUnit async test lifecycle and per-test isolation. [Rubric §14, Testability]: the base implements IAsyncLifetime so InitializeAsync runs before each test and DisposeAsync after, and it hangs the database reset off that hook so every test starts from a clean database, the single most important property for reliable integration tests.
  • Walkthrough
    • Fields and properties: a static int _nextId = 1000 seed (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:16), and the Fixture / Client protected properties (:19-22).
    • Constructor (:24-28): stores the injected fixture and eagerly creates the HttpClient from it.
    • InitializeAsync (:31): a ValueTask that awaits Fixture.ResetDatabaseAsync() before each test. DisposeAsync (:34-39): suppresses finalization and disposes the client.
    • Auth helpers: SetBearerToken(string) / ClearAuthentication() (:42-48) set or clear the Authorization header, the hook through which a JwtTokenGenerator token is applied.
    • Typed HTTP helpers: GetAsync<T> (:51-56, which calls EnsureSuccessStatusCode then deserializes), and PostAsync<T> / PutAsync<T> / PutAsync / DeleteAsync (:59-72) returning the raw HttpResponseMessage so a test can assert status codes.
    • NextId() (:75): Interlocked.Increment over the shared seed, so parallel tests never collide on generated ids.
  • Why it's built this way: per-test database reset plus a per-test client is the isolation contract; centralizing the typed helpers keeps individual tests short and consistent. The static Interlocked counter is the cheapest safe way to hand out unique ids under xUnit's parallelism. Note the asymmetry: GetAsync<T> throws on a non-success status while the others hand the response back, which is what makes a read helper terse and a write assertion explicit.
  • Where it's used: the direct base of all three contract test bases in this unit (OpenApiContractTestsBase<TFixture>, ProblemDetailsContractTestsBase<TFixture>, ServiceInfoVersioningContractTestsBase<TFixture>), and of every concrete integration test in the downstream apps.

RecordingHttpForwarder

MMCA.Common.Testing · MMCA.Common.Testing.Support · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RecordingHttpForwarder.cs:29 · Level 1 · sealed class

  • What it is: an IHttpForwarder that never proxies. It echoes the destination prefix, the matched cluster, and the ForwarderRequestConfig it was handed into response headers, so a gateway test can assert which cluster a route targets and which forwarder budget it carries (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RecordingHttpForwarder.cs:8-13).
  • Depends on: YARP only, Yarp.ReverseProxy.Forwarder (IHttpForwarder, ForwarderRequestConfig, HttpTransformer, ForwarderError) and Yarp.ReverseProxy.Model (IReverseProxyFeature), plus Microsoft.AspNetCore.Http and System.Globalization (:1-4). No first-party dependency. The trace headers it reads back are the ones the gateway kit's GatewayTraceHeaderTransformProvider stamps. The Yarp.ReverseProxy package reference lives in MMCA.Common.Testing itself rather than in every gateway test project, and the project file records why (MMCA.Common/Source/Hosting/MMCA.Common.Testing/MMCA.Common.Testing.csproj:43-46).
  • Concept introduced, the echoing fake at a framework extension point. Three ideas are worth taking from this type. First, echoing into response headers rather than storing state is what keeps a singleton fake safe for concurrent requests (:12-13): there is no shared mutable field to race. Second, replacing the DI singleton still intercepts under a config-driven route table, because YARP's own forwarder middleware resolves IHttpForwarder from DI, so MapReverseProxy() goes through the fake exactly as a hand-written MapForwarder call would (:14-22). Third, it runs the real transform pipeline against a throwaway outbound request before echoing (:23-27), so the request transforms are observable without a network hop, which is what makes an assertion about what a downstream would actually receive meaningful. [Rubric §2, Design Patterns] covers the test-double form (a recording fake, not a mock); [Rubric §14, Testability] covers keeping every gateway test off the network, since configured destinations are usually service-discovery names that resolve to nothing in a test host; and [Rubric §7, Microservices Readiness] is the topology this makes testable (ADR-089).
  • Walkthrough
    • Header constants (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RecordingHttpForwarder.cs:32-54): DestinationHeader, ClusterHeader, ActivityTimeoutHeader, VersionHeader, VersionPolicyHeader, plus RouteTraceEchoHeader and ClusterTraceEchoHeader for the two trace headers read back off the transformed outbound request. UnsetValue (:57) is the (unset) marker written when the forwarder config leaves a nullable setting unset, so a test asserts a value either way rather than distinguishing an absent header from an unset setting.
    • RouteTraceHeaderName (:63) and ClusterTraceHeaderName (:69): init properties defaulting to the shared kit's own header names, X-MMCA-Route and X-MMCA-Cluster, so a host with different header names can still use the fake.
    • The five-argument SendAsync overload (:72-78) simply delegates to the six-argument one with CancellationToken.None.
    • The real SendAsync (:81-115): null-guards context, config and transformer (:89-91); writes the destination prefix (:93); reads the matched cluster id off IReverseProxyFeature, falling back to UnsetValue (:94-95); writes the activity timeout, forwarded HTTP version and version policy (:96-101); runs transformer.TransformRequestAsync against a throwaway HttpRequestMessage (:106-108); echoes the two stamped trace headers (:110-111); then sets a 200 and returns ForwarderError.None (:113-114).
    • HeaderOrUnset (:121-124): reads one outbound header, joining multiple values with a comma, or returns the unset marker.
  • Why it's built this way: a gateway test wants to assert routing decisions and forwarder budgets, not the behavior of a backend. Swapping the forwarder is the smallest replacement that leaves the entire gateway pipeline (routing, transforms, limiter, correlation, health) genuinely running, which is what makes the same fixture reusable for both the route-map assertions and the hardening gates.
  • Where it's used: both gateway test projects register it by replacing the singleton. ADC uses it in the gateway-hardening fixture (MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GatewayHardeningTests.cs:103) and in the route-map suite, which aliases all seven header constants at the top of the class (MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/RouteMapTests.cs:42-60, registration at :430); Store does the same in both places (MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/GatewayHardeningTests.cs:115, MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/RouteMapTests.cs:378, with the rationale written out at :21). MMCA.Common covers the fake itself, including the unset-marker paths and the custom trace header names (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/RecordingHttpForwarderTests.cs:17).

SqlServerIntegrationTestFixtureBase<TEntryPoint>

MMCA.Common.Testing · MMCA.Common.Testing.Fixtures · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/SqlServerIntegrationTestFixtureBase.cs:27 · Level 1 · class (abstract)

  • What it is: the reusable fixture that boots a real service host in-process against a throwaway SQL Server database, applies the module's migrations on first start, resets data between tests with Respawn, and drops the database on disposal. It is the concrete engine behind IIntegrationTestFixture for SQL Server hosts.
  • Depends on: IIntegrationTestFixture (implemented, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/SqlServerIntegrationTestFixtureBase.cs:27), plus Microsoft.AspNetCore.Mvc.Testing (WebApplicationFactory), Microsoft.Data.SqlClient, Respawn, and Xunit's IAsyncLifetime (:1-4).
  • Concept introduced, the disposable-database integration fixture and environment-variable overrides. [Rubric §14, Testability] and [Rubric §8, Data Architecture]: real integration coverage needs a real relational database, and this fixture makes that cheap and hermetic, a fresh GUID-named database per fixture, migrated from scratch, Respawned between tests, dropped at the end. The database-per-service routing (ADR-006) is why the class doc stresses the DataSources collapse onto a single overridden connection string (:16-24).
  • Walkthrough
    • State (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/SqlServerIntegrationTestFixtureBase.cs:30-45): the recorded original-environment map, the server-base and database-name strings, the WebApplicationFactory, the Respawner, a _databaseCreated flag, and the public Client / ConnectionString. ConnectionString (:45) is exposed so SQL-fidelity tests can read raw tables (for example to assert an integration event landed in the outbox).
    • Services (:52): the booted host's root service provider, exposed so cross-service tests can resolve a consumer-side integration-event handler or a repository and drive the flow directly against the real database.
    • Abstract knobs: SqlBaseEnvironmentVariable (:58, names the env var holding the CI SQL base connection string), DatabaseNamePrefix (:61), and CreateFactory() (:134, where the subclass builds the host). CreateClient() (:64) satisfies the interface by delegating to the factory.
    • InitializeAsync (:67-96): resolves the server base from SqlBaseEnvironmentVariable or falls back to LocalDB (:69-70), composes a GUID-suffixed database name and connection string (:71-72), forces ASPNETCORE_ENVIRONMENT=Testing and pushes the top-level SQL connection string as environment variables (:75-76), lets the subclass push its own via ConfigureTestEnvironment (:77), and builds the factory (:79); creating the client is what triggers the host's Migrate init to create the database and apply the module's migrations (:81-84). It then builds the Respawner, ignoring __EFMigrationsHistory (:86-95).
    • ResetDatabaseAsync (:99-112): returns immediately when no respawner exists, otherwise opens a connection and calls Respawner.ResetAsync.
    • DisposeAsync (:115-131): disposes client and factory, drops the database when one was created, and restores the environment.
    • ConfigureTestEnvironment (:142-144) is an empty virtual hook receiving the setter delegate. SetEnvironmentVariable (:146-155) records only the first original value per key so re-pushing a key cannot clobber the restore point; RestoreEnvironment (:157-165) puts them all back and clears the map.
    • DropDatabaseAsync (:167-188): clears pooled connections so the database is free to drop (:170), connects to master, and runs a guarded SET SINGLE_USER WITH ROLLBACK IMMEDIATE plus DROP DATABASE (:180-183), with a scoped CA2100 suppression justified because the database name is a server-generated GUID, never user input (:179).
  • Why it's built this way: overrides go through process environment variables because the host reads its connection string at configure-time; forcing the Testing environment skips appsettings.Development.json (which would point DataSources at localhost) so the resolver collapses onto the single overridden top-level connection string, making the fixture behave like a clean single-database monolith. LocalDB-by-default keeps local runs zero-config while CI can point at a SQL service container. Note the contrast with CrossServiceFixtureBase, which needs the opposite (named sources per host so several EF models can coexist in one process).
  • Where it's used: the base of every per-service integration fixture in both apps, four in ADC (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Infrastructure/ConferenceIntegrationTestFixture.cs:18, .../MMCA.ADC.Engagement.IntegrationTests/Infrastructure/EngagementIntegrationTestFixture.cs:17, .../MMCA.ADC.Identity.IntegrationTests/Infrastructure/IdentityIntegrationTestFixture.cs:22, .../MMCA.ADC.Notification.IntegrationTests/Infrastructure/NotificationIntegrationTestFixture.cs:17) and three in Store (MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Infrastructure/CatalogIntegrationTestFixture.cs:16, .../MMCA.Store.Identity.IntegrationTests/Infrastructure/IdentityIntegrationTestFixture.cs:15, .../MMCA.Store.Sales.IntegrationTests/Infrastructure/SalesIntegrationTestFixture.cs:17). Each is then the TFixture for that service's integration and contract tests.
  • Caveats / not-in-source: the fixture needs a reachable SQL Server, so these suites build but do not run without one; they execute in each repo's SQL-service CI job via the *.Integration.slnf filter.

OpenApiContractTestsBase<TFixture>

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:22 · Level 2 · class (abstract)

  • What it is: a contract conformance base that boots a host and asserts its /openapi/v1.json document is served, is a well-formed OpenAPI 3.x document, and still describes the core public resources, so an accidental controller or route removal fails CI instead of silently changing the published contract.
  • Depends on: IntegrationTestBase<TFixture> (inherited, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:22), System.Net, System.Text.Json, AwesomeAssertions, and Xunit (:1-4).
  • Concept introduced, the contract guard on the live document. [Rubric §9, API & Contract Design] assesses whether the API surface is described and kept stable; the pattern across all three Level 2 bases is a live-document guard with no committed snapshot (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:15-17), the assertions run against the document the host actually serves, so new controllers can never leave a stale snapshot behind and a removed one is caught immediately. This is one of the suites recorded in ADR-058, and the one with the widest adoption.
  • Walkthrough
    • Overridable and abstract knobs: OpenApiDocumentPath (:30, defaults to /openapi/v1.json), MinimumPathCount (:37, a coarse floor under the route surface), MinimumPathCountBecause (:44, the failure-message reason), and CorePublicResources (:50, the resource paths that must keep being described).
    • OpenApiDocument_IsServed_AsWellFormedOpenApiDescribingTheApiSurface (:52-65): parses the JSON and asserts openapi starts with 3., info.title is non-empty, a paths object exists, and it holds at least MinimumPathCount entries.
    • OpenApiDocument_DescribesEveryCorePublicResource (:67-85): first guards against a vacuous pass (the subclass must pin at least one resource, :70-71), then checks every CorePublicResources entry is present, matching on name case-insensitively (:77-80) so presence, not exact casing, is the contract.
    • GetOpenApiJsonAsync (:91-100): clears auth (the document is anonymous outside Production), fetches the path, asserts 200 with a message naming the path, and returns the raw JSON.
  • Why it's built this way: asserting the live document (rather than diffing a checked-in snapshot) keeps the guard maintenance-free while still catching the two failures that matter, the document disappearing and a public resource vanishing. The two assertions are deliberately coarse for the same reason: a strict shape diff would fail on every ordinary additive change.
  • Where it's used: subclassed once per REST service host, four in ADC (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/OpenApiContractTests.cs:15, .../MMCA.ADC.Engagement.IntegrationTests/Contract/OpenApiContractTests.cs:15, .../MMCA.ADC.Identity.IntegrationTests/Contract/OpenApiContractTests.cs:16, .../MMCA.ADC.Notification.IntegrationTests/Contract/OpenApiContractTests.cs:17) and three in Store (MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Contract/OpenApiContractTests.cs:15, .../MMCA.Store.Identity.IntegrationTests/Contract/OpenApiContractTests.cs:15, .../MMCA.Store.Sales.IntegrationTests/Contract/OpenApiContractTests.cs:15), each supplying the fixture, the path floor, and the pinned resource list.

ProblemDetailsContractTestsBase<TFixture>

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ProblemDetailsContractTestsBase.cs:22 · Level 2 · class (abstract)

  • What it is: a contract conformance base that asserts a host's error responses are RFC 9457 Problem Details documents, machine-readable bodies carrying status, title, and a diagnostic extension, across both error-shaping paths the framework uses.
  • Depends on: IntegrationTestBase<TFixture> (inherited, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ProblemDetailsContractTestsBase.cs:22), System.Net, System.Net.Http.Json, System.Text.Json, AwesomeAssertions, and Xunit (:1-5). Same live-guard shape as the OpenAPI base above.
  • Concept: still [Rubric §9, API & Contract Design], here the pinned contract is the error shape. The class covers the two distinct paths that produce errors (class doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ProblemDetailsContractTestsBase.cs:11-19): ASP.NET Core model validation (a 400 application/problem+json body) and the framework's HandleFailure Result-error mapping (see ApiControllerBase), which turns a Result failure such as an Error not-found into a 404 problem (ADR-013 defines that edge contract).
  • Walkthrough
    • Validation_400_HasProblemDetailsShape (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ProblemDetailsContractTestsBase.cs:30-40): sends the subclass's validation probe, asserts the shared shape at 400, then checks the problem+json content type and the model-validation-only extensions type, traceId, and errors (:35-38).
    • NotFound_404_HasProblemDetailsShape (:41-47): sends the 404 probe and asserts the shared shape.
    • Abstract probes: SendValidationErrorProbeAsync (:54) and SendNotFoundProbeAsync (:60), the only app-specific pieces, authenticating first when the endpoint requires it (the docs suggest pageNumber=0 against a [Range(1, int.MaxValue)] paged read, and reading an id that does not exist, :49-60).
    • AssertProblemDetailsShapeAsync (:67-83): the shared protected static assertion, JSON content type, echoed status, non-empty title, and at least one diagnostic extension (errors, traceId, or requestId, :76-80), returning the parsed body so a subclass can follow up.
  • Why it's built this way: pinning both the validation path and the HandleFailure path in one base means a regression in either error channel breaks CI, and factoring the shape assertion into a shared static keeps every host's error contract identical while still letting a host with a reachable 409-conflict path layer its own test on top (:16-18).
  • Where it's used: subclassed per host, four in ADC (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/ProblemDetailsContractTests.cs:21, .../MMCA.ADC.Engagement.IntegrationTests/Contract/ProblemDetailsContractTests.cs:17, .../MMCA.ADC.Identity.IntegrationTests/Contract/ProblemDetailsContractTests.cs:17, .../MMCA.ADC.Notification.IntegrationTests/Contract/ProblemDetailsContractTests.cs:16) and three in Store (MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Contract/ProblemDetailsContractTests.cs:20, .../MMCA.Store.Identity.IntegrationTests/Contract/ProblemDetailsContractTests.cs:16, .../MMCA.Store.Sales.IntegrationTests/Contract/ProblemDetailsContractTests.cs:16). ADC Conference is the one that adds a 409 stale-RowVersion conflict test on top of the inherited facts (.../MMCA.ADC.Conference.IntegrationTests/Contract/ProblemDetailsContractTests.cs:40-67, reusing AssertProblemDetailsShapeAsync at :67).

ServiceInfoVersioningContractTestsBase<TFixture>

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ServiceInfoVersioningContractTestsBase.cs:20 · Level 2 · class (abstract)

  • What it is: a contract conformance base that proves the API-versioning machinery actually works across more than one version: that /ServiceInfo is served by both v1.0 (deprecated) and v2.0, selected by the api-version header, and that the host reports supported and deprecated versions in response headers.
  • Depends on: IntegrationTestBase<TFixture> (inherited, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ServiceInfoVersioningContractTestsBase.cs:20), System.Net, System.Text.Json, AwesomeAssertions, and Xunit (:1-4).
  • Concept: [Rubric §9, API & Contract Design] again, the versioning axis (ADR-046). The class doc (:8-17) makes the point that without a second working version the whole versioning story would be untestable, so this base keeps the machinery exercised rather than merely asserted. Because the ServiceInfo controller ships in MMCA.Common.API (ServiceInfoControllerBase), the entire test body is identical across repos; a subclass supplies only its fixture.
  • Walkthrough
    • ServiceInfo_V1_ReturnsMinimalShape_AndIsReportedDeprecated (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ServiceInfoVersioningContractTestsBase.cs:28-42): requests v1.0, asserts 200, checks apiVersion == "1.0" and that the evolved supportedVersions list is absent in the v1 shape (:35-36), then asserts an api-deprecated-versions response header contains 1.0 (:38-40).
    • ServiceInfo_V2_ReturnsEvolvedShape_AndIsReportedSupported (:43-57): requests v2.0, asserts 200, checks apiVersion == "2.0" and that supportedVersions contains 2.0 (:50-52), then asserts an api-supported-versions header advertises 2.0 (:54-56).
    • GetServiceInfoAsync(string apiVersion) (:59-65): clears auth and sends the GET with the api-version header set to the requested version, which is the header-based selection ADR-046 standardizes.
  • Why it's built this way: keeping a real deprecated v1 and a real v2 side by side, and asserting both the payload shapes and the ReportApiVersions headers, is what proves version negotiation is wired end to end rather than configured and forgotten.
  • Where it's used: two adopters today, each a body-less one-line subclass, MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/ApiVersioningTests.cs:15 and MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Contract/ApiVersioningTests.cs:16.
  • Caveats / not-in-source: adoption is per-repo partial, one host each in ADC and Store, not every extracted REST service.

MiddlewarePipelineOrderTestsBase

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:29 · Level 11 · class (abstract)

  • What it is: the HTTP-edge counterpart of DecoratorPipelineOrderTestsBase<TCommand, TCommandResult, TQuery, TQueryResult>. It seeds the framework's default middleware step list, applies the host's own customization if it has one, and asserts the resulting step order is exactly the documented pipeline, plus that the startup-validated adjacency invariants still hold.
  • Depends on: MiddlewarePipelineBuilder and MiddlewarePipelineStepNames from MMCA.Common.API.Startup (MMCA.Common/Source/Hosting/MMCA.Common.Testing/MiddlewarePipelineOrderTestsBase.cs:2), plus AwesomeAssertions and Xunit (:1,3). This is the reference that makes MMCA.Common.Testing depend on MMCA.Common.API (MMCA.Common/Source/Hosting/MMCA.Common.Testing/MMCA.Common.Testing.csproj:53-55).
  • Concept introduced, order-as-data at the HTTP edge. In ASP.NET Core middleware order is behavior, not style, and the failures it produces do not look like ordering bugs. The class doc names three (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:13-18): an unreachable jwks_uri when the pre-forwarded capture drifts away from UseForwardedHeaders, a tenant that never resolves when tenant resolution runs before authentication, and a per-user rate cap that never engages when the limiter runs before authentication. What makes the check cheap is that ADR-079 turned the order into data: MiddlewarePipelineBuilder.CreateDefault() produces a list of named steps that are inert until applied, so no WebApplication has to be built and the test runs in the fast unit tier with no database and no host (:24-27). [Rubric §17, DevOps & Deployment] assesses whether cross-cutting edge concerns are composed deliberately; [Rubric §11, Security] is why authentication before the rate limiter is load-bearing (ADR-019); [Rubric §14, Testability] and [Rubric §34, Architecture Governance & Documentation] cover the fitness-function form itself (ADR-015).
  • Walkthrough
    • Configure (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:35): virtual, defaults to null, meaning the host under test calls the zero-argument UseCommonMiddlewarePipeline() overload. A host that customizes the pipeline overrides this with the same Action<MiddlewarePipelineBuilder> its Program.cs passes (:20-23).
    • ExpectedStepNames (:38-58): the pinned order, outermost first, all eighteen steps named through MiddlewarePipelineStepNames constants rather than string literals: ExceptionHandler, CorrelationId, RequestLocalization, PreForwardedCapture, ForwardedHeaders, HttpsRedirection, ResponseCompression, Routing, Cors, Authentication, TenantResolution, RateLimiting, SoftDeletedUserFilter, Authorization, OutputCache, JwksEndpoint, OidcDiscoveryEndpoint, Controllers. It is virtual, so a host with a deliberately different pipeline states its own order.
    • EdgePipeline_OrdersSteps_InDocumentedOrder (:60-67): the first [Fact]. It builds the seeded builder and asserts builder.StepNames equals ExpectedStepNames, with a because-reason (:66) that spells out the three adjacencies rather than just reporting a list mismatch.
    • EdgePipeline_SatisfiesLoadBearingInvariants (:69-77): the second [Fact]. It asserts builder.Build() does not throw. Build() (MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/Pipeline/MiddlewarePipelineBuilder.cs:258-281) re-checks four invariants at startup, PreForwardedCapture immediately before ForwardedHeaders (:259-262), Authentication immediately before TenantResolution (:264-267), Authentication before RateLimiting (:269-272), and ForwardedHeaders before HttpsRedirection (:274-277), so a pipeline that fails here would have thrown while the host was starting.
    • CreateBuilder (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:79-84): the two-line private helper both facts share, MiddlewarePipelineBuilder.CreateDefault() followed by Configure?.Invoke(builder).
  • Why it's built this way: the two facts are complementary rather than redundant. The first pins the exact order, so any reorder (including one that still satisfies every adjacency) fails visibly; the second re-runs the host's own startup validation in the unit tier, so an override that breaks an adjacency fails in a test rather than at boot. Naming steps through MiddlewarePipelineStepNames constants means a step rename is a compile error in the test rather than a silent string mismatch.
  • Where it's used: four subclasses, every one of them body-less because every host calls the zero-argument overload, MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/MiddlewarePipelineOrderTests.cs:12, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/MiddlewarePipelineOrderTests.cs:16, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/MiddlewarePipelineOrderTests.cs:15, and MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/MiddlewarePipelineOrderTests.cs:15. The framework's own UseCommonMiddlewarePipeline doc points back at this base as the way to freeze the order (MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/WebApplicationExtensions.cs:44).

HandlerTestBase<THandler>

MMCA.Common.Testing · MMCA.Common.Testing.Support · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/HandlerTestBase.cs:38 · Level 14 · class (abstract)

  • What it is: the reusable Moq scaffold for command/query handler unit tests. It hands a derived test class a pre-configured Mock<IUnitOfWork>, a no-op logger typed to the handler under test, and two one-line helpers that register a repository mock into that unit of work.
  • Depends on: IUnitOfWork, IRepository<TEntity, TIdentifierType>, IReadRepository<TEntity, TIdentifierType>, AuditableAggregateRootEntity<TIdentifierType> and AuditableBaseEntity<TIdentifierType> (the two generic constraints), plus Moq, Microsoft.Extensions.Logging, and NullLogger<T> (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/HandlerTestBase.cs:1-5).
  • Concept: the arrange-phase base class. Where IntegrationTestBase<TFixture> gives an end-to-end test a booted host, this gives an isolated unit test a mocked persistence boundary: no database, no host, no HTTP. The class doc (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/HandlerTestBase.cs:10-12) frames it as the shared replacement for the per-test copy-paste of Mock<IUnitOfWork> plus GetRepository wiring plus SaveChangesAsync setup. [Rubric §14, Testability] assesses whether the design permits fast isolated tests; the fact that handlers depend on IUnitOfWork (an Application-layer abstraction) rather than a DbContext is what makes this scaffold possible at all, which is [Rubric §3, Clean Architecture] paying off in the test tier (ADR-055 records that contract). [Rubric §15, Best Practices & Code Quality] covers the deduplication itself.
  • Walkthrough
    • Constructor (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/HandlerTestBase.cs:41-42): a single expression-bodied statement that pre-configures UnitOfWork.SaveChangesAsync(...) to return 1, the success path, so a happy-path test writes no persistence setup at all. Failure-path tests override it with their own Setup (doc :32-35).
    • UnitOfWork (:45): the Mock<IUnitOfWork> every registered repository is wired into, created by a property initializer so the constructor can configure it.
    • Logger (:48): NullLogger<THandler>.Instance, typed by the handler type parameter so it binds directly to the handler's ILogger<THandler> constructor parameter.
    • RegisterRepository<TEntity, TIdentifierType>() (:56-64): creates a Mock<IRepository<TEntity, TIdentifierType>>, wires that same object into both GetRepository<...>() and GetReadRepository<...>() (:61-62), and returns the mock for further Setup / Verify. Constrained to AuditableAggregateRootEntity<TIdentifierType> (:57), that is, to aggregate roots.
    • RegisterReadRepository<TEntity, TIdentifierType>() (:72-79): the read-only counterpart for non-aggregate child entities that expose no read-write repository, constrained to the looser AuditableBaseEntity<TIdentifierType> (:73) and wiring only GetReadRepository<...>() (:77).
  • Why it's built this way: wiring one repository mock into both accessors matters because a handler may read through GetReadRepository and write through GetRepository on the same aggregate; a test forced to register two mocks would have to keep their state in sync. Pre-succeeding SaveChangesAsync encodes the common case so only the interesting deviation appears in a test.
  • Where it's used: 123 test files today, the framework's own scaffold test (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/HandlerTestBaseTests.cs:13) plus 122 ADC Application-tier files spread across all four modules (for example MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Application.Tests/Categories/UseCases/CreateConferenceCategoryHandlerTests.cs:13, MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Application.Tests/LivePolls/UseCases/CastVoteHandlerTests.cs:13, MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Application.Tests/Users/UseCases/ChangePassword/ChangePasswordHandlerTests.cs:13, MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.Application.Tests/UserNotificationExportServiceTests.cs). The class doc carries a worked CreateEventHandlerTests example (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/HandlerTestBase.cs:19-31).
  • Caveats / not-in-source: adoption is uneven. MMCA.Store and MMCA.Helpdesk handler tests do not subclass it at all: a workspace-wide search finds no HandlerTestBase reference under either repo's Tests/ tree. Why those two arrange their handler tests by hand is not recorded in source.

AnonymousEndpointTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:30 · Level 0 · abstract class

  • What it is: an allow-list gate for authorization opt-outs. Every [AllowAnonymous] in the assemblies a subclass names must appear in an explicit, reviewed list, so an endpoint cannot quietly lose its authorization gate.
  • Depends on: [Fact] (xUnit), AwesomeAssertions, RuleHelpers.LoadableTypes, and reflection over attribute instances matched by full name: AllowAnonymousAttribute, ControllerBase, and Blazor's RouteAttribute are three private full-name constants (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:32-34), not compile-time references.
  • Concept introduced, the two-directional allow-list gate. A one-directional "no anonymous endpoints" rule is unusable (login has to be anonymous) and a one-directional "these are allowed" rule rots. This base asserts both halves: no unlisted [AllowAnonymous] exists, and no list entry matches nothing. The second half is the subtle one, since a stale entry hides a renamed or re-gated endpoint behind a permission that is no longer being granted (line 88). [Rubric §11, Security] assesses whether authentication gates stay where they were put; [Rubric §14, Testability] assesses turning a review-only property into an executable one; [Rubric §34, Architecture Governance] applies because the exception list, with its per-entry justification comments, becomes the reviewed record of the anonymous surface.
  • Walkthrough
    • The subclass supplies TargetAssemblies (line 37), AllowedAnonymousEndpoints (line 44, identified as the type's FullName for a type-level attribute and FullName.MethodName for a method-level one, lines 40-42), and a MinimumScannedTypes floor (line 51, default 1).
    • Three [Fact]s. AnonymousEndpoints_AreAllowListed (line 54) subtracts the allow-list from the discovered set and names what is left. ScannedEndpointSet_IsNotEmpty (line 66) is the non-vacuity guard: with no controllers and no routable components discovered, the first assertion passes without having looked at anything (comment, lines 68-69). AllowList_HasNoStaleEntries (line 79) runs the comparison the other way.
    • Two shapes are scanned, both by reflection: IsController (line 101) walks the base chain for ControllerBase, and IsRoutableComponent (line 117) looks for RouteAttribute on the type, combined in IsScannedEndpointType (line 95).
    • AnonymousEndpoints() (line 125) is protected rather than private so a subclass can build a richer report over the same data (doc, lines 120-123); it flattens the assemblies, filters to the scanned shapes, and returns a distinct, ordinally-ordered set.
    • AnonymousEndpointsOf (line 133) is the load-bearing detail: type-level attributes are read with inherit: false (line 135) and methods are enumerated DeclaredOnly (line 142), so a framework base action is reported once at its declaration site instead of once per derived controller in every consumer repo (comment, lines 140-141).
  • Why it's built this way: the class doc is explicit about the limit (lines 18-24). Minimal-API endpoints opt out through the .AllowAnonymous() builder call, which produces endpoint metadata at map time and is invisible to static reflection, so the framework's own minimal-API anonymous surface (JWKS, OIDC discovery, app-association, session-cookie refresh, health) is outside this gate; catching it would need an endpoint-metadata check over a built host. Matching ASP.NET types by full name keeps the package free of an ASP.NET reference, the same stance the whole rule library takes (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/MMCA.Common.Testing.Architecture.csproj:22-29).
  • Where it's used: subclassed in all four repos, and the DeclaredOnly decision is what keeps each list local to what that repo declares. MMCA.Common's AnonymousEndpointTests lists the credential-exchange actions on its controller bases (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTests.cs:14, list at :22, floor of 21 at :43). ADC's covers the two Identity credential actions plus the public conference-browse reads, calendar exports and bookmark counts, with a floor of 79 (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/AnonymousEndpointTests.cs:21, floor at :108). Store's covers the public storefront reads, product images, registration and the Stripe webhook, with a floor of 32 (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/AnonymousEndpointTests.cs:10, floor at :54). Helpdesk's single entry is its whole TicketsController, because the seed ships without an Identity issuer so there is no token to require (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/AnonymousEndpointTests.cs:17, entry and reason at :26-30, floor of 1 at :35). MMCA.Common also carries the adversarial coverage for the base itself in AnonymousEndpointTestsBaseTests, which proves each assertion fails on its own drift through private DriftedTests, StaleAllowListTests and EmptyScanTests subclasses (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:100, :108, :117) plus a conformant control (:126).

ArchitectureAssert

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureAssert.cs:8 · Level 0 · static class

  • What it is: the shared failure-reporting helper for every architecture fitness function, a static class with two NoViolations overloads that turn a rule breach into a readable, offender-listing assertion failure.
  • Depends on: NetArchTest.Rules.TestResult and AwesomeAssertions' Should() fluent API, both global-imported for the whole package (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/GlobalUsings.cs:3-4). No first-party dependencies: this is the bottom of the fitness-function stack.
  • Concept introduced, architecture fitness functions. A fitness function is an automated test that asserts a structural property of the codebase (a layer never references another, a controller is sealed) rather than a behavioral one. This package makes those rules first-class, shared code (ADR-015). [Rubric §14, Testability] assesses how well invariants are guarded by executable checks; ArchitectureAssert is the reporting primitive that makes a failing invariant name its offenders instead of just going red. [Rubric §34, Architecture Governance] assesses whether architectural decisions are enforced rather than merely documented; every rule in this package funnels its verdict through here.
  • Walkthrough
    • NoViolations(NetArchTest.Rules.TestResult result, string reason) (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureAssert.cs:11) returns early when result.IsSuccessful (line 13), otherwise null-coalesces FailingTypes to an empty list, joins the full names into a bullet list (lines 18-19), and asserts IsSuccessful.Should().BeTrue(...) with the reason plus the violation list as the because argument (lines 21-22).
    • NoViolations(IEnumerable<string> violations, string reason) (line 26) materializes the sequence once and asserts list.Should().BeEmpty(...) (line 30), for the reflection-derived, IL-reading and file-scanning rules that produce a plain string list rather than a NetArchTest result.
  • Why it's built this way: the XML doc (lines 3-7) names it the un-drifted successor to the three per-repo ArchitectureTestHelper.AssertNoViolations copies: the reporting logic was duplicated in MMCA.Common, MMCA.Store, and MMCA.ADC, and centralizing it here removes the drift.
  • Where it's used: every rule in ArchitectureRules and several reflection-based test bases call one of these two overloads as their final step.

BrandColorTokenTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/BrandColorTokenTestsBase.cs:13 · Level 0 · abstract class

  • What it is: an abstract xUnit test base that fails the build when a landing-page stylesheet re-hardcodes the brand hex instead of sourcing it from the shared var(--mmca-primary) CSS custom property.
  • Depends on: [Fact] (xUnit), AwesomeAssertions, and Assembly.GetManifestResourceStream (BCL) to read embedded CSS. No first-party type dependency: it operates on the strings the subclass embeds.
  • Concept introduced, the drift fitness function. Unlike a layer rule that reflects over assemblies, a drift function reads committed text (CSS here) and asserts a single source of truth is used. [Rubric §20, Design System & Theming] assesses whether visual tokens have one authoritative definition; this base guards that consumers of the framework palette cannot silently fork the primary color.
  • Walkthrough
    • Two private constants pin the forbidden literal #1565C0 and the required token var(--mmca-primary) (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/BrandColorTokenTestsBase.cs:15-16).
    • The subclass supplies EmbeddedCssLogicalNames (line 22), the manifest-resource names of its landing-page stylesheets.
    • The single [Fact] LandingPageCss_SourcesBrandColorFromToken_NotHardcodedHex (line 25) first asserts the list is non-empty (a non-vacuity guard, lines 27-28), then for each stylesheet reads it via ReadEmbeddedCss (line 56, which throws a clear InvalidOperationException when the resource is missing, lines 58-60) and records a violation when the file is blank (line 37), when the token is absent (line 43), or when the raw hex is present (line 48, matched with OrdinalIgnoreCase so a lowercase spelling cannot slip past).
    • Resources are resolved from GetType().Assembly (line 33), that is, the subclass's assembly, which is what lets a package-shipped base read a consumer's stylesheet.
  • Why it's built this way: the doc (lines 3-12) explains the split. MMCA.Common's own BrandColorTokenTests guards the C#-to-CSS token definition (from BrandColors.Primary), while this base guards every downstream consumer of it, embedding the stylesheets as manifest resources so the package needs no file-system access into the consumer repo.
  • Where it's used: subclassed once per repo that ships a branded landing page, as BrandColorTokenTests in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/BrandColorTokenTests.cs:12) and Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/BrandColorTokenTests.cs:10).

CallGraphIndex

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/CallGraphIndex.cs:20 · Level 0 · internal sealed class

  • What it is: a read-only index over the compiled assemblies an IL-reading rule scans, built once per rule run. It holds every type by full name, every type's transitive ancestor names, and the reverse map from an ancestor name to the scanned types that implement or derive from it.
  • Depends on: Mono.Cecil (ModuleDefinition, TypeDefinition, TypeReference, MethodDefinition, MethodReference), which the package already carries transitively through NetArchTest, plus BCL dictionaries and HashSet<string>. No first-party dependencies.
  • Concept introduced, following an interface call in a static walk. IL records the static callee: a handler that calls IPointsAwarder.AwardAsync emits a callvirt naming the interface method, not the class that does the work. A walk that stops there never reaches the implementation, so a transitive rule would report nothing. The reverse implementor map is what closes that gap (doc, lines 9-13). The second design decision is equally load-bearing: ancestors are recorded by name, not by resolved definition, so a contract declared in an assembly the map does not register (typically a framework interface such as IDomainEventHandler<T>) is still recognized as an ancestor; only the walk through such a type stops, because its metadata is absent (doc, lines 14-18). [Rubric §14, Testability] assesses whether behavioral invariants inside method bodies can be checked at all; this index is the primitive that makes the body-reading rules in this package possible without a Roslyn or IL-rewriting dependency.
  • Walkthrough
    • Three ordinal-keyed dictionaries back the index: _types by full name, _ancestors per type, and _implementors from an ancestor name to the derived types (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/CallGraphIndex.cs:22-24).
    • The constructor (line 26) makes two passes: it registers every type in every module, nested types included (lines 28-31), then computes each type's ancestor set and inverts it into _implementors (lines 33-48).
    • Types (line 52) exposes the whole inventory. ElementFullName (line 55) strips a generic instantiation down to the open definition's name, which is how a reference to a closed generic still matches the type it was declared as.
    • Implements(type, ancestorFullName) (line 58) is the membership test the handler and cascade rules select on. Find(reference) (line 63) resolves a reference to a scanned type or null when it lives outside the scan.
    • TargetsOf(callee) (line 72) is the core: it resolves the declaring type, collects the directly matching methods, and returns them unchanged when the declaring type is neither an interface nor virtual (lines 80-84); otherwise it adds every matching method on every known implementor (lines 86-87). Overload resolution is approximated by name and parameter count, which deliberately over-approximates so no real path is missed (doc, lines 66-71).
    • MatchingMethods (line 94) requires a body and an equal parameter count, and accepts either an exact name match or a .-suffix match, because an explicit interface implementation is named Some.Ns.IFoo.Method in metadata (doc, lines 90-93).
    • ComputeAncestors (line 116) walks base types and interfaces with an explicit stack, adding each name once and descending only into definitions the scan actually contains (lines 122-138), which is both cycle-safe and tolerant of out-of-scan contracts.
  • Why it's built this way: the package's whole stance is zero compile references to the code it governs (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/MMCA.Common.Testing.Architecture.csproj:22-29), and reading Cecil metadata by name is the same discipline applied one level deeper. Building the index once per rule run, rather than resolving references per call site, is what keeps a transitive breadth-first walk over a whole repo's assemblies fast enough to sit in a normal test run.
  • Where it's used: by the three IL-walking rules that need to follow calls rather than just find them: AggregatesCascadeSoftDeleteToChildren (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.CascadeSoftDelete.cs:115) and DomainEventHandlersDoNotSave (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:93), surfaced through CascadeSoftDeleteConventionTestsBase and DomainEventHandlerSaveTestsBase.
  • Caveats / not-in-source: the type is internal, so a consumer repo cannot build its own call-graph rule on top of it; the reach is only through the public rules.

CrossEntityNavigationFinder

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:97 · Level 0 · private sealed class

  • What it is: a private ExpressionVisitor nested in ArchitectureRules that walks a specification's Criteria lambda and collects the names of other entity types it navigates into.
  • Depends on: System.Linq.Expressions.ExpressionVisitor, MemberExpression, PropertyInfo (BCL) and the RuleHelpers extension property InheritsAuditableEntity.
  • Concept introduced, expression-tree inspection as a fitness check. NetArchTest reasons about assembly-level references only; to catch a rule expressed inside a lambda body, the code instantiates the specification, reads its Criteria expression tree, and visits it. This backs the polyglot and database-per-service invariant (ADR-006): a Criteria that navigates to an entity in another physical data source produces an untranslatable join at runtime. [Rubric §8, Data Architecture] assesses cross-source data access discipline; this visitor is how that discipline is machine-checked.
  • Walkthrough
    • The primary constructor captures ownEntityType (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:97), and _navigated is the accumulating HashSet<string> (line 99).
    • Find(Expression body) visits the body and returns the set (lines 101-105).
    • VisitMember (line 107) resolves the accessed property's type through EntityTypeOf (line 121) and, when the result is an auditable entity other than the specification's own type, adds its name (lines 111-115).
    • EntityTypeOf (line 121) treats a direct entity property as a navigation (lines 123-126) and unwraps generic collection navigations such as ICollection<TChild> to their element type (lines 129-136).
  • Why it's built this way: filtering by a foreign-key column is engine-portable; navigating is not (notably on Cosmos, where the cross-source relationship is degraded out of the model). The finder is the enforcement half of ArchitectureRules.SpecificationsDoNotNavigateToOtherEntities (line 24), whose doc and failure message point authors at CrossSourceSpecification instead (line 15, lines 74-76). The rule is deliberately best-effort: only parameterless specifications can be instantiated and inspected (lines 37-41), and a specification whose constructor or Criteria throws on standalone evaluation is skipped rather than failing the suite (lines 49-59).
  • Where it's used: only inside that rule (line 66), which is surfaced through SpecificationConventionTestsBase.

Layer

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:9 · Level 0 · enum

  • What it is: the closed vocabulary of architectural layers a fitness function can reason about: Shared, Domain, Application, Infrastructure, Api, Ui, Grpc, Contracts, ServiceHost (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:11-19).
  • Depends on: nothing; a plain enum.
  • Concept introduced: the Clean Architecture layer taxonomy made into a type. The layer flow itself is taught in primer §1; here it becomes an enum the rule library keys off, so a rule that iterates layers is written once against the enum rather than hard-coded per repo. [Rubric §3, Clean Architecture] assesses whether the layering is explicit and enforced; this enum is the shared alphabet.
  • Walkthrough: the doc (lines 3-8) notes that Ui, Grpc, Contracts, and ServiceHost are optional: a repo simply omits them from its map when absent, so a rule iterating them is vacuously satisfied with no compile dependency on the missing assembly. ArchitectureMapBase.Segment translates each member to its namespace segment, and two of those translations are not the identity mapping: Api becomes "API" and ServiceHost becomes "Service" (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:105-117).
  • Where it's used: carried by LayerRef, projected by IArchitectureMap.OfLayer, and threaded through nearly every method in ArchitectureRules. Contracts is the one member no repo registers today, which is exactly why ServiceContractPurityTestsBase and ContractImplementationTestsBase are attribute-driven rather than layer-driven.

ModuleConformanceTestsBase<TModule>

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleConformanceTestsBase.cs:21 · Level 0 · abstract generic class

  • What it is: a per-module conformance gate. It asserts that one module's Name, Dependencies, and RequiresDependencies still say what the repo expects, and gives the subclass a hook to assert what RegisterDisabledStubs puts in the container.
  • Depends on: [Fact] (xUnit), AwesomeAssertions, and System.Reflection (GetInterfaces, GetProperty, BindingFlags). It has no Map: it reflects over a single module type, not over an assembly inventory. The contract it reads is IModule, matched by the full-name constant "MMCA.Common.Application.Modules.IModule" (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleConformanceTestsBase.cs:24).
  • Concept introduced, gating a silent-drift contract. The three members this base checks are the whole surface ModuleLoader registers on: it resolves the topological (Kahn) registration order from Dependencies, matches ModulesSettings entries by Name, and decides between a hard start failure and stub registration from RequiresDependencies (doc, lines 4-10). Drift in any of the three throws nothing: it silently reorders registration, leaves a module permanently enabled, or swaps a real service for a disabled stub. That is exactly the failure class a fitness function exists for. [Rubric §7, Microservices Readiness] assesses whether module boundaries and their declared dependencies stay honest as modules move between hosts; [Rubric §14, Testability] assesses whether a silent contract is made loud; [Rubric §34, Architecture Governance] applies because the module contract is enforced rather than described. The module system itself is taught in Group 14.
  • Walkthrough
    • The type parameter is constrained where TModule : class, new() (line 22), so the default CreateModule() is just new() (line 68); a module without a parameterless constructor overrides that hook.
    • Three expectation members the subclass declares: the abstract ExpectedName (line 27), and the virtual ExpectedDependencies (line 30, defaulting to the empty list for a leaf module) and ExpectedRequiresDependencies (line 36, defaulting to false).
    • Four [Fact]s. Module_ShouldDeclare_ExpectedName (line 39) compares the read Name, with a because spelling out the consequence: renaming silently disables the module's configuration and drops it from other modules' dependency graphs (line 42). Module_ShouldDeclare_ExpectedDependencies (line 45) casts the value to IEnumerable<string>, asserts it is not null (the shape ModuleLoader sorts on, lines 49-50), then BeEquivalentTo the expectation (lines 52-54). Module_ShouldDeclare_ExpectedRequiresDependencies (line 58) pins the flag that turns a disabled dependency into a startup failure instead of a substituted stub (line 61). Module_ShouldRegister_ExpectedDisabledStubs (line 64) delegates to the AssertDisabledStubs hook.
    • AssertDisabledStubs (line 77) is deliberately empty by default: a module exporting no cross-module contract registers no stubs, so the fact passes vacuously and only a module that does export one overrides it (doc, lines 70-76).
    • The private ReadContractMember (line 81) is the load-bearing mechanism. It finds the IModule interface on the instance by full-name match (lines 85-87), asserts the module implements it at all (lines 89-90), resolves the named public instance property off the interface (line 92), and reads it (line 99). Reading through the interface property dispatches to the module's override when there is one and to the framework's default interface implementation when there is not (comment, lines 97-98).
  • Why it's built this way: the reflection-by-full-name approach is the same discipline the rest of the package uses, and for the same reason: it keeps the package free of the framework's transitive graph (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/MMCA.Common.Testing.Architecture.csproj:22-29). It also buys a second property the doc calls out (lines 11-18): dispatching through the interface property is what asserts a leaf module against IModule's defaults, the reach the hand-written per-repo tests needed an explicit (IModule) cast for.
  • Where it's used: subclassed once per module across the consumer repos, five times today: ADC's NotificationModuleTests (MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.API.Tests/NotificationModuleTests.cs:8, the fullest example: it expects ["Identity"], RequiresDependencies true, and overrides AssertDisabledStubs to assert the IUserNotificationExportService stub descriptor is a singleton) and IdentityModuleTests (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.API.Tests/IdentityModuleTests.cs:5), plus Store's Catalog, Sales, and Identity module tests (MMCA.Store/Tests/Modules/Catalog/MMCA.Store.Catalog.API.Tests/CatalogModuleTests.cs:5, MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.API.Tests/SalesModuleTests.cs:8, MMCA.Store/Tests/Modules/Identity/MMCA.Store.Identity.API.Tests/IdentityModuleTests.cs:5). MMCA.Common holds the adversarial coverage for the base itself in ModuleConformanceTestsBaseTests, which runs it against a leaf and a dependent fake module (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:51, :60) and proves each assertion actually fails on the drift it claims to catch through a private DriftedTests subclass (:131).

ObservabilityConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs:30 · Level 0 · abstract partial class

  • What it is: an SLO alert-to-runbook pairing gate. It parses the SLO metric alerts a consumer's infra/main.bicep provisions and asserts each one keeps a matching, severity-correct triage section in that repo's infra/OPERATIONS.md, in both directions: a missing runbook section fails, and so does an orphan runbook section whose alert no longer exists.
  • Depends on: System.Globalization, source-generated System.Text.RegularExpressions regexes, Assembly.GetManifestResourceStream, AwesomeAssertions, and [Fact] (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs:1-2). No first-party dependency and, notably, no IArchitectureMap: it reads embedded text, not assemblies.
  • Concept introduced, the documentation-pairing gate. [Rubric §13, Observability & Operability] assesses whether an operator paged at 3am has usable guidance; the failure mode this base closes is the silent one, an alert added, renamed, or re-tiered while its runbook section stays behind. [Rubric §17, DevOps] applies because the source of truth is the IaC template itself, and [Rubric §34, Architecture Governance] because it makes the "every alert has a runbook" convention executable rather than aspirational.
  • Walkthrough
    • Overridable knobs: MinimumAlertSpecs (line 39, default 3), the two manifest-resource logical names BicepResource (line 42, infra.main.bicep) and RunbookResource (line 45, infra.OPERATIONS.md), and ResourceAssembly (line 51), which defaults to GetType().Assembly.
    • SloAlertSpecs_AreDiscovered_GateIsNotVacuous (lines 53-61) is the honesty guard: discovering fewer specs than the floor means the parse anchors drifted, not that alerts disappeared.
    • EveryProvisionedSloAlert_HasASeverityCorrectRunbookSection (lines 63-89) matches each alert key against the runbook headings by the -alert- infix (line 73, the constant at line 32) and then asserts the heading also carries the (sev N) tag matching the bicep severity (lines 80-84).
    • EveryRunbookAlertSection_MapsToAProvisionedAlert (lines 91-103) is the reverse direction, flagging stale guidance.
    • DiscoverAlertSpecs (lines 105-126) slices the bicep between var sloAlertSpecs and resource sloAlerts (lines 109-112, each index assertion carrying its own because), runs AlertKeyRegex and AlertSeverityRegex over that block, and asserts the two match counts agree (line 117) so a changed spec shape fails loudly instead of silently mis-pairing. DiscoverRunbookAlertHeadings (line 128) keeps only the ### headings carrying the infix. The three [GeneratedRegex] partial properties sit at lines 139-146, each with a 2000 ms match timeout.
    • ReadEmbedded (lines 131-137) throws a message naming the assembly when a resource is missing.
  • Why it's built this way: the class doc (lines 23-28) records why the ResourceAssembly default is load-bearing. The base ships inside the framework package, so resolving resources against its own assembly would look for the consumer's bicep inside MMCA.Common.Testing.Architecture.dll and always throw. Defaulting to the derived type's assembly means a subclass needs no wiring beyond two EmbeddedResource entries in its csproj (the snippet is in the doc at lines 18-22).
  • Where it's used: subclassed as a one-line body-less class in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/ObservabilityConventionTests.cs:7) and Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/ObservabilityConventionTests.cs:7); see ObservabilityConventionTests. MMCA.Common carries ObservabilityConventionTestsBaseTests instead, a deliberate cross-assembly guard that repoints both resource names at local fixtures (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/ObservabilityConventionTestsBaseTests.cs:14, resources at :16-18) and adds one extra [Fact] pinning the ResourceAssembly default to the derived type's assembly rather than the base's (:24-29).

ProtoScopeKind

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:286 · Level 0 · private enum

  • What it is: the four block kinds the .proto parser tracks while walking a file: Service, Message, Enum, Oneof (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:286-292).
  • Depends on: nothing; a private enum nested in ArchitectureRules.
  • Concept: the proto contract gate parses .proto files with a hand-rolled line scanner, not a protobuf compiler, so it needs an explicit notion of "which block am I inside" to decide how to render the current line. The kind is what DescribeMember branches on (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:194): a Service scope renders rpcs (lines 198-209), an Enum scope renders values (lines 211-217), and anything else renders message fields (lines 219-227). [Rubric §9, API & Contract Design] assesses whether a published contract surface is pinned deliberately; this enum is the vocabulary that pinning is expressed in.
  • Walkthrough: TryPushScope (line 166) maps the regex-captured keyword to a member (lines 174-180), with Oneof as the discard fallback arm. Oneof exists specifically to be treated as transparent: a oneof block contributes no name segment because on the wire its members belong to the enclosing message (comment plus push, lines 182-184).
  • Where it's used: carried by ProtoScope and consumed by DescribeMember (line 194); both are private to the ArchitectureRules.Protos.cs partial.

RouteAuthorizationTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs:22 · Level 0 · abstract class

  • What it is: an abstract test base that reflects over a UI assembly's routable Blazor pages and fails the build if a page the subclass marks as governed has lost its [Authorize(Roles = "...")] role gate.
  • Depends on: [Fact] (xUnit), AwesomeAssertions, RuleHelpers.LoadableTypes, and pure reflection over attribute instances matched by full name (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs:24-25).
  • Concept introduced, the security-regression fitness function. [Rubric §11, Security] and [Rubric §25, Navigation & IA] assess whether protected routes stay protected; this base turns "the admin page must require the Organizer role" from a review checklist into a compiled assertion, so a page cannot silently regress from a role-scoped [Authorize] to a bare [Authorize] reachable by any authenticated user. It is the page-level counterpart to AnonymousEndpointTestsBase, which guards the opt-out side of the same question.
  • Walkthrough
    • The subclass supplies TargetAssembly (line 28), the exact RequiredRole (line 31), an IsGovernedPage strategy (line 40), and a MinimumGovernedPages non-vacuity floor (line 47, default 1).
    • GovernedPages_RequireDeclaredRole (line 50) collects pages that are routable, governed, and do not require the role, then asserts the offender set is empty, naming each offender's route templates (lines 52-60).
    • GovernedPageSet_IsNotEmpty (line 64) guards the guard: if a refactor moved namespaces so IsGovernedPage matched nothing, the first test would pass vacuously, so this one asserts the discovered count meets the floor (lines 68-73).
    • Detection is all reflection by attribute full name: IsRoutablePage (line 77), RequiresRole (line 83, which reads the Roles property off the attribute instance and requires an exact ordinal match, so a bare [Authorize] or a different role fails), the subclass helper HasAuthorizeAttribute (line 94), Routes (line 97), and the base-type walk IsOrDerivesFrom (line 105) that lets a derived authorize attribute still count.
  • Why it's built this way: matching attributes by full name keeps the shared package free of an ASP.NET Core reference (lines 16-20) while still inspecting ASP.NET attributes; the package's only dependencies are NetArchTest, AwesomeAssertions, and xUnit (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/MMCA.Common.Testing.Architecture.csproj:18-20). A reflection scan also covers future pages matching the strategy without hand-enumeration. Deliberately anonymous public pages and bare-[Authorize] self-service pages simply must not match IsGovernedPage (lines 12-15).
  • Where it's used: subclassed per module UI test project, five times today: MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/ManagementRouteAuthorizationTests.cs:19, MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/IdentityRouteAuthorizationTests.cs:16, and Store's Catalog, Sales, and Identity equivalents (MMCA.Store/Tests/Modules/Catalog/MMCA.Store.Catalog.UI.Tests/CatalogRouteAuthorizationTests.cs:15, MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.UI.Tests/SalesRouteAuthorizationTests.cs:18, MMCA.Store/Tests/Modules/Identity/MMCA.Store.Identity.UI.Tests/IdentityRouteAuthorizationTests.cs:15).

RuleHelpers

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/RuleHelpers.cs:14 · Level 0 · internal static class

  • What it is: the internal reflection toolbox the reflection-based fitness functions share: extension members for enumerating loadable types, matching suffix conventions on generic types, detecting base types and interfaces by open generic or name prefix, and classifying property setters as mutable or init-only.
  • Depends on: System.Reflection (Assembly, Type, PropertyInfo, ReflectionTypeLoadException, BindingFlags) only.
  • Concept introduced: the doc (lines 5-9) states the premise: NetArchTest cannot inspect method return types, generic-argument constraints, property accessors, or attribute usage, so those rules reflect over loaded types directly through these helpers. [Rubric §14, Testability] and [Rubric §15, Best Practices & Code Quality] apply: the reflection subtleties (partial assembly loads, init-only detection) are solved once here rather than re-derived per rule.
  • Walkthrough: the class body is three C# preview extension(T) blocks, so every helper is an extension property or method, not a classic this-parameter extension method (the syntax is taught in primer §4).
    • extension(Assembly assembly) (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/RuleHelpers.cs:16): LoadableTypes (line 19) tolerates a partially-resolvable assembly by catching ReflectionTypeLoadException and returning the types that did load through OfType<Type>(), which both filters nulls and narrows the element type (lines 27-31). ConcreteClasses (line 36) narrows that to non-abstract classes.
    • extension(Type type) (line 40): SimpleName (line 47) strips the generic-arity backtick so suffix conventions match generic types too. InheritsGeneric (line 58) walks the base chain and ImplementsGeneric (line 72) scans the interface set for an open generic. HasBaseTypeStartingWith (line 80) detects a framework base by full-name prefix without a compile dependency (for example FluentValidation's AbstractValidator). DeclaredPublicProperties (line 94) narrows to declared-only public instance properties. InheritsAggregateRoot (line 101) and InheritsAuditableEntity (line 108) hard-code the MMCA entity base full names (AuditableAggregateRootEntity<TIdentifierType> plus the AuditableBaseEntity<TIdentifierType> and BaseEntity<TIdentifierType> ancestors) so the entity rules can classify types cross-repo.
    • extension(PropertyInfo property) (line 114): HasPublicMutableSetter (line 121) is the immutability primitive. It reports false when there is no public setter, and false for init-only setters by looking for the System.Runtime.CompilerServices.IsExternalInit required custom modifier on the setter's return parameter (lines 131-135).
  • Why it's built this way: every helper avoids a compile-time reference to the type it detects (base types matched by string prefix), which is what lets one rule body run identically across four repos that do not reference each other. The class carries a file-level [SuppressMessage] for CA1708 (lines 10-13): with multiple extension(T) blocks in one static class the analyzer flags the compiler-generated grouping members as case-colliding, a documented false positive.
  • Where it's used: throughout the ArchitectureRules partials, inside CrossEntityNavigationFinder, and directly by RouteAuthorizationTestsBase and AnonymousEndpointTestsBase.
  • Caveats / not-in-source: the type is internal, so consumer repos cannot call these helpers directly; they reach the same behavior only through the public rules and bases. StateManagementConventionTestsBase is the one base that re-implements the tolerant type load privately rather than using this class (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:99).

LayerRef

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:31 · Level 1 · sealed record

  • What it is: an immutable record describing one assembly in a repo's architecture: its owning Module, its Layer, the compiled Assembly, and its RootNamespace (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:31).
  • Depends on: Layer and System.Reflection.Assembly.
  • Concept introduced: the atomic unit of an architecture map. Module is the empty string for framework (MMCA.Common) layers that belong to no business module (lines 22-30), which is how the same record models both a module assembly and a shared framework assembly. Every projection and every isolation rule keys off that one convention.
  • Walkthrough: a four-parameter positional sealed record (line 31), so it gets structural equality and immutability for free; its members are set once at construction by the map's DefineLayers.
  • Where it's used: ArchitectureMapBase stores a lazy IReadOnlyList<LayerRef> and derives every projection from it; its Framework and Module factory helpers are what build these. The namespace-cycle rule takes a LayerRef directly, using its RootNamespace to decide which namespace node a type belongs to (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:84); the [ServiceContract] purity rule iterates map.Layers directly rather than one projection (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:40); and the IL-reading rules resolve each ref's on-disk Assembly.Location through ScannableAssemblyLocations (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:88).

ProtoScope

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:297 · Level 1 · private sealed record

  • What it is: one open block in the .proto parser's scope stack: a ProtoScopeKind plus the block's name, empty for a transparent oneof (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:294-297).
  • Depends on: ProtoScopeKind. Private to the ArchitectureRules.Protos.cs partial.
  • Concept: the two-field record is what makes nested messages and enums render under their qualified name without a real grammar. DescribeProtoFile keeps a Stack<ProtoScope> (line 112), pushes on a scope header and pops on a line starting with a closing brace (lines 124-132), and QualifiedName (line 234) reverses the stack and joins the non-empty names under the file's package.
  • Walkthrough: a two-parameter positional sealed record (line 297), constructed only in TryPushScope (line 184). Because the Oneof arm passes string.Empty as the name, the length filter in QualifiedName (line 238) drops it, which is exactly the wire semantics: a oneof member is a field of the enclosing message.
  • Where it's used: DescribeProtoFile (line 109), TryPushScope (line 166), DescribeMember (line 194), and QualifiedName (line 234), all feeding ProtoContractTestsBase through BuildProtoContract (line 86).

IArchitectureMap

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:39 · Level 2 · interface

  • What it is: the single per-repo abstraction every architecture fitness function keys off. Each repo supplies one implementation declaring its layer and module assemblies; the shared rule library and abstract test bases consume only this interface, so a rule is written once and runs identically across every repo that supplies a map (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:33-38, whose doc names MMCA.Common, MMCA.Store and MMCA.ADC; MMCA.Helpdesk supplies a fourth map).
  • Depends on: LayerRef, Layer, System.Reflection.Assembly.
  • Concept introduced, the architecture map as the fitness-function extension point. This is a classic Dependency Inversion: the rules depend on an abstraction (the map), and each repo provides the concrete inventory of its assemblies. [Rubric §1, SOLID] (DIP) and [Rubric §7, Microservices Readiness] apply: the map also models the per-module layers a would-be extracted service owns, so the isolation rules can check module boundaries the same way in any repo.
  • Walkthrough: the interface exposes identity (RepoToken line 42, ModuleNames line 45), the raw Layers inventory (line 48), and the projections the rules lean on: OfLayer (all assemblies of a kind, line 51), the per-module ModuleDomain/ModuleApplication/ModuleShared (lines 54-60), Infrastructure()/Api() across framework plus modules (lines 63-66), the lookups For(module, layer) (line 69) and ModuleOf(assembly) (line 72), namespace derivation RootNamespace(module, layer) (line 75), and OtherModuleNamespaces (line 81), which returns the same-layer namespaces of every other module (the forbidden targets for a module-isolation rule, empty for framework layers and single-module repos).
  • Why it's built this way: funneling every rule through one interface is what removed the drifting per-repo copies of the architecture-test suite (ADR-015); add a repo and you write one map, not a new rule set.
  • Where it's used: held as the protected abstract IArchitectureMap Map on nearly every *TestsBase in this group and passed to nearly every method of ArchitectureRules. ArchitectureMapBase is the reusable partial implementation, and CommonArchitectureMap / AdcArchitectureMap are two of the four concrete maps (Store and Helpdesk supply the others).

ArchitectureMapBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:11 · Level 3 · abstract class

  • What it is: the reusable base implementation of IArchitectureMap: a repo supplies only RepoToken and a DefineLayers() declaration, and every projection, namespace derivation, and module-isolation target computation is derived here (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:3-10).
  • Depends on: IArchitectureMap, LayerRef, Layer, System.Lazy, and System.IO (for FindRepoRoot).
  • Concept introduced, the template-method shape for a repo map: the base fixes the algorithm and the subclass fills two holes. It also centralizes every namespace and assembly string in one file, which the doc (lines 8-9) notes fixes Ubuntu CI case-sensitivity in one place.
  • Walkthrough
    • The constructor wraps DefineLayers() in a Lazy<IReadOnlyList<LayerRef>> (lines 15-16) so the assembly list materializes once, and Layers reads that value (line 25).
    • ModuleNames (line 28) filters out framework refs, then distinct-orders the module names ordinally.
    • OfLayer (line 35), ModuleDomain/ModuleApplication/ModuleShared (lines 39-45, via the private ModuleLayer, line 101), Infrastructure (line 48), and Api (line 51) are one-line LINQ projections over Layers.
    • For (line 54) and ModuleOf (line 59) are the lookups, both ordinal-comparison based.
    • RootNamespace (line 63) branches on module: framework layers become the MMCA.Common prefix plus the segment, module layers the repo token plus the module plus the segment (lines 64-66). OtherModuleNamespaces (line 69) maps every other module through it.
    • The static FindRepoRoot(solutionFileName) (line 79) walks up from AppContext.BaseDirectory to the directory containing the named .slnx, so doc and config consistency tests can read committed files regardless of the runner's working directory, throwing a clear InvalidOperationException when not found (lines 89-90).
    • The protected static Framework(...) (line 94) and protected Module(...) (line 98) factory helpers build LayerRefs with the right namespace, and the internal Segment (line 105) maps each Layer to its namespace token, throwing ArgumentOutOfRangeException on an unmapped member (line 116).
  • Why it's built this way: a per-repo map stays a flat declaration of assemblies (the two abstract members), and everything derivable is derived, so the maps cannot drift in how they compute namespaces.
  • Where it's used: each repo's concrete map subclasses this: CommonArchitectureMap (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommonArchitectureMap.cs:15), AdcArchitectureMap (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/AdcArchitectureMap.cs:14), StoreArchitectureMap (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/StoreArchitectureMap.cs:8), and HelpdeskArchitectureMap (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/HelpdeskArchitectureMap.cs:8), plus the private SpecTestMap fixture inside MMCA.Common's SpecificationFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:40). FindRepoRoot is called directly by every file-reading base and rule: DataResidencyTestsBase, FormsConventionTestsBase, FrameworkVersionConsistencyTestsBase, LocalizedTextConventionTestsBase, RawQueryableConventionTestsBase, StateManagementConventionTestsBase, UIArchitectureConventionTestsBase, the SortableColumnConventionTestsBase subclasses that build their own markup roots, and the proto rule ProtoContractsMatchFrozenList (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:45).

ConstructorDependencyCountTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:14 · Level 3 · abstract class

  • What it is: a single-responsibility-ceiling fitness function: it fails the build if any Application-layer *Service class has a constructor with more than the repo's accepted dependency count.
  • Depends on: IArchitectureMap (via Map.ModuleApplication()), [Fact], AwesomeAssertions, and reflection over constructors.
  • Concept introduced, quantifying the SRP smell. [Rubric §1, SOLID] assesses single-responsibility discipline; a ballooning constructor-dependency list is the canonical smell, and this base turns a previously implicit judgement call into an enforced ceiling so the next service cannot silently grow past it (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:3-13).
  • Walkthrough: the subclass supplies Map (line 16) and the MaxConstructorDependencies high-water mark (line 22; it is abstract, so every repo states its own number). ApplicationServices_DoNotExceedConstructorDependencyCeiling (line 25) scans Map.ModuleApplication() for concrete *Service classes (lines 27-31), asserts at least one was found (non-vacuity, lines 33-34), computes each service's maximum constructor parameter count with DefaultIfEmpty(0) so a parameterless service does not throw (lines 36-47), and asserts none exceed the ceiling, naming offenders with their counts (lines 49-53).
  • Why it's built this way: the ceiling is raised only with a conscious decision, and both adopters record the history of every move in the subclass doc. It overlaps the arity check in HandlerConventionTestsBase but scopes specifically to service facades and lets a repo pin an exact number rather than take the shared default. Repos without business modules (MMCA.Common itself) have nothing to scan and do not subclass this (lines 11-12).
  • Where it's used: subclassed in ADC with a ceiling of 9, held by its AuthenticationService facade (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/ConstructorDependencyCountTests.cs:20, ceiling at :24, the recorded history of the number at :9-18), and in Store with a ceiling of 8, one below ADC because Store is local-credential only and has no external-login email verifier (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/ConstructorDependencyCountTests.cs:20, ceiling at :24, rationale at :8-18). MMCA.Helpdesk deliberately does not adopt it and records why in a comment: its Application layer is handlers-only, and the base's anti-vacuity guard fails when it finds no *Service at all (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:177-179).

ArchitectureRules

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Api/ArchitectureRules.Controllers.cs:3 · Level 4 · static partial class

  • What it is: the reusable rule library: one large static partial class split across thirty ArchitectureRules.*.cs files, whose methods each assert one architectural invariant across every applicable assembly a map declares. A repo's test classes reduce to a sealed subclass of the matching *TestsBase supplying its own map.
  • Depends on: IArchitectureMap, Layer, ArchitectureAssert, RuleHelpers, CallGraphIndex, NetArchTest (Types.InAssembly(...)), Mono.Cecil plus Mono.Cecil.Cil for the IL-reading rules, System.Xml.Linq for the props-file and .resx rules, source-generated System.Text.RegularExpressions for the proto and localized-text parsers, and, for the specification rule, System.Linq.Expressions plus CrossEntityNavigationFinder.
  • Concept introduced, the rule as a parameterized function. Each method takes an IArchitectureMap (or, for the file-reading rules, a path and an allowlist) and does its own loop, so the *TestsBase classes are thin [Fact] shells that delegate. The partial is organized by concern across the files ArchitectureRules.{CancellationTokens, CascadeSoftDelete, CommandValidators, Contracts, Controllers, Cycles, DomainEventHandlerSaves, DomainThrows, Entities, ErrorCatalog, Events, FolderWidth, Governance, HandlerResults, Handlers, Idempotency, Immutability, Layers, Localization, LocalizedText, Markup, Modules, Naming, Protos, Purity, Slices, SoftDelete, Specifications, Transport, Upcasters}.cs, filed under seven concern folders (Rules/Api, Rules/Contracts, Rules/Cqrs, Rules/Domain, Rules/Governance, Rules/Layering, Rules/Ui), so the rule library follows the same feature-by-folder convention it enforces. [Rubric §3, Clean Architecture], [Rubric §4, DDD], [Rubric §7, Microservices Readiness], and [Rubric §34, Architecture Governance] all apply: this is where the codebase's structural decisions become executable assertions.
  • Walkthrough: six representative shapes, each the template for a family of rules.
    • NetArchTest shape, ControllersDoNotDependOnInfrastructure (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Api/ArchitectureRules.Controllers.cs:6): loops the map's per-module API layer refs, computes the forbidden Infrastructure namespace via map.RootNamespace(...), runs a Types.InAssembly(...) chain filtered to the Controller suffix, and reports through ArchitectureAssert.NoViolations(result, ...). ControllersDoNotDependOnEntityFrameworkCore (line 22) is the same loop against the literal Microsoft.EntityFrameworkCore namespace, keeping data access out of the API layer.
    • Layer-flow shape, ArchitectureRules.Layers.cs: one public method per forbidden edge, DomainDoesNotDependOnApplication (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Layers.cs:12) through UiDoesNotDependOnInfrastructure (line 60), all delegating to the private LayerNotDependOnLayer (line 139), which loops every assembly of the from layer and asserts no dependency on the to layer's namespace. Two non-vacuity rules sit alongside them, LayerMapDeclaresLayers (line 72) and the two ModulesDeclareLayers overloads (lines 89 and 113).
    • Reflection shape, ControllersAreSealed (ArchitectureRules.Controllers.cs:37): flat-maps map.Api() to each assembly's ConcreteClasses (the RuleHelpers extension property), filters non-sealed controllers via the private IsController (line 70, which matches on the Controller suffix or an MVC base type), and asserts the string offender list is empty. ControllersInheritApiControllerBase (line 54) is the same shape with a caller-supplied exempt set, accepting either ApiControllerBase or EntityControllerBase<TEntity, TEntityDTO, TIdentifierType> as the base.
    • Custom-rule shape, ServiceContractsDoNotDependOnServiceInternals (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:32): the one rule that hands NetArchTest a MeetCustomRule predicate (line 44) reading Cecil metadata directly, because the selector is an attribute, not a name or a namespace.
    • IL-reading shape, the six rules that must see inside a method body, because neither NetArchTest nor reflection can: HardDeletesOnlyInAllowedTypes (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:62), DomainThrowsOnlyArgumentGuards (ArchitectureRules.DomainThrows.cs:67), the two error-catalog rules (ArchitectureRules.ErrorCatalog.cs:62, :102), AggregatesCascadeSoftDeleteToChildren (ArchitectureRules.CascadeSoftDelete.cs:100), and the transitive DomainEventHandlersDoNotSave (ArchitectureRules.DomainEventHandlerSaves.cs:76). All six open each mapped assembly with Mono.Cecil.ModuleDefinition.ReadModule over ScannableAssemblyLocations (ArchitectureRules.SoftDelete.cs:88) and match callees, base types and thrown exception types by full NAME, which is how the package keeps its zero-reference stance one level deeper than the reflection rules do.
    • Graph shape, NamespacesHaveNoDependencyCycles (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:45): builds a namespace-to-namespace graph per layer assembly from signature-level references (BuildNamespaceGraph, line 84), finds every strongly connected component (FindNamespaceCycles, line 253), and reports the shortest cycle path plus any extra members of the component. This is the largest single rule file in the library.
  • Why it's built this way: ADR-015 records the intent: the rule bodies live once here, and each repo's architecture test project is a set of sealed subclasses supplying its map, so all four repos enforce identical rules. The compile-time MMCA.Common/Source/Build/MMCA.Common.LayerEnforcement.targets guards the same layer flow at build time as a second, faster gate.
  • Where it's used: every *TestsBase in this group calls into it; those [Fact] methods are its public surface. A handful of rules are also called directly from MMCA.Common's own fitness self-tests, for example BuildProtoContract/AssertProtoContract from ProtoContractFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ProtoContractFitnessTests.cs:14) and the cascade rule from CascadeSoftDeleteFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:17).
  • Caveats / not-in-source: the full method roster spans thirty partials; only the entry file and representative methods are cited here. The authoritative fitness-method and base-class counts are generated into MMCA.Common/FACTS.md:45-50 and CI-gated, so read them there rather than counting by hand.

DataResidencyTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DataResidencyTestsBase.cs:14 · Level 4 · abstract class

  • What it is: a compliance-drift fitness function: the data-residency statement in a repo's PRIVACY.md must match the region where personal data is actually provisioned, and known-stale region claims must not reappear.
  • Depends on: IArchitectureMap, ArchitectureMapBase.FindRepoRoot, System.IO (File.ReadAllText), AwesomeAssertions.
  • Concept introduced, a document-versus-infrastructure consistency gate. [Rubric §30, Compliance / Privacy / Data Governance] assesses whether privacy claims track reality; this base fails the build if either the deployed region or the privacy policy changes without the other, closing the gap where a policy once claimed a region the data never lived in (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DataResidencyTestsBase.cs:3-13).
  • Walkthrough: the subclass supplies Map (line 16), the optional ForbiddenResidencyClaims list (line 23), and implements ExtractDeployedRegion(repoRoot) (line 53) against its own source of truth (the doc cites ADC parsing the SQL region default out of deploy.yml and Store parsing infra/DISASTER-RECOVERY.md). The single [Fact] PrivacyPolicy_DataStorageRegion_MatchesDeployedRegion (line 26) locates the repo root via FindRepoRoot on the map's repo token (line 28), asserts the extracted region is non-blank (lines 31-32), reads PRIVACY.md, then asserts the normalized policy contains the region (line 37) and none of the forbidden claims (lines 40-44). Normalize (line 57) strips whitespace and upper-cases (ToUpperInvariant, per CA1308), so "West US 2" matches the "westus2" token.
  • Where it's used: subclassed in Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/DataResidencyTests.cs:12) and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/DataResidencyTests.cs:12). Module-less MMCA.Common has no deployed region, and Helpdesk records it as N/A for reduced-scope reasons (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:180).

FormsConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/FormsConventionTestsBase.cs:15 · Level 4 · abstract class

  • What it is: a UX-safety fitness function: every admin *Create.razor form under Source/Modules must keep its unsaved-changes guard, dirty tracking, and validated MudForm, so those protections cannot silently regress.
  • Depends on: IArchitectureMap, ArchitectureMapBase.FindRepoRoot, System.IO file enumeration, AwesomeAssertions.
  • Concept introduced, the markup-scanning fitness function (it reads .razor text, not assemblies). [Rubric §24, Forms / Validation / UX Safety] assesses whether navigate-away data loss and missing validation are prevented; the base checks for six literal markers (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/FormsConventionTestsBase.cs:27-35): the UnsavedChangesGuard component, an IsDirtyAccessor binding (through the live accessor, which pre-empts the one-render stale-IsDirty lag, a §19 concern), an _isDirty field, a MudForm element, a required-field marker, and a RequiredError message. The marker list is virtual, so a repo can narrow or extend it.
  • Walkthrough: the subclass supplies Map (line 17) and optionally a higher MinimumCreateForms count (line 24, default 1). AdminCreateForms_KeepUnsavedChangesGuardAndValidation (line 38) resolves the repo root, enumerates *Create.razor under Source/Modules excluding obj and bin (lines 43-48), asserts the discovered count meets the floor (lines 50-51), and records a violation naming each missing marker per form (lines 53-67).
  • Why it's built this way: self-service forms with no navigate-away step (for example a single-section Profile password or delete form) carry no guard by design and simply must not match the *Create.razor glob (lines 11-13).
  • Where it's used: subclassed in the repos with admin create forms, Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/FormsConventionTests.cs:17) and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/FormsConventionTests.cs:17).

FrameworkVersionConsistencyTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FrameworkVersionConsistencyTestsBase.cs:13 · Level 4 · abstract class

  • What it is: an evolvability and drift fitness function that makes ADR-016 executable: all MMCA.Common.* packages in a consumer's Directory.Packages.props must be pinned to one version, so a partial sweep is caught at CI time.
  • Depends on: IArchitectureMap, ArchitectureMapBase.FindRepoRoot, System.Xml.Linq (XDocument), AwesomeAssertions.
  • Concept introduced, enforcing the lockstep release policy. [Rubric §15, Best Practices & Code Quality] and [Rubric §32, Dependency & Supply-Chain] assess coordinated versioning; the framework releases in lockstep with no phased rollout (ADR-016), and this gate fails if any MMCA.Common.* entry diverges (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FrameworkVersionConsistencyTestsBase.cs:3-11).
  • Walkthrough: the subclass supplies Map (line 15) and optionally MinimumCommonPackageCount (line 22, default 13). AllMmcaCommonPackages_ArePinnedToOneVersion (line 25) loads Directory.Packages.props from the repo root (lines 27-30), selects every PackageVersion element whose Include starts with the MMCA.Common. prefix (lines 31-41), asserts the count meets the floor (lines 43-44), asserts none has an empty version (lines 46-48), and asserts the distinct-version count is exactly one, listing what it found (lines 50-56).
  • Why it's built this way: MMCA.Common itself does not subclass this, because it declares no MMCA.Common.* pins; only consumers do (lines 10-11). The default floor of 13 is deliberately loose, and the doc points at MMCA.Common/FACTS.md for the authoritative released-package count (lines 17-21; that count is 17 today, generated at MMCA.Common/FACTS.md:19).
  • Where it's used: subclassed in Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/FrameworkVersionConsistencyTests.cs:9), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/FrameworkVersionConsistencyTests.cs:9), and Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:47). None of the three overrides the floor today, so all three run against the shared default of 13.

RawQueryableConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/RawQueryableConventionTestsBase.cs:30 · Level 4 · abstract partial class

  • What it is: an opt-in extraction-readiness gate: Application-layer code must not use the repository's raw IQueryable surfaces (Table, TableNoTracking, TableNoTrackingSingleQuery, TableNoTrackingSplitQuery on IReadRepository<TEntity, TIdentifierType>), because a handler written against a raw queryable is EF-coupled and its query shape cannot cross a gRPC boundary.
  • Depends on: IArchitectureMap, ArchitectureMapBase.FindRepoRoot, ArchitectureAssert, System.IO enumeration, and a source-generated Regex (System.Text.RegularExpressions).
  • Concept introduced, the honest textual scan (and its stated limits). The doc is unusually candid about the tradeoff (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/RawQueryableConventionTestsBase.cs:13-23): NetArchTest and plain reflection cannot see member usage inside method bodies, and this base deliberately carries no IL or Roslyn dependency, so the rule reads .cs text instead. It cannot see through variable indirection (an interface alias re-exposing the queryable is missed), and because it skips only whole-line comments, a match inside a string literal or trailing comment is a rare false positive. [Rubric §7, Microservices Readiness] is the invariant being protected; [Rubric §8, Data Architecture] and [Rubric §15, Best Practices & Code Quality] apply to the handler style it pushes toward (focused repository methods, readers, queriers, specifications).
  • Walkthrough
    • AllowedFiles (line 38) is the adoption ratchet: a repo with existing violations subclasses, runs once, and moves the reported file names in, so new files stay clean while the list shrinks (lines 24-28).
    • ApplicationSourceDirectories() (line 45) defaults to locating each declared module's Application project directory under the repo's Source/ tree by project name (lines 47-57), and is virtual for a custom layout.
    • The [Fact] ApplicationLayer_DoesNotUseRawQueryableSurfaces (line 61) first asserts the directory list is non-empty, with a message telling the author to override the directory hook (lines 65-66), then enumerates every .cs file, skipping obj, bin, and allowlisted file names (lines 71-79), and reports through ArchitectureAssert.NoViolations (line 84).
    • ScanFile (line 88) yields a file-name, line-number and trimmed-line triple for every non-comment line matching RawQueryableAccessRegex (line 103), a [GeneratedRegex] partial property matching the Table and TableNoTracking member-access family with a 2000 ms match timeout.
  • Why it's built this way: making the offender message carry the file and line (lines 96-98) is what makes a textual gate actionable; combined with the allowlist ratchet it can be adopted in a repo that is not yet clean. Note the contrast with the IL-reading rules in ArchitectureRules, which do read method bodies: a member read on a property is what a text scan is cheapest at, while a call is what Cecil sees best.
  • Where it's used: subclassed opt-in in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:13), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/RawQueryableConventionTests.cs:9), and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:11). Because it is a ratchet, a repo's AllowedFiles override is the record of its remaining debt.

StateManagementConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:17 · Level 4 · abstract class

  • What it is: a Blazor Server state-management gate: user and session state must live in per-circuit scoped services, never in mutable static members (which leak one user's state to another) or in singleton-registered stateful services.
  • Depends on: IArchitectureMap, ArchitectureMapBase.FindRepoRoot, reflection over the UI assemblies, a Source/ file scan, and System.Runtime.CompilerServices.CompilerGeneratedAttribute.
  • Concept introduced, a reflection plus source-scan combined gate. [Rubric §19, State Management] assesses per-circuit state safety; Blazor Server shares one process across every circuit, so a static member is shared across every user (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:5-16).
  • Walkthrough: the subclass supplies Map (line 19, whose UI assemblies must be registered under Layer.Ui) and optionally AllowedStaticMembers (line 25).
    • UiAssemblies_CarryNoMutableStaticState (line 28) reflects over Map.OfLayer(Layer.Ui), first asserting the set is non-empty (lines 32-33), then skipping enums, interfaces, and compiler-generated types (line 40) before flagging any declared static field that is not readonly, not const, and not compiler-generated, plus any settable static property, minus the exempted members (lines 45-56).
    • UiProjects_RegisterStatefulServicesScoped (line 66) scans Source/ .cs files (skipping obj, bin, non-UI paths, and Testing paths, lines 74-80) for a line containing both AddSingleton and a StateService or StateContainer name, recording a file name and line number as an offender (lines 85-90).
    • The private GetLoadableTypes (line 99) repeats the tolerant load locally rather than using the internal RuleHelpers, and IsCompilerGenerated (line 111) treats any member name containing an angle bracket as generated, or one carrying CompilerGeneratedAttribute.
  • Where it's used: subclassed in the repos with Blazor UI assemblies: MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/StateManagementConventionTests.cs:11), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/StateManagementConventionTests.cs:10), and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/StateManagementConventionTests.cs:9).

UIArchitectureConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/UIArchitectureConventionTestsBase.cs:14 · Level 4 · abstract class

  • What it is: a UI-architecture convention gate holding the container and presentational split with two mechanical line-count caps: a *.razor.cs code-behind stays within MaxCodeBehindLines, and a .razor file's inline @code block stays within MaxInlineCodeLines.
  • Depends on: IArchitectureMap, ArchitectureMapBase.FindRepoRoot, System.IO file enumeration, AwesomeAssertions.
  • Concept introduced, enforcing a design convention by file metrics. [Rubric §18, UI Architecture] assesses the container/presentational discipline; a ballooning code-behind signals page logic that belongs in an injected UI service or an extracted sub-component, and putting a number on it moves the judgement from review to CI (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/UIArchitectureConventionTestsBase.cs:3-13).
  • Walkthrough: the subclass supplies Map (line 16); the caps MaxCodeBehindLines (line 22, default 400), MaxInlineCodeLines (line 29, default 120), MinimumCodeBehindFiles (line 35, a non-vacuity floor, default 1), and ExcludedPathFragments (line 41) are all overridable, and the doc says the caps should move only with a recorded decision (lines 11-12).
    • CodeBehinds_StayWithinTheLineCap (line 44) enumerates *.razor.cs, asserts the floor (lines 48-49), and flags files over the cap with their line counts (lines 51-59).
    • RazorFiles_KeepInlineCodeBlocksSmall (line 63) finds each .razor file's @code line and measures the tail block from there to end of file (line 77), flagging it when it exceeds the inline cap.
    • EnumerateSourceFiles (line 89) drives both, resolving the repo root and excluding obj, bin, and the excluded fragments.
  • Caveats / not-in-source: the inline-@code measurement assumes the block is the file's tail (stated in the doc at lines 25-28), so a .razor file with markup after its @code block would over-count.
  • Where it's used: subclassed in the repos with Blazor UI: MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:11), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/UIArchitectureConventionTests.cs:9), and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:10).

AggregateConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/AggregateConventionTestsBase.cs:10 · Level 5 · abstract class

  • What it is: the minimal DDD aggregate fitness base for repos with no business modules (MMCA.Common itself): it asserts the Domain layer exposes aggregate roots, each built through a static Create(...) factory returning Result<T> with no public constructor, and that every domain or value-object factory returns a Result.
  • Depends on: IArchitectureMap, ArchitectureRules, [Fact].
  • Concept introduced, the thin delegating test base shared by most Level-5 types in this group: a protected abstract IArchitectureMap Map plus one [Fact] per rule that forwards to an ArchitectureRules method, with no logic of its own. The factory-returning-Result idiom on AuditableAggregateRootEntity<TIdentifierType> is what these rules verify. [Rubric §4, DDD] assesses aggregate discipline.
  • Walkthrough: four [Fact]s, each a one-line delegate: Domain_ShouldExpose_AggregateRoots (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/AggregateConventionTestsBase.cs:15), AggregateRoots_ShouldHave_ResultReturningCreateFactory (line 18), AggregateRoots_ShouldHave_NoPublicConstructors (line 21, which targets the framework-specific DomainAggregateRootsHaveNoPublicConstructors rule at MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Entities.cs:90 rather than the module-scoped one at line 167), and DomainFactories_ShouldReturn_Result (line 24, delegating to ArchitectureRules.DomainFactoriesReturnResult, ArchitectureRules.Entities.cs:53).
  • Why it's built this way: module-bearing repos use the fuller EntityConventionTestsBase instead (doc, lines 7-8); this base exists so a module-less repo still guards its aggregates.
  • Where it's used: subclassed only in MMCA.Common's architecture test project (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/AggregateConventionTests.cs:9).

CancellationTokenConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/CancellationTokenConventionTestsBase.cs:16 · Level 5 · abstract class

  • What it is: a cancellation-uniformity gate: every public async method on a public Application- or Infrastructure-layer type must declare a trailing CancellationToken cancellationToken parameter.
  • Depends on: IArchitectureMap and ArchitectureRules.AsyncMethodsDeclareTrailingCancellationToken (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CancellationTokens.cs:35).
  • Concept introduced, uniformity as the invariant (not presence). The doc states the reasoning (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/CancellationTokenConventionTestsBase.cs:4-9): cancellation is only end-to-end if it is uniform. One method that swallows the token turns a cancelled request, an expired CQRS timeout budget, or a stopping host into work that keeps running against the database, and a token in a non-trailing position or under a different name defeats the mechanical forwarding the decorator pipeline and the repositories rely on. That is why the rule reports three distinct problems, not one: a missing token, a token that is not the last parameter, and a trailing token under the wrong name (ArchitectureRules.CancellationTokens.cs:115). [Rubric §12, Performance & Scalability] assesses whether abandoned work is actually abandoned; [Rubric §29, Resilience & Business Continuity] assesses graceful shutdown behavior; [Rubric §15, Best Practices & Code Quality] covers the async convention itself.
  • Walkthrough
    • The subclass supplies Map (line 19) and optionally CancellationTokenExemptMethods (line 26, empty by default), entries in "TypeName.MethodName" form. The doc is deliberately narrow about when to use them (lines 21-25): a shipped public API where adding the parameter would break consumers, each entry justified by a comment.
    • The single [Fact] AsyncMethods_ShouldDeclare_TrailingCancellationToken (line 29) forwards both to the rule.
    • The rule itself carries the interesting mechanics. Scope is methods returning Task, Task<T>, ValueTask, or ValueTask<T> (IsAwaitableReturn, ArchitectureRules.CancellationTokens.cs:105) declared on a publicly-visible, non-delegate, non-compiler-generated type (IsCandidateType, line 92). ExternallyFixedMethods (line 141) computes the signatures the repo does not own: overrides whose base definition is declared outside the map's assemblies, and implicit implementations of an interface declared outside the map (resolved through InterfaceTargets, line 178, with unmappable generic interfaces swallowed). Dispose/DisposeAsync and special-name members are excluded outright (line 97).
  • Why it's built this way: exempting only what is externally fixed keeps the gate honest. A framework contract such as IHostedService.StartAsync cannot grow a parameter, but a first-party service method can, so it is not excused just for implementing an interface.
  • Where it's used: subclassed today only in MMCA.Common, as CancellationTokenConventionTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CancellationTokenConventionTests.cs:10), which exempts exactly two methods: NotificationHub.JoinChannelAsync and NotificationHub.LeaveChannelAsync (:23-27). Its recorded reason is that a SignalR hub method signature is the client-visible RPC contract bound by name and argument list, and both methods already pass Context.ConnectionAborted into the group calls, so the work is cancellable through a path the rule cannot see (:14-22; the hub itself is NotificationHub).

CascadeSoftDeleteConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/CascadeSoftDeleteConventionTestsBase.cs:18 · Level 5 · abstract class

  • What it is: a delegating base for the cascade-soft-delete rule: an aggregate root that owns child entities must delete them in its own Delete() override, because a soft delete is an ordinary UPDATE and nothing cascades for free.
  • Depends on: IArchitectureMap and ArchitectureRules.AggregatesCascadeSoftDeleteToChildren (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.CascadeSoftDelete.cs:100), which reads IL through CallGraphIndex.
  • Concept introduced, the invariant a soft delete quietly breaks. Setting IsDeleted = true on a root hides the root behind the global query filter and leaves every child row ACTIVE: an order line of a deleted order, a session of a deleted event. Nothing reads those rows through the root any more, so the orphans stay invisible until a report, an export or a per-child query surfaces them, and an erasure request that walks the child table finds data whose owner was deleted months ago. The database cannot help, because there is no ON DELETE CASCADE to fire on an UPDATE (rule doc, ArchitectureRules.CascadeSoftDelete.cs:35-50). [Rubric §4, DDD] assesses whether an aggregate really owns its children's lifecycle; [Rubric §8, Data Architecture] and [Rubric §30, Compliance / Privacy / Data Governance] assess orphaned rows surviving into exports and erasure accounting (ADR-005).
  • Walkthrough
    • The subclass supplies Map (line 20) and optionally AllowedCascadeExemptTypes (line 27, type full names or namespace prefixes; empty by default, which requires every child-bearing aggregate to cascade). The doc frames the list as the point of the rule: it turns "delete cascades, mostly" into a reviewed inventory of every aggregate that leaves children behind on purpose (lines 10-16).
    • The single [Fact] AggregatesWithChildCollections_MustCascadeSoftDelete_InDelete (line 30) forwards both to the rule.
    • In the rule, a child collection is an instance field declared on the aggregate whose type is one of six generic collection types (ChildCollectionTypeNames, ArchitectureRules.CascadeSoftDelete.cs:25-33) over an element type deriving from the auditable-entity base. Auto-properties are covered by the same pass, because an auto-property is a compiler-generated instance field whose backing-field name is normalized back to the property name for the report (doc, lines 61-69).
    • What counts as cascading is equally precise (doc, lines 70-78): a direct call inside the Delete() override to DeleteChildren, or to a zero-argument Delete on something other than this, which is what a hand-rolled loop over the children compiles to. base.Delete() is deliberately NOT accepted, because C# emits it as a non-virtual call to a virtual method, a shape nothing else produces, so the rule can tell "delete myself" from "delete my children" without guessing.
  • Why it's built this way: the rule reads IL because neither NetArchTest nor reflection can see a call inside a method body, and it matches bases and callees by full name with the generic arity stripped, so the package keeps its zero-reference stance and still recognizes an aggregate whose base lives in an unregistered assembly (doc, lines 52-60). Four limits are stated outright (lines 84-93): only direct calls in the override are read, the rule proves a cascade EXISTS rather than that it covers every collection, only fields declared on the aggregate are inspected, and grandchildren are the child's own cascade to own.
  • Where it's used: subclassed in all three repos with aggregates plus the framework. ADC exempts exactly one type, Speaker, whose junction rows deliberately outlive the soft-deleted root because the Sessionize import reactivates them in place (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:12, exemption and reasoning at :17-26). Store exempts Category, which REFUSES the delete instead of cascading it (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/CascadeSoftDeleteConventionTests.cs:12, :17-25). MMCA.Common runs it as a ratchet over its own module-less Source/, which declares no child-bearing aggregate today (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:14); the rule's own behavior is proven against compiled fixtures in CascadeSoftDeleteFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:17).

CommandValidatorCoverageTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/CommandValidatorCoverageTestsBase.cs:22 · Level 5 · abstract class

  • What it is: a validation-coverage gate: every command that carries data and has a handler must be covered by a validator, either its own IValidator<TCommand> or the request-validator bridge.
  • Depends on: IArchitectureMap and two ArchitectureRules members, CommandsHaveValidators (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:48) and HandledCommandCount (:80), plus AwesomeAssertions for the count assertion.
  • Concept introduced, gating a pipeline stage that has nothing to run. The Validating decorator runs FluentValidation before the transaction opens, so a command with no validator carries whatever the caller sent straight into the handler: the stage exists, it simply has nothing to execute, and the gap is invisible until bad input reaches the domain (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/CommandValidatorCoverageTestsBase.cs:4-7). The decorator pipeline itself is taught in primer §2. [Rubric §6, CQRS & Event-Driven] assesses the completeness of the pipeline's stages; [Rubric §11, Security] and [Rubric §24, Forms / Validation / UX Safety] assess whether untrusted input is checked before it acts.
  • Walkthrough
    • The subclass supplies Map (line 24) and optionally AllowedUnvalidatedCommands (line 31, empty by default) and MinimumCommands (line 38, default 1).
    • Two [Fact]s: Commands_ShouldHave_ValidationCoverage (line 41) forwards to the rule, and CommandInventory_ShouldNotBe_Empty (line 45) asserts the handled-command count meets the floor, so a map that resolves to no module Application assemblies cannot let the gate pass without reading anything (lines 46-48).
    • In the rule, a command is the command argument of every ICommandHandler implemented in the map's per-module Application assemblies, restricted to closed types with at least one public settable property; an init setter counts, so the usual positional-record command is inspected, and a payload-free marker command is skipped (ArchitectureRules.CommandValidators.cs:96-102, :135).
    • Coverage means one of two things (:112): a concrete IValidator<TCommand>, or the bridge, where the command implements ICommandWithRequest<TRequest> AND a concrete IValidator<TRequest> exists. Both halves of the bridge are required on purpose, because the framework registers the bridge validator for every such command whether or not a request validator resolves, and one that resolves none adds no rules (doc, lines 8-16).
    • Where it looks mirrors the DI reality (:31-36): both commands and validators are read from the module Application assemblies, because the framework registers validators from the module assembly, so a validator living anywhere else would not resolve at run time and must not count as coverage here either.
  • Why it's built this way: interfaces are matched by open-generic full NAME through a shared ClosedGenericArguments projection (:122), which keeps the package free of a compile reference to FluentValidation and to the framework's own Application layer. Two limits are stated (:37-41): a validator declared for a base command does not cover its derived commands, matching FluentValidation's closed-type resolution, and abstract validators are ignored since nothing registers them.
  • Where it's used: subclassed in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/CommandValidatorCoverageTests.cs:18), Store, whose allowlist is grouped by why each entry is there (identifier-only commands and two false positives the module-scoped scan cannot see through, with the frozen-debt group now empty, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/CommandValidatorCoverageTests.cs:17, rationale at :8-15), and Helpdesk, which deliberately keeps AllowedUnvalidatedCommands empty and writes real validators instead, because an allowlist entry names a type that a generated app or a scaffolded slice would never inherit (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:163, reasoning at :150-162, floor of 6 against 7 discovered commands at :173).

ConcurrencyConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ConcurrencyConventionTestsBase.cs:9 · Level 5 · abstract class

  • What it is: a one-rule delegating base asserting that no *UpdateRequest implements IConcurrencyAware, because the optimistic-concurrency token travels in the If-Match request header and not in the body.
  • Depends on: IArchitectureMap, ArchitectureRules.
  • Concept: cross-references the delegating-base shape from AggregateConventionTestsBase. The invariant is a single-source-of-truth one: a token in the request body would give the same conditional-update check a second, competing source (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ConcurrencyConventionTestsBase.cs:3-8). [Rubric §8, Data Architecture] assesses optimistic-concurrency handling (ADR-035); [Rubric §9, API & Contract Design] assesses keeping a precondition in the one place HTTP defines for it.
  • Walkthrough: one [Fact] UpdateRequests_ShouldNotImplement_IConcurrencyAware (line 13) delegating to ArchitectureRules.UpdateRequestsAreNotConcurrencyAware(Map) (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Governance.cs:24). The rule scans the map's module Application assemblies for classes or value types whose RuleHelpers.SimpleName ends with UpdateRequest and reports any that implement the full-name-matched contract (lines 26-34). The base's doc notes that modules with no mutable aggregate are legitimately vacuous (line 7).
  • Where it's used: subclassed in all four repos: MMCA.Common, where the module-less run is a ratchet that fires the moment the framework itself grows a body-token update request (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:12, rationale at :5-11), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/ConcurrencyConventionTests.cs:3), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:3), and Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:56).

ContractImplementationTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ContractImplementationTestsBase.cs:20 · Level 5 · abstract class

  • What it is: an encapsulation gate for the [ServiceContract] boundary: the interface is the published surface of a service, so the concrete class serving it must not be public.
  • Depends on: IArchitectureMap and ArchitectureRules.ServiceContractImplementationsAreNotPublic (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:81).
  • Concept introduced, the other half of a contract boundary. ServiceContractPurityTestsBase keeps the producer's internals out of the contract; this base keeps the contract's implementation out of the consumer's reach (doc, lines 10-13). A public implementation lets a consumer reference, construct or subclass the type, and each of those references is a coupling the extraction has to sever later, because an interface can be answered over a wire and a class cannot (lines 4-8, ADR-007). [Rubric §7, Microservices Readiness] assesses whether an extraction stays reversible; [Rubric §1, SOLID] (DIP) and [Rubric §9, API & Contract Design] assess binding to abstractions rather than to concretes.
  • Walkthrough: the subclass supplies Map (line 22) and optionally AllowedPublicImplementations (line 30, type full names or namespace prefixes for a shipped default a consumer is meant to construct, or a test double in a testing package). The single [Fact] ServiceContractImplementations_ShouldNotBe_Public (line 33) forwards both to the rule. In the rule, visibility is judged with Type.IsVisible (ArchitectureRules.Contracts.cs:92), so a public class nested inside an internal one is correctly treated as unreachable and passes; only concrete classes are scanned, so an abstract base carrying shared behavior for several implementations is exempt by construction (doc, lines 63-68). ServiceContractInterfaceOf (line 116) returns the first marked interface a type implements, matched by the attribute's full name through CarriesServiceContractAttribute (line 143).
  • Why it's built this way: like its twin, the rule is attribute-driven rather than Layer.Contracts-driven and honest about the vacuous case (doc, lines 14-18): a repo that marks no interface passes without asserting anything, and the value is the ratchet, with no test to remember to write when the first contract appears.
  • Where it's used: subclassed in all four repos: MMCA.Common as a ratchet, since the framework marks no interface (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ContractImplementationTests.cs:11), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/ContractImplementationTests.cs:19), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/ContractImplementationTests.cs:10), and Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ContractImplementationTests.cs:12).

ControllerConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/ControllerConventionTestsBase.cs:7 · Level 5 · abstract class

  • What it is: the presentation-layer convention base: controllers are thin and sealed, never reach Infrastructure or EF Core directly, and inherit the framework ApiControllerBase for consistent Result-to-HTTP mapping.
  • Depends on: IArchitectureMap, ArchitectureRules.
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); it adds a protected virtual ControllersExemptFromApiControllerBase list (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/ControllerConventionTestsBase.cs:12) for controllers that legitimately bypass the base (for example a webhook endpoint that owns its own response semantics). [Rubric §9, API & Contract Design] assesses consistent controller shape.
  • Walkthrough: four [Fact]s: Controllers_ShouldNotDependOn_Infrastructure (line 15), Controllers_ShouldNotDependOn_EntityFrameworkCore (line 18), Controllers_ShouldBe_Sealed (line 21), and Controllers_ShouldInherit_ApiControllerBase (line 24, passing the exempt list). The underlying rules live in ArchitectureRules.Controllers.cs at lines 6, 22, 37 and 54.
  • Where it's used: subclassed in every repo with business modules: Store, ADC, and Helpdesk (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/ControllerConventionTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/ControllerConventionTests.cs:3, MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:84).

DependencyVersionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DependencyVersionTestsBase.cs:15 · Level 5 · abstract class

  • What it is: a dependency-pin fitness function guarding two commercial-license traps at build time: MassTransit must stay below v9 and SixLabors.ImageSharp below v4, both parsed out of Directory.Packages.props.
  • Depends on: ArchitectureRules.PinnedPackageMajorBelow (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Governance.cs:38), [Fact]. Note there is no Map on this base; it reads the props file directly through the rule, which walks up from the test assembly's base directory to find it (ArchitectureRules.Governance.cs:63).
  • Concept introduced, enforcing a policy pin as a test. [Rubric §32, Dependency & Supply-Chain] assesses whether risky upgrades are guarded. The doc explains both traps: MassTransit v9 fails the startup license check and crashes every broker-enabled host while CI never starts a broker, so a blanket bump otherwise stays green (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DependencyVersionTestsBase.cs:4-6); ImageSharp v4's MSBuild targets fail without a Six Labors license key, so a blanket bump breaks every build (lines 39-44).
  • Walkthrough: MassTransit_MustNotExceed_MajorVersion8 (line 25) loops MassTransitPackageIds (lines 17-22: MassTransit, MassTransit.RabbitMQ, MassTransit.Azure.ServiceBus.Core) and calls PinnedPackageMajorBelow with an exclusive ceiling of 9. ImageSharp_MustNotExceed_MajorVersion3 (line 48) does the same for ImageSharpPackageIds (line 45) with ceiling 4. Both id lists are virtual so a repo can override to an empty list when it does not pin the package. The rule asserts on the nullable major directly rather than dereferencing after a not-null assertion, a shape the comment records as avoiding a CI-only IDE0370 (ArchitectureRules.Governance.cs:40-45).
  • Why it's built this way: the doc is explicit (lines 8-13): the consumer repos (ADC, Store) do NOT pin MassTransit (it flows transitively via MMCA.Common.Infrastructure), so they must not subclass this base with the default list, or the "must remain pinned" assertion would fail on a pin they do not declare. The v8 pin is enforced only in MMCA.Common, where MassTransit is actually pinned.
  • Where it's used: subclassed only in MMCA.Common, as the body-less DependencyVersionTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/DependencyVersionTests.cs:9).

DomainEventHandlerSaveTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/DomainEventHandlerSaveTestsBase.cs:22 · Level 5 · abstract class

  • What it is: a domain-event-handler purity gate: a handler must not persist, and the check is TRANSITIVE, walking the call graph out of every handler method rather than only looking for a save typed into the handler itself.
  • Depends on: IArchitectureMap and ArchitectureRules.DomainEventHandlersDoNotSave (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:76), which is built on CallGraphIndex.
  • Concept introduced, the second write inside the first one. Dispatch happens after SaveChangesAsync (and after commit inside an ITransactional command), so a handler that saves opens a second write in the middle of the first: it re-enters the change tracker, can raise a fresh event cascade, and persists work the outer transaction may still roll back. Handlers mutate state and let the owning unit of work flush it; an independent write belongs on the outbox (doc, lines 3-9; ADR-003 is where the outbox itself is decided). [Rubric §6, CQRS & Event-Driven] assesses event-handling discipline; [Rubric §8, Data Architecture] assesses transaction integrity; [Rubric §7, Microservices Readiness] assesses whether a handler stays movable.
  • Walkthrough
    • The subclass supplies Map (line 24) and optionally AllowedSavingTypes (line 33) and MaxCallDepth (line 40, default 6, which the doc justifies as covering the realistic handler to service to helper to repository chain while keeping the scan fast).
    • The default allowlist is ["MMCA.Common"] (line 33), and the reason is precise: the framework's outbox event bus persists by design, and a handler publishing an integration event is not the defect this rule hunts. A handler calling a save DIRECTLY is still reported, because detection happens at the call site and needs no descent, so the default silences nothing real (lines 26-32, and the rule doc at ArchitectureRules.DomainEventHandlerSaves.cs:50-60).
    • The single [Fact] DomainEventHandlers_ShouldNotReach_SaveChanges (line 43) forwards all three to the rule.
    • The rule identifies handlers by the IDomainEventHandler interface full name through CallGraphIndex.Implements (ArchitectureRules.DomainEventHandlerSaves.cs:96), then breadth-first walks to a save with a visited set, reporting the shortest chain it finds (FindSavePath, line 125). A save is a call to SaveChanges or SaveChangesAsync (line 12) on a declaring type whose name ends with DbContext, DbContextFactory, UnitOfWork or Repository (line 20), which is what keeps an unrelated SaveChanges out of the rule.
    • The walk follows direct calls, delegate creations (so lambdas and local functions are covered), and interface or virtual calls expanded to every implementation inside the scanned assemblies, and it descends into the compiler-generated state machine of an async or iterator method, whose MoveNext holds the real body (doc, lines 44-49).
  • Why it's built this way: an allowlist entry does two things, and the doc says so explicitly (lines 15-20 of the base): it silences the type it names AND stops the walk from descending into it, which is the correct reading of "we accept what happens in there". Four limits are stated in the rule (:61-69): the depth bound, unresolvable interface dispatch when the implementation lives outside the map, name-and-arity overload matching that over-reports rather than misses, and total blindness to reflection, DI-resolved delegates and expression trees.
  • Where it's used: subclassed in ADC, which accepts one deliberate cascade, the gamification points ledger written by the awarder its point-scoring handlers call (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/DomainEventHandlerSaveTests.cs:11, entry and reasoning at :22-27), and in Store, whose two entries are both the compensating-saga pattern, a handler opening its own DI scope for an independent, idempotent follow-up write (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/DomainEventHandlerSaveTests.cs:19, rationale at :11-17).

DomainPurityTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/DomainPurityTestsBase.cs:8 · Level 5 · abstract class

  • What it is: a framework-independence base: Domain and Shared stay free of infrastructure frameworks, and Application stays host-agnostic (no EF Core, no ASP.NET Core).
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Purity.cs, whose shared ForbiddenDomainDependencies list is at line 9).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); it adds an ExtraForbiddenDomainDependencies hook (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/DomainPurityTestsBase.cs:12) so a repo bans its own frameworks (the doc cites Store banning "Stripe" and ADC banning "RabbitMQ"). [Rubric §3, Clean Architecture] and [Rubric §4, DDD] assess the framework-free core.
  • Walkthrough: four [Fact]s: Domain_ShouldBe_FrameworkFree (line 15) and Shared_ShouldBe_FrameworkFree (line 18), both passing the extra-forbidden list, then Application_ShouldNotDependOn_EntityFrameworkCore (line 21) and Application_ShouldNotDependOn_AspNetCore (line 24), whose rules sit at ArchitectureRules.Purity.cs:24, :39, :54 and :68.
  • Where it's used: subclassed in all four repos (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/DomainPurityTests.cs:9, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/DomainPurityTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/DomainPurityTests.cs:3, and Helpdesk at MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:10).

DomainThrowTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/DomainThrowTestsBase.cs:21 · Level 5 · abstract class

  • What it is: a Result-pattern purity gate (ADR-013): the domain returns Result, it does not throw. Any throw in the map's Domain assemblies whose exception is not an argument guard fails the build.
  • Depends on: IArchitectureMap and ArchitectureRules.DomainThrowsOnlyArgumentGuards (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:67), which reads IL with Mono.Cecil.
  • Concept introduced, why a thrown business failure is a defect and not a style choice. It skips Result.Combine invariant composition, arrives at the API as a 500 instead of the outcome the caller can act on, and pays an exception unwind on a path that is not exceptional (doc, lines 4-9; rule doc at ArchitectureRules.DomainThrows.cs:23-29). The three argument guards stay allowed because they report a caller BUG rather than a business outcome, and a caller cannot recover from passing null, so there is nothing for a Result to carry (:11-21). [Rubric §4, DDD] and [Rubric §15, Best Practices & Code Quality] assess the error-handling model; [Rubric §9, API & Contract Design] assesses the status code a caller actually sees.
  • Walkthrough
    • The subclass supplies Map (line 23) and optionally AllowedThrowingTypes (line 31, empty by default, which allows nothing beyond the argument guards).
    • The single [Fact] Domain_ShouldNotThrow_ExceptArgumentGuards (line 34) forwards both to the rule.
    • The rule opens each Domain assembly with ModuleDefinition.ReadModule over DomainAssemblyLocations (ArchitectureRules.DomainThrows.cs:107) and, for every throw instruction, reads the type of the exception constructed immediately before it (ConstructedExceptionType, line 143).
    • Three things it deliberately does not touch (doc, lines 39-48): a bare rethrow compiles to a distinct opcode and is ignored, so preserving a caught exception stays free; the ArgumentNullException.ThrowIfNull family emits a plain call with no throw in the caller, so the modern guard style always passes; and compiler-written bodies are skipped (the skeleton members of a C# extension block, and the explicit interface implementations on a compiler-generated type), because their exceptions are not anyone's code.
    • When the thrown value was not constructed in place (a prepared local, a field, a factory call), the type is not knowable from the instruction stream. Those sites are reported as UNVERIFIABLE in the failure message rather than passed or failed (doc, lines 49-54; WithUnverifiable composes the message at ArchitectureRules.ErrorCatalog.cs:151).
  • Why it's built this way: the allowlist is framed as an inventory rather than a mute button (doc, lines 14-19): adopting the rule in a repo with existing throws means converting each site to a Result.Failure or recording why throwing is right there, which turns "the domain returns Result, mostly" into a reviewed list of every place that does not.
  • Where it's used: subclassed in ADC, whose allowlist is an explicit triage placeholder filled from the first run (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/DomainThrowTests.cs:9, list at :14-17), and in Store, which overrides nothing at all because the scan over its three module Domain assemblies found no non-guard throw at adoption (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/DomainThrowTests.cs:14, that decision recorded at :8-12).

EntityConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/EntityConventionTestsBase.cs:10 · Level 5 · abstract class

  • What it is: the fuller DDD entity and aggregate convention base (the module-bearing counterpart to AggregateConventionTestsBase): entities are sealed and live only in Domain, aggregate roots use a Create(...) factory returning Result<T> with no public constructor, every domain and value-object factory returns a Result, entity properties carry no public setter, and DTOs and requests stay out of Domain and Infrastructure.
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Entities.cs).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase). The property-setter rule is the one worth calling out on its own: mutation goes through named domain methods rather than a setter, which is what keeps an invariant enforceable in one place (doc, lines 6-8). [Rubric §4, DDD] and [Rubric §3, Clean Architecture] apply.
  • Walkthrough: eight [Fact]s: Domain_ShouldExpose_AggregateRoots (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/EntityConventionTestsBase.cs:15), AggregateRoots_ShouldHave_ResultReturningCreateFactory (line 18), AggregateRoots_ShouldHave_NoPublicConstructors (line 21, the module-scoped rule at ArchitectureRules.Entities.cs:167), DomainFactories_ShouldReturn_Result (line 24), DomainEntities_ShouldBe_Sealed (line 27), DomainEntities_ShouldReside_InDomainLayer (line 30), DomainEntityProperties_ShouldNotHave_PublicSetters (line 33, delegating to ArchitectureRules.EntityPropertySettersAreNonPublic, ArchitectureRules.Entities.cs:149), and DtosAndRequests_ShouldNotResideIn_DomainOrInfrastructure (line 36).
  • Where it's used: subclassed in Store, ADC, and Helpdesk (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/EntityConventionTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/EntityConventionTests.cs:3, MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:89).

ErrorCatalogTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ErrorCatalogTestsBase.cs:20 · Level 5 · abstract class

  • What it is: a two-rule gate over the set of Error codes a repo's modules construct: one code means one thing (no literal code owned by two different types), and a code carries the prefix of the vocabulary that owns it.
  • Depends on: IArchitectureMap and three ArchitectureRules members, ErrorCodesAreUnique (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.ErrorCatalog.cs:62), ErrorCodesUseAnAllowedPrefix (:102) and DistinctErrorCodeCount (:135), plus AwesomeAssertions.
  • Concept introduced, the error code as a public vocabulary. A client switches on a code and a support ticket quotes it, so two modules that both ship the same code make that vocabulary ambiguous, and the ambiguity only surfaces in production (rule doc, ArchitectureRules.ErrorCatalog.cs:31-37). [Rubric §9, API & Contract Design] assesses the stability and legibility of a published error surface; [Rubric §15, Best Practices & Code Quality] assesses whether a code is traceable back to the code that raised it. Uniqueness is measured per declaring TYPE, which is the subtle and correct choice: reusing one code across two branches of the same class is one error with two exits and passes, while the same code owned by two different types is the collision the rule exists to catch (doc, lines 45-50).
  • Walkthrough
    • Four overridable members: AllowedCodePrefixes (line 29, defaulting to Map.ModuleNames, which is the convention), AllowedSharedCodes (line 35, defaulting to the three generic statics on the framework's Error class that exist precisely to be reused), MinimumErrorCodes (line 43, default 1), and IsCodePrefixAllowed (line 62), a hook for a convention a prefix list cannot express.
    • Three [Fact]s: ErrorCodes_ShouldBe_Unique (line 46), ErrorCodes_ShouldCarry_TheOwningModulePrefix (line 50, passing the delegate rather than the list, so the hook is what the rule actually calls), and ErrorCodeCatalog_ShouldNotBe_Empty (line 54), the non-vacuity guard that fails when the map registers no module Domain or Application assemblies.
    • Codes are read out of IL at the Error factory call sites across the per-module Domain and Application assemblies. The recognised members are every static factory on Error plus the primary constructor (ErrorFactoryNames, ArchitectureRules.ErrorCatalog.cs:17-29); the record's copy constructor is excluded by a first-parameter-is-string check, so a with expression is not mistaken for a new code (comment, lines 11-15).
    • A code that is not a literal (built by concatenation, or read from a field) cannot be judged statically, so it is listed as UNVERIFIABLE in the failure message rather than passed or failed (doc, lines 51-55), the same honesty the domain-throw rule shows.
  • Why it's built this way: framework layers are deliberately out of scope (doc, lines 39-44), because the framework's codes are not the consumer's catalog. Scoping uniqueness to the module Domain and Application assemblies is what keeps a shared framework code from being reported as a collision in every repo.
  • Where it's used: subclassed in ADC and Store, both of which namespace their codes by AGGREGATE rather than by module name, so both extend Map.ModuleNames with that vocabulary and treat the list as the reviewed inventory: a code under a new prefix fails until the prefix is added (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/ErrorCatalogTests.cs:20, prefixes from :28; MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/ErrorCatalogTests.cs:19, prefixes from :27). Both subclass docs record the same constraint: error codes are a public contract that reaches clients in problem-details payloads, so nothing already shipped is renamed to satisfy the rule, and the legitimately shared codes are frozen in AllowedSharedCodes instead.

EventConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/EventConventionTestsBase.cs:9 · Level 5 · abstract class

  • What it is: an integration-event convention base (ADR-010): every concrete integration event inherits BaseIntegrationEvent, declares an int SchemaVersion, and lives in a *.IntegrationEvents namespace in the Shared layer. It also polices the upcasters that carry a retired contract forward.
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Events.cs and ArchitectureRules.Upcasters.cs).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase). [Rubric §6, CQRS & Event-Driven] and [Rubric §9, API & Contract Design] assess versioned, discoverable cross-service event contracts. It pairs with IntegrationEventContractTestsBase, which freezes the exact shape.
  • Walkthrough: five [Fact]s. The three schema rules come first: IntegrationEvents_ShouldDeclare_SchemaVersion (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/EventConventionTestsBase.cs:14), IntegrationEvents_ShouldInherit_BaseIntegrationEvent (line 17), IntegrationEvents_ShouldResideIn_SharedIntegrationEventsNamespace (line 20), backed by ArchitectureRules.Events.cs:6, :17 and :28. Two upcaster rules follow (ADR-090, doc lines 6-7): EventUpcasters_ShouldHave_UniqueSourceTypes (line 23, delegating to ArchitectureRules.Upcasters.cs:12, because with two IEventUpcaster implementations reading one source contract the message a handler receives would depend on DI registration order) and EventUpcasters_ShouldIncrease_SchemaVersion (line 26, delegating to ArchitectureRules.Upcasters.cs:28, which skips a source or target whose SchemaVersion is missing or non-int, that being the first rule's business). A repo with no upcasters passes both vacuously (doc, line 7).
  • Where it's used: subclassed in every repo that publishes integration events: Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/EventConventionTests.cs:3), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/EventConventionTests.cs:3), Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:38), and MMCA.Common itself under the name EventVersioningConventionTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventVersioningConventionTests.cs:12).

FolderWidthTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FolderWidthTestsBase.cs:14 · Level 5 · abstract class

  • What it is: the folder-width fitness base (ADR-109): no folder under a repo's Source/ or Tests/ tree holds more than 12 direct code files, so a folder keeps naming a feature instead of drifting into a technical bucket (doc, FolderWidthTestsBase.cs:3-13).
  • Depends on: ArchitectureRules (FoldersStayNarrow, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.FolderWidth.cs:34) and, in every subclass, ArchitectureMapBase.FindRepoRoot. It does not take an IArchitectureMap: the defect it hunts is a layout one, so the rule walks the filesystem rather than IL (rule doc, ArchitectureRules.FolderWidth.cs:9-11).
  • Concept introduced, the layout rule as a test. [Rubric §5, Modularity & Vertical Slicing] assesses whether a codebase is organised by feature rather than by technical kind. A flat Services/ holding forty files is not a compiler error and no reviewer catches it in the diff that adds the forty-first file, so the convention is only enforceable if something asserts it continuously. This base is that assertion, and it is the same delegating shape as the other architecture bases (AggregateConventionTestsBase): overridable knobs, one [Fact], the judgement itself living in ArchitectureRules.
  • Walkthrough
    • Three members to override or accept: RepoRoot (line 20, abstract, the only required one), MaxDirectFiles (line 23, virtual, default 12), and ExemptFolderSuffixes (line 30, virtual, default empty).
    • One [Fact], Folders_stay_narrow (line 33), which passes all three straight to ArchitectureRules.FoldersStayNarrow.
    • The rule walks Source/ and Tests/ (skipping a tree that does not exist), collects every offending folder as relative: count direct code files (max N), sorts the list ordinally and hands it to ArchitectureAssert.NoViolations, so a failure reports every offender at once rather than the first (ArchitectureRules.FolderWidth.cs:42-70).
    • Counting is deliberate about what an author actually chose to put in a folder: a .razor file counts once and its co-located X.razor.cs code-behind counts with it, .resx satellites never count, and generated files (*.g.cs, *.generated.cs, *.Designer.cs) never count (ArchitectureRules.FolderWidth.cs:88-126). Whole trees are skipped by path segment: bin, obj, Migrations, Platforms, Resources, node_modules, wwwroot, .git (:78).
  • Why it's built this way: exemptions are matched as repo-relative path SUFFIXES with forward slashes (:85), not as leaf names, so a repo can exempt one specific folder without accidentally exempting every folder that shares its name. The alternative, a leaf-name allowlist, would silently widen with every new project that reuses the name.
  • Where it's used: two thin subclasses today. ADC supplies only its root and takes every default (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/FolderWidthTests.cs:10-12). MMCA.Common supplies a root plus three documented exemptions, and its doc records why each is deliberate: the decorator pipeline folder (nine cross-cutting concerns times command and query, where splitting yields nine two-file folders), its test twin, and the entity marker interfaces (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FolderWidthTests.cs:18-32).

HandlerConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerConventionTestsBase.cs:8 · Level 5 · abstract class

  • What it is: the CQRS handler convention base: handlers and validators live only in Application, handlers and services do not broker other handlers, and no *Service exceeds the god-class constructor-arity ceiling.
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Handlers.cs).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); it adds a MaxServiceConstructorParameters override (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerConventionTestsBase.cs:12, default 8, matching the rule's own default at ArchitectureRules.Handlers.cs:44). [Rubric §6, CQRS & Event-Driven] and [Rubric §1, SOLID] apply; the CQRS decorator pipeline itself is taught in primer §2.
  • Walkthrough: six [Fact]s: Handlers_ShouldResideIn_ApplicationLayer (line 15), Handlers_ShouldNotInject_OtherHandlers (line 18), ApplicationServices_ShouldNotInject_Handlers (line 21), ApplicationServices_ShouldNotExceed_ConstructorArity (line 24, passing the max), Validators_ShouldResideIn_ApplicationLayer (line 27), EventHandlers_ShouldResideIn_ApplicationLayer_AndBeSealed (line 30). The rules sit at ArchitectureRules.Handlers.cs:6, :18, :31, :44, :70 and :82.
  • Where it's used: subclassed in Store, ADC, and Helpdesk (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/HandlerConventionTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/HandlerConventionTests.cs:14, MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:79). ConstructorDependencyCountTestsBase is the narrower, per-repo-pinned version of the arity check.

HandlerResultConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerResultConventionTestsBase.cs:16 · Level 5 · abstract class

  • What it is: an opt-in base asserting that every concrete command or query handler's TResult is Result or Result<T> (or a type derived from them), turning a runtime-only constraint into a build-time gate.
  • Depends on: IArchitectureMap and ArchitectureRules (ApplicationLayersDeclareHandlers, CommandHandlersReturnResult, QueryHandlersReturnResult, at MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.HandlerResults.cs:18, :39, :48).
  • Concept introduced, closing a deliberately unconstrained generic. The CQRS interfaces carry no compile-time constraint on TResult (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerResultConventionTestsBase.cs:6-7), but the decorator pipeline's short-circuit paths (feature gate, validation) fabricate failures through ResultFailureFactory, which throws InvalidOperationException at runtime for any non-Result TResult (lines 7-9). A handler with the wrong result type therefore compiles cleanly and only fails when a gate short-circuits it. This base moves that failure to CI. [Rubric §6, CQRS & Event-Driven], [Rubric §14, Testability], and [Rubric §15, Best Practices & Code Quality] apply.
  • Walkthrough: three [Fact]s. ApplicationLayers_DeclareAtLeastOneHandler (line 21) is the non-vacuity guard the doc calls out (lines 12-13): a mis-pinned assembly cannot make the other two pass by finding nothing. CommandHandlers_Return_ResultTypes (line 24) and QueryHandlers_Return_ResultTypes (line 27) delegate to the matching rules, which share the private HandlersReturnResult implementation (ArchitectureRules.HandlerResults.cs:51) and judge the result type by the two full-name constants at :9-10.
  • Why it's built this way: it is opt-in and map-driven like the rest of the family, so a repo adds it next to its other architecture test classes with the same Map and no other wiring.
  • Where it's used: subclassed in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:12), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/HandlerResultConventionTests.cs:6), and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:8).

IdempotencyConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/IdempotencyConventionTestsBase.cs:10 · Level 5 · abstract class

  • What it is: a one-rule delegating base asserting that every POST action in a repo's API layer states, in code, whether a retried request replays the original response (IdempotentAttribute) or deliberately does not (NonIdempotentAttribute with a written reason).
  • Depends on: IArchitectureMap and ArchitectureRules.PostActionsDeclareIdempotencyIntent (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:44).
  • Concept introduced, gating an omission rather than a mistake. The rule's remarks make the argument (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:21-29): POST is the one verb HTTP does not define as idempotent, so a client retrying a timed-out POST cannot know whether the first attempt landed. The framework's Idempotency-Key filter (ADR-017) costs an existing client nothing, because it no-ops for a request with no key header. The only failure mode worth gating is therefore the silent omission: an action that should replay but does not. The gate leaves the opt-out available as long as it is written down. [Rubric §9, API & Contract Design] assesses contract-level retry semantics; [Rubric §29, Resilience & Business Continuity] assesses safe retry behavior.
  • Walkthrough: the subclass supplies Map (line 12); the single [Fact] PostActions_ShouldDeclare_IdempotencyIntent (line 15) forwards to the rule. In the rule, UndeclaredPostActions (line 67) enumerates public instance methods on concrete API-layer controller types, keeps those carrying HttpPostAttribute, and reports the ones carrying neither IdempotentAttribute nor NonIdempotentAttribute (lines 71-74). Attributes are matched by simple type name (HasAttributeNamed, line 80) with inherit: true, so a concrete controller inheriting a framework base action satisfies the rule through the base, and abstract controller types are skipped as declaration sites (remarks, lines 31-36).
  • Caveats / not-in-source: only [HttpPost] is recognised. An action routed through [AcceptVerbs("POST")] or a conventional route is out of scope, which the remarks justify on the grounds that neither appears in this framework or its consumers (lines 38-42).
  • Where it's used: subclassed in all four repos, as IdempotencyConventionTests in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:10), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:3), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/IdempotencyConventionTests.cs:3), and Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:65, whose comment records that TicketsController's POST actions are all opted in, so the gate is non-vacuous, :61-64).

ImmutabilityTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ImmutabilityTestsBase.cs:8 · Level 5 · abstract class

  • What it is: an immutability convention base: DTOs, command and query messages, domain events, integration events, and value objects expose no public mutable (non-init) setter; value objects are additionally sealed and confined to the Shared layer.
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Immutability.cs), which uses RuleHelpers.HasPublicMutableSetter underneath through the shared MutablePropertyViolations projection (ArchitectureRules.Immutability.cs:76).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); the init-only versus mutable distinction is exactly what HasPublicMutableSetter detects via the IsExternalInit modifier. [Rubric §15, Best Practices & Code Quality] and [Rubric §4, DDD] assess immutable contracts and value objects.
  • Walkthrough: five [Fact]s: Dtos_ShouldBe_Immutable (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ImmutabilityTestsBase.cs:13), CommandsAndQueries_ShouldBe_Immutable (line 16), DomainEvents_ShouldBe_Immutable (line 19), IntegrationEvents_ShouldBe_Immutable (line 22), ValueObjects_ShouldBe_ImmutableSealedAndInShared (line 25), backed by ArchitectureRules.Immutability.cs:6, :18, :34, :45 and :56.
  • Where it's used: subclassed in Store, ADC, and Helpdesk (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/ImmutabilityTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/ImmutabilityTests.cs:3, MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:94).

IntegrationEventContractTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventContractTestsBase.cs:18 · Level 5 · abstract class

  • What it is: a frozen wire-contract guard: it rebuilds the live integration-event contract (one line per event, the full name followed by its members in braces) and compares it to a committed snapshot the subclass supplies, so a renamed, removed, or retyped property, or a new event shipped without its consumer, fails the build.
  • Depends on: IArchitectureMap, ArchitectureRules.BuildIntegrationEventContract (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Events.cs:45), and ArchitectureAssert.
  • Concept introduced, the snapshot fitness function, and why it compares as a SET. [Rubric §9, API & Contract Design] and [Rubric §7, Microservices Readiness] assess whether cross-service contracts stay stable; because a consumer in another service deserializes by shape, this gate makes any contract change a deliberate, coordinated commit. The member list inside each event's braces is compared as a set rather than a sequence (doc, lines 10-16), and the reasoning is worth reading: JSON carries no member order, so reordering two properties in a record's declaration changes nothing a consumer can observe, and failing the build for it would train the one gate that guards real breakage to be updated by rote. Everything observable stays a failure: a missing member, an extra member, a changed type, and any change to the set of events itself.
  • Walkthrough
    • The subclass supplies Map (line 20) and the committed ExpectedContract snapshot (line 23).
    • IntegrationEventContracts_ShouldMatch_TheFrozenSnapshot (line 26) builds the actual contract (line 28) and funnels the differences through ArchitectureAssert with a message instructing the author to version the event, coordinate the consumer rollout, and update ExpectedContract in the same commit (lines 30-35).
    • Compare (line 46) parses both sides and reports three classes of difference: an event the committed contract declares and the code no longer does (line 53), a NEW event shipped without a consumer rollout or a snapshot update (line 56), and per-event member differences (line 59).
    • CompareMembers (line 73) reports a same-named member with a different type as a retype rather than as an unrelated add and remove, because that is the failure a consumer actually sees: the property still deserializes, into the wrong shape (doc, lines 64-68). Missing and extra members are reported separately (lines 82, 94).
    • Parse (line 106) turns contract lines into an event-to-members map, and keeps a line that does not carry the braced shape whole as an event with no members, so a malformed committed literal surfaces as a mismatch rather than being silently dropped (doc, lines 99-103). Braces are stripped from that key deliberately, because the reported difference is formatted through the shared assertion helper and a stray brace in a reason string is a format hazard (comment, lines 116-117).
  • Where it's used: subclassed in the repos publishing integration events: Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/IntegrationEventContractTests.cs:3), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventContractTests.cs:3), and Helpdesk, whose one-line snapshot is a compact worked example (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:99, snapshot at :105-108). It complements EventConventionTestsBase and has a synchronous counterpart in ProtoContractTestsBase.

LayerDependencyTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/LayerDependencyTestsBase.cs:7 · Level 5 · abstract class

  • What it is: the Clean Architecture layer-flow base: fifteen [Fact]s asserting that the map declares the expected layers at all, and that each layer references only layers below it (Domain not on Application, Infrastructure, or API; Application not on Infrastructure or API; Infrastructure not on API; Shared on nothing above it; UI only on Shared).
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Layers.cs).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); this is the runtime half of the two-gate layer enforcement described in ADR-015, the compile-time half being MMCA.Common/Source/Build/MMCA.Common.LayerEnforcement.targets. [Rubric §3, Clean Architecture] is the whole point.
  • Walkthrough
    • Three overridable declarations come first: RequiredLayers (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/LayerDependencyTestsBase.cs:16, defaulting to the five core layers Shared, Domain, Application, Infrastructure, Api), RequiredModuleLayers (line 27, defaulting to the same list), and ModuleRequiredLayerOverrides (line 49, empty by default).
    • The per-module override is the design decision worth understanding. Both the base doc (lines 29-38) and the rule doc (ArchitectureRules.Layers.cs:96-104) argue the same point: trimming the DEFAULT list to accommodate one deliberately thin module (a hub module that is Shared plus Application plus Api and nothing else) silently stops enforcing those layers for every OTHER module in the repo, so one thin module would buy blanket permission to forget an assembly anywhere. Naming the exception keeps the default strict and puts the weakening exactly where it is true. The base doc even carries the worked override snippet (lines 40-48).
    • Two non-vacuity [Fact]s guard the rest: LayerMap_DeclaresEveryExpectedLayer (line 52) and LayerMap_ModulesDeclareEveryExpectedLayer (line 55, passing the overrides through to the three-argument ModulesDeclareLayers at ArchitectureRules.Layers.cs:113). Without them a map that forgot an assembly would satisfy every dependency rule by having nothing to check (ArchitectureRules.Layers.cs:64-68).
    • Thirteen forbidden-edge [Fact]s follow, each a one-line delegate onto an ArchitectureRules.Layers.cs method: Domain_ShouldNotDependOn_Application (line 59) through Ui_ShouldNotDependOn_Infrastructure (line 95). The UI trio (lines 89-95) encodes the documented exception that UI depends only on Shared for Blazor WASM compatibility.
  • Where it's used: subclassed in all four repos (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/LayerDependencyTests.cs:9, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/LayerDependencyTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/LayerDependencyTests.cs:3, and Helpdesk at MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:5). NamespaceCycleTestsBase is the finer-grained companion that looks inside a layer assembly, and ModuleIsolationTestsBase the one that looks across modules.

LocalizationResourceTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizationResourceTestsBase.cs:10 · Level 5 · abstract class

  • What it is: an opt-in translation-coverage gate (ADR-027): a repo that ships localized .resx resources subclasses this and lists its required cultures; the build fails if any base .resx under Source/ lacks a complete, non-empty sibling for a required culture.
  • Depends on: ArchitectureRules.ResourceTranslationsAreComplete (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.Localization.cs:23), [Fact]. There is no Map on this base; it scans Source/ directly through the rule.
  • Concept introduced, a coverage fitness function for i18n. [Rubric §27, i18n] assesses translation completeness; this gate ensures a new English string can never ship without its translation (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizationResourceTestsBase.cs:3-9).
  • Walkthrough: the subclass supplies RequiredCultures (line 13, for example a single Spanish entry) and optionally MinimumBaseResources (line 21, a non-vacuity floor whose default of 0 skips the guard). The single [Fact] Translations_AreComplete_ForEveryRequiredCulture (line 24) passes both to the rule, which reads each .resx string entry with XDocument (ArchitectureRules.Localization.cs:81) and skips build output (:75).
  • Why it's built this way: single-locale repos need not subclass it (the rule is vacuous for an empty list). It pairs with LocalizedTextConventionTestsBase: this gate keeps the extracted resources translated, that gate keeps literals out of markup.
  • Where it's used: subclassed in MMCA.Common as LocalizationResourceTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/LocalizationResourceTests.cs:12) and in Store, ADC, and Helpdesk under the name TranslationCompletenessTests (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/TranslationCompletenessTests.cs:13, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/TranslationCompletenessTests.cs:12); Helpdesk's requires Spanish with a floor of 3, covering the two page resx pairs and the Tickets error resources (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:130, cultures at :132, floor at :135).

LocalizedTextConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizedTextConventionTestsBase.cs:13 · Level 5 · abstract class

  • What it is: a localized-text convention gate (ADR-027): user-visible literals must not be hard-coded in .razor or .razor.cs under Source/ (snackbar messages, page Title properties, page-title markup, breadcrumb labels) but resolve through IStringLocalizer resources.
  • Depends on: IArchitectureMap, ArchitectureMapBase.FindRepoRoot, ArchitectureRules.UserVisibleTextIsLocalized (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.LocalizedText.cs:52), which drives six source-generated regexes (:10-32).
  • Concept: cross-references the markup-scanning gate idea from FormsConventionTestsBase. [Rubric §27, i18n] assesses that visible strings follow the selected language.
  • Walkthrough: the subclass supplies Map (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizedTextConventionTestsBase.cs:15) and optionally MinimumScannedFiles (line 21, default 1) and AllowedFiles (line 28, whole-file exemptions; the preferred exemption is a per-line i18n: allow comment, per the class doc at lines 8-11). UserVisibleText_IsLocalized (line 31) resolves the repo root and delegates to the rule with the Source directory, the allowlist, and the floor (lines 33-37).
  • Where it's used: subclassed in all four repos (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:11, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/LocalizedTextConventionTests.cs:14, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:14, and Helpdesk at MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:142, which sets a floor of 5 against the seed's 8 razor files at :147). It pairs with LocalizationResourceTestsBase.

MicroserviceExtractionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/MicroserviceExtractionTestsBase.cs:8 · Level 5 · abstract class

  • What it is: a transport-boundary base for the modular-monolith to microservices path: MassTransit, gRPC, and Protobuf must never leak into Domain, Application, or Shared, so a module behaves identically in-process or extracted and the split stays reversible.
  • Depends on: IArchitectureMap, ArchitectureRules (TransportDoesNotLeakIntoCoreLayers, ArchitectureRules.Transport.cs:19, over the shared TransportDependencies list at :11).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); the extraction invariant (application and domain code talks to abstractions, transport choices live at the edges) is the ADR-006 / ADR-007 / ADR-008 story the doc cites (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/MicroserviceExtractionTestsBase.cs:3-7). [Rubric §7, Microservices Readiness] assesses exactly this reversibility. ServiceContractPurityTestsBase guards the same boundary from the contract side.
  • Walkthrough: one [Fact] CoreLayers_ShouldNotDependOn_Transport (line 13) delegating to ArchitectureRules.TransportDoesNotLeakIntoCoreLayers(Map).
  • Where it's used: subclassed in all four repos (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/MicroserviceExtractionTests.cs:11, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/MicroserviceExtractionTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/MicroserviceExtractionTests.cs:3, and Helpdesk at MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:111).

ModuleIsolationTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleIsolationTestsBase.cs:8 · Level 5 · abstract class

  • What it is: a modular-monolith boundary base: a module must not reach another module's internal layers; cross-module communication goes only through the Shared (contract) layer. It is vacuous for single-module or module-less repos.
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Modules.cs), which uses OtherModuleNamespaces to compute the forbidden targets through the private ModuleLayerIsolated (:98).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase). [Rubric §5, Vertical Slice] and [Rubric §7, Microservices Readiness] assess module autonomy. The IModule system is taught in Group 14.
  • Walkthrough: seven [Fact]s. Six name a specific pair worth its own failure message: ModuleDomains_ShouldBe_Isolated (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleIsolationTestsBase.cs:13), ModuleApplications_ShouldBe_Isolated (line 16), ModuleInfrastructures_ShouldBe_Isolated (line 19), ModuleApis_ShouldBe_Isolated (line 22), plus the two cross-layer reach rules ModuleDomains_ShouldNotReach_OtherModuleInfrastructures (line 25) and ModuleApplications_ShouldNotReach_OtherModuleInfrastructures (line 28).
    • The seventh, ModuleInternalLayers_ShouldNotReach_OtherModuleInternalLayers (line 36), closes the coverage the other six leave open: it runs the complete cross product of Domain, Application, Infrastructure and Api against every other module's four internal layers (ArchitectureRules.Modules.cs:47, the loop at :49-57). The rule doc names the gap it fixes precisely (:31-41): a per-module layer rule only forbids the SAME module's higher layers, and the compile-time layer guard only knows about framework references, so a cross-module project reference such as one module's Domain onto another's Application passed every gate while compile-coupling two modules that ADR-007/008 promise can be extracted separately.
    • Layer.Ui is deliberately excluded from that product, because a module's UI composing another module's UI is a real, intended arrangement in the shipped apps (:42-45).
  • Where it's used: subclassed in Store, ADC, and Helpdesk (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/ModuleIsolationTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/ModuleIsolationTests.cs:3, MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:15, where the single-module seed makes it deliberately vacuous but future-proof).

NamespaceCycleTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/NamespaceCycleTestsBase.cs:15 · Level 5 · abstract class

  • What it is: an acyclicity gate one level below the layer rules: the top-level namespaces inside each layer assembly a map declares must form a directed acyclic graph.
  • Depends on: IArchitectureMap and ArchitectureRules.NamespacesHaveNoDependencyCycles (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:45).
  • Concept introduced, the namespace cycle as an early extraction warning. The doc frames it plainly (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/NamespaceCycleTestsBase.cs:4-8): a namespace cycle is the first visible symptom of a folder pair that has grown into one tangled unit, where neither half can be read, tested, or lifted into its own service without the other. That is the granularity the two coarse rules (LayerDependencyTestsBase and ModuleIsolationTestsBase) cannot see inside of. [Rubric §7, Microservices Readiness] assesses extractability; [Rubric §34, Architecture Governance & Documentation] assesses coupling that makes a folder unreadable on its own.
  • Walkthrough
    • The subclass supplies Map (line 18) and optionally AllowedCycleNamespaces (line 26), fully-qualified nodes whose cycle is accepted by design.
    • The allowance rule is the interesting part: a cycle is skipped only when every namespace on its reported path appears in the list, so an allowance can never hide a NEW cycle that merely touches an accepted namespace (doc, lines 20-25). The rule enforces that over the whole strongly connected component, not just the rendered shortest path.
    • The single [Fact] Namespaces_ShouldNotHave_DependencyCycles (line 29) forwards both to the rule, which builds a per-assembly namespace graph from base types, interfaces, field, property, method return and parameter types, and attribute types, with generic arguments and array or by-ref element types expanded (ArchitectureRules.Cycles.cs:84, :163, :174, :215, :228), then finds the strongly connected components (:253) and renders the shortest cycle through each (:330).
  • Caveats / not-in-source: the rule is signature-level reflection and blind to method bodies, because this rule carries no IL dependency, so a green result means "no STRUCTURAL cycle", not "no coupling" (doc, lines 9-13). Compiler-generated types are skipped deliberately so the answer stays a signature-level one.
  • Where it's used: subclassed today only in MMCA.Common, as NamespaceCycleTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/NamespaceCycleTests.cs:9). Its AllowedCycleNamespaces records the single accepted tangle in the framework, MMCA.Common.Infrastructure to Settings to Persistence and back (:39-44), with each of the three edges justified in the doc comment above it (:13-38).

NamingConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/NamingConventionTestsBase.cs:8 · Level 5 · abstract class

  • What it is: a naming and sealing convention base across the CQRS plus DDD building blocks: handlers, command and query messages, validators, DTOs, domain events, invariants, EF configurations, specifications, and repositories each follow their established suffix and sealing convention.
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Naming.cs), which uses RuleHelpers.SimpleName to match suffixes on generic types and funnels the two message rules through the shared AssertMessageSuffix (:130).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase). [Rubric §15, Best Practices & Code Quality] and [Rubric §15, Best Practices & Code Quality] assess consistent, discoverable naming.
  • Walkthrough: ten [Fact]s: Handlers_ShouldBeSealed_WithHandlerSuffix (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/NamingConventionTestsBase.cs:13), Commands_ShouldHave_CommandOrRequestSuffix (line 16), Queries_ShouldHave_QuerySuffix (line 19), Validators_ShouldHave_ValidatorOrRulesSuffix (line 22), SharedDtos_ShouldHave_DtoOrLookupSuffix (line 25), DomainEvents_ShouldBeSealed_InDomainEventsNamespace (line 28), InvariantClasses_ShouldBe_Static (line 31), EfConfigurations_ShouldBeSealed_WithConfigurationSuffix (line 34), Specifications_ShouldBeSealed_WithSpecificationSuffix (line 37), Repositories_ShouldHave_RepositorySuffix (line 40), backed by the ten rules at ArchitectureRules.Naming.cs:21, :33, :37, :41, :54, :67, :79, :91, :103 and :116.
  • Where it's used: subclassed in Store, ADC, and Helpdesk (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/NamingConventionTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/NamingConventionTests.cs:3, MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:74).

PiiConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/PiiConventionTestsBase.cs:7 · Level 5 · abstract class

  • What it is: a GDPR/CCPA right-to-erasure base (ADR-005): any domain entity that declares a PiiAttribute-marked property must implement IAnonymizable, so it has an erasure path.
  • Depends on: IArchitectureMap, ArchitectureRules.EntitiesWithPiiImplementAnonymizable (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Governance.cs:11).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); the [Pii] plus IAnonymizable soft-delete-versus-erasure model is taught in Group 02 (ADR-005). [Rubric §30, Compliance / Privacy / Data Governance] and [Rubric §11, Security] assess erasure discipline. The IAnonymizable contract is matched by its full name (ArchitectureRules.Governance.cs:7) so a same-named local interface cannot satisfy the rule (comment, lines 5-6), while the [Pii] marker is matched by simple name (:50).
  • Walkthrough: one [Fact] EntitiesWithPiiProperties_ShouldImplement_IAnonymizable (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/PiiConventionTestsBase.cs:12) delegating to the rule, which scans every Domain-layer type for a [Pii]-marked public instance property (HasPiiProperty, ArchitectureRules.Governance.cs:48) and reports the ones that do not implement the contract.
  • Where it's used: subclassed in all four repos (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiConventionTests.cs:13, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/PiiConventionTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/PiiConventionTests.cs:3, and Helpdesk at MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:116).

ProtoContractTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ProtoContractTestsBase.cs:19 · Level 5 · abstract class

  • What it is: the frozen wire-contract guard for a repo's gRPC .proto files, the synchronous counterpart to IntegrationEventContractTestsBase. The subclass names its solution file, its proto files, and the committed snapshot; the base rebuilds the live contract and reports the diff.
  • Depends on: ArchitectureRules.ProtoContractsMatchFrozenList (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:37), which resolves paths through ArchitectureMapBase.FindRepoRoot (line 45) and parses through ProtoScope / ProtoScopeKind. There is no Map on this base: it reads committed .proto text, not assemblies.
  • Concept introduced, pinning a contract by what actually crosses the wire. The rule's remarks are explicit about the two halves of the decision (ArchitectureRules.Protos.cs:19-35). Pinned: the file's package, every service with each rpc (name, request type, response type, and both streaming flags), every message field (name, declared type, label, and field NUMBER), and every enum value and number, with nested messages and enums under their qualified name. Deliberately not pinned: syntax, import lines, and option declarations including csharp_namespace, because none of them changes a byte on the wire; including them would fail the gate on edits that break nobody, which is the fastest way to teach a team to update the snapshot without reading it. [Rubric §9, API & Contract Design] and [Rubric §7, Microservices Readiness] assess contract stability across a service boundary.
  • Walkthrough
    • Three abstract members: SolutionFileName (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ProtoContractTestsBase.cs:22, marking the repo root), ProtoFiles (line 25, repo-root-relative paths), and FrozenProtoContracts (line 30, the committed snapshot).
    • The single [Fact] ProtoContracts_ShouldMatch_TheFrozenSnapshot (line 33) forwards all three to the rule.
    • The comparison is two-directional: AssertProtoContract (ArchitectureRules.Protos.cs:58) reports lines present in the protos but not frozen with a plus marker and frozen lines missing from the protos with a minus marker (lines 67-70), then funnels both through ArchitectureAssert. A .proto path that does not exist yields one explicit missing-file line rather than silently contributing nothing (line 96), and the whole snapshot is distinct-and-ordered so the comparison is stable (line 103).
  • Why it's built this way: the remarks say the snapshot is meant to be regenerated deliberately by printing ArchitectureRules.BuildProtoContract(...) for the same files, as part of the commit that changes the contract, never edited to make a red test go green (Bases/ProtoContractTestsBase.cs:12-17). MMCA.Common ships no .proto of its own (it supplies the gRPC plumbing, not the contracts), so the framework does NOT subclass this (lines 9-11).
  • Where it's used: subclassed in the two repos with *.Contracts projects: ADC, pinning seven protos across four Contracts projects (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/ProtoContractTests.cs:3, file list at :11-17), and Store, pinning three (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/ProtoContractTests.cs:9, file list at :15-17). MMCA.Common exercises the underlying rule instead, from fixture protos including a deliberately drifted copy, in ProtoContractFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ProtoContractFitnessTests.cs:14).

ServiceContractPurityTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ServiceContractPurityTestsBase.cs:20 · Level 5 · abstract class

  • What it is: a one-rule delegating base asserting that every type marked with the framework's ServiceContractAttribute stays free of the producing service's Domain, Application, and Infrastructure, so a consumer can take the contract package without taking the producer's internals.
  • Depends on: IArchitectureMap and ArchitectureRules.ServiceContractsDoNotDependOnServiceInternals (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:32).
  • Concept introduced, the attribute-driven ratchet. Two design choices are worth reading closely, and both are recorded in the base's remarks (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ServiceContractPurityTestsBase.cs:8-18). First, the rule is attribute-driven rather than Layer.Contracts-driven, because no repo registers that layer in its map today, so a layer-iterating rule would pass vacuously forever; scanning every registered assembly for the marker enforces the invariant wherever the contract types live. Second, the base is honest about the vacuous case: a repo that has marked no type yet asserts nothing, and the value is the ratchet, the invariant bites from the first marked type onward with no test left to remember. [Rubric §7, Microservices Readiness] assesses whether an extraction stays reversible; a contract that leaks a domain entity, a handler abstraction, or a persistence type forces every consumer to depend on the producer's internals, which is what makes an extraction irreversible (ArchitectureRules.Contracts.cs:13-19). [Rubric §9, API & Contract Design] assesses the published surface itself (ADR-007).
  • Walkthrough
    • The subclass supplies Map (line 22); the single [Fact] ServiceContracts_ShouldNotDependOn_ServiceInternals (line 25) forwards to the rule.
    • In the rule, ServiceInternalNamespaces (ArchitectureRules.Contracts.cs:120) collects the distinct, ordered root namespaces of every Domain, Application and Infrastructure ref in the map (lines 122-128) and the rule returns immediately when that set is empty (lines 34-38).
    • It then loops map.Layers and runs NetArchTest per assembly with MeetCustomRule(CarriesServiceContractAttribute) as the selector (lines 40-47). That predicate (line 132) reads Mono.Cecil.TypeDefinition custom attributes and matches the public constant ServiceContractAttributeFullName (line 10) by string, the same zero-reference stance the rest of the library takes; a reflection twin for the loaded-type rules sits at line 143.
    • Because the rule scans every registered assembly, a marked type that lives inside a Domain, Application or Infrastructure assembly fails by construction, and the remarks say that is the intent: a published contract belongs in a *.Contracts or Shared assembly (ArchitectureRules.Contracts.cs:25-29).
  • Where it's used: subclassed once per repo, in all four: MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ServiceContractPurityTests.cs:11), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/ServiceContractPurityTests.cs:9), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/ServiceContractPurityTests.cs:9), and Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ServiceContractPurityTests.cs:9); see ServiceContractPurityTests. The ratchet has now engaged in the two multi-module apps: ADC marks five contract interfaces (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Shared/Events/IEventLiveValidationService.cs, its Sessions/ISessionBookmarkValidationService.cs sibling, MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/UserSessionBookmarks/IBookmarkCountService.cs, MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/Exports/IUserEngagementExportService.cs, MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.Shared/Users/IAttendeeQueryService.cs, plus MMCA.ADC/Source/Modules/Notification/MMCA.ADC.Notification.Shared/UserNotifications/IUserNotificationExportService.cs) and Store marks three (MMCA.Store/Source/Modules/Catalog/MMCA.Store.Catalog.Shared/Products/IProductVariantService.cs, MMCA.Store/Source/Modules/Identity/MMCA.Store.Identity.Shared/Customers/ICustomerService.cs, MMCA.Store/Source/Modules/Sales/MMCA.Store.Sales.Shared/Exports/IUserSalesExportService.cs), all of them in a module's Shared layer, which is exactly where the rule says a contract belongs. MMCA.Common and Helpdesk mark none, so their runs remain ratchets (MMCA.Common/Source/Core/MMCA.Common.Shared/Abstractions/ServiceContractAttribute.cs:10-12). It complements, and does not replace, ContractImplementationTestsBase, the transport-purity rule behind MicroserviceExtractionTestsBase, and the layer-purity rules behind LayerDependencyTestsBase (ADR-015).

SharedLayerTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/SharedLayerTestsBase.cs:7 · Level 5 · abstract class

  • What it is: a Shared (contract) layer base: a module's Shared is contracts-only, so it must not depend on its own internal layers, on another module's Shared, or on EF Core.
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Modules.cs:61, :80, :84).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase). [Rubric §3, Clean Architecture] and [Rubric §5, Vertical Slice] assess a clean contract boundary a would-be extracted consumer can reference safely.
  • Walkthrough: three [Fact]s: ModuleShared_ShouldNotDependOn_OwnInternalLayers (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/SharedLayerTestsBase.cs:12), ModuleShared_ShouldBe_Isolated (line 15), ModuleShared_ShouldNotDependOn_EntityFrameworkCore (line 18).
  • Where it's used: subclassed in Store, ADC, and Helpdesk (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/SharedLayerTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/SharedLayerTests.cs:3, MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:20).

SliceCohesionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/SliceCohesionTestsBase.cs:10 · Level 5 · abstract class

  • What it is: a vertical-slice cohesion base: a use-case slice keeps its command or query, its handler, and its validator together in one namespace, so a feature is a cohesive unit rather than spread across horizontal Handlers/ and Validators/ folders.
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Slices.cs:27, :55, with the namespace comparison at :103).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase). [Rubric §5, Vertical Slice] assesses feature cohesion. The doc notes MMCA.Common scopes to its Notifications slices while ADC and Store scope to their module Application layers (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/SliceCohesionTestsBase.cs:6-8).
  • Walkthrough: two [Fact]s: Handlers_ShouldBeCoLocatedWith_TheirContracts (line 15) and Validators_ShouldBeCoLocatedWith_TheirContracts (line 19). Both rules resolve the type a handler or validator is written for (HandlerContract, ArchitectureRules.Slices.cs:78; ValidatedType, :85) and only compare when the contract is a concrete type in the same assembly (:100), so a handler written against a framework abstraction is out of scope rather than misreported.
  • Where it's used: subclassed in all four repos (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionTests.cs:10, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/SliceCohesionTests.cs:9, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/SliceCohesionTests.cs:8, and Helpdesk at MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:121).

SoftDeleteEnforcementTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SoftDeleteEnforcementTestsBase.cs:17 · Level 5 · abstract class

  • What it is: a soft-delete enforcement gate: EF Core's row-erasing members may only be called from the purge and erasure types a repo names in an allowlist. Everything else deletes by setting IsDeleted = true.
  • Depends on: IArchitectureMap and ArchitectureRules.HardDeletesOnlyInAllowedTypes (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:62), which reads IL with Mono.Cecil.
  • Concept introduced, enforcing the framework's deletion model at the call site. Soft delete is the default taught in the primer, and what it buys is that a deleted row survives for audit, restore and erasure accounting; a hard delete bypasses all of it (rule doc, ArchitectureRules.SoftDelete.cs:26-35). [Rubric §8, Data Architecture] assesses the deletion model; [Rubric §30, Compliance / Privacy / Data Governance] assesses erasure accounting (ADR-005); [Rubric §13, Observability & Operability] applies because an audit trail with a silently removed row is not an audit trail.
  • Walkthrough
    • The subclass supplies Map (line 19) and AllowedHardDeleteTypes (line 27, empty by default, which bans hard deletes outright). The single [Fact] HardDeletes_ShouldOnlyOccurIn_AllowedPurgeTypes (line 30) forwards both.
    • The scope is precise. Four member names are watched (HardDeleteMemberNames, ArchitectureRules.SoftDelete.cs:10-11), and the two common ones (Remove, RemoveRange) are additionally scoped to four EF entity-set declaring types (EntitySetTypeNames, :18-24), which is what keeps every unrelated Remove on a dictionary, a list or a cache out of the rule.
    • An allowlist entry is a type full name or a namespace prefix, and it also covers the compiler-generated async state machines and closures nested inside the type it names (doc, :44-49). Matching is ordinal and case-sensitive.
    • The stated limit (:50-55) is the one worth remembering: the rule sees only direct calls compiled into the scanned assemblies, so a hard delete reached through an interface the repo owns is caught at the implementing type, not at the caller. That implementation belongs on the allowlist and the abstraction stays free to be used.
  • Why it's built this way: the doc frames the allowlist exactly as the cascade and domain-throw rules frame theirs (lines 10-15): adoption means moving each reported type into a fix or into the list with a comment, which turns "we soft-delete, mostly" into a reviewed inventory of every place that does not.
  • Where it's used: subclassed in all three repos that carry persistence plus the framework. MMCA.Common runs the rule back on its own author, naming four framework erasers individually rather than exempting a namespace, so a fifth would still fail and get reviewed: the repository's set-based delete escape hatch, outbox and inbox retention, audit-trail retention, and refresh-session retention (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:19, entries and per-entry reasoning at :24-47). Store allowlists the same four framework types plus its own image-blob side table (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/SoftDeleteEnforcementTests.cs:15, rationale at :8-13), and ADC subclasses it the same way (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:15). The framework subclass is what keeps the consumers' lists honest, since a new framework eraser would otherwise land in the packages and fail downstream instead of in the repo that wrote it.

SortableColumnConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/SortableColumnConventionTestsBase.cs:16 · Level 5 · abstract class

  • What it is: a MudDataGrid sorting gate: a column marked sortable must be a PropertyColumn, never a TemplateColumn.
  • Depends on: ArchitectureRules.SortableGridColumnsUsePropertyColumn (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.Markup.cs:42), [Fact]. There is no Map: the subclass supplies absolute directory roots directly.
  • Concept introduced, gating a defect that compiles and renders. Server-side sort reads the bound property off the column, and a TemplateColumn has none, so marking one sortable renders a header that toggles an arrow without ordering the data (doc, lines 4-8). The base doc says plainly why this belongs in a fitness function rather than a review: the defect compiles, renders, and is invisible until someone checks the order. [Rubric §24, Forms / Validation / UX Safety] assesses whether an interactive control does what it appears to do; [Rubric §18, UI Architecture] assesses using the right grid primitive for the job.
  • Walkthrough
    • The subclass supplies MarkupRoots (line 24), absolute directory paths scanned recursively for *.razor. The doc tells the author to build them from ArchitectureMapBase.FindRepoRoot so the scan is independent of the runner's working directory (lines 18-23).
    • The single [Fact] SortableColumns_ShouldNotBe_TemplateColumns (line 27) forwards to the rule.
    • The rule scans .razor TEXT rather than IL, because the defect lives in markup that compiles perfectly (rule doc, ArchitectureRules.Markup.cs:16-19). It reads each TemplateColumn tag to its closing bracket through quoted attribute values (TagEnd, :142), so attribute order, wrapped lines and a generic type argument are all handled, and it accepts the quoted, unquoted and expression-prefixed spellings of a true Sortable value while leaving a value bound to a field or property alone, since that value is not knowable from the markup (HasSortableTrue, :171; IsBoundToTrue, :198).
    • Commented-out markup is blanked before the scan, with spaces replacing the comment and newlines surviving (BlankRazorComments, :82), so a commented example neither fails the gate nor shifts the reported line numbers (doc, :31-35).
    • A missing root is reported as a violation rather than silently skipped (line 52), so a path typo cannot make the gate vacuous.
  • Where it's used: subclassed in ADC and Store, both pointing at the whole Source tree so the scan covers the three module UI projects and both web heads, and both freezing an already-clean state (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/SortableColumnConventionTests.cs:11, root at :16; MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/SortableColumnConventionTests.cs:11, root at :16).

SpecificationConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SpecificationConventionTestsBase.cs:10 · Level 5 · abstract class

  • What it is: an opt-in base for the Specification pattern in polyglot / database-per-service repos: it guarantees no specification filters by navigating to another entity, which would not translate when that entity lives in a different physical source.
  • Depends on: IArchitectureMap, ArchitectureRules.SpecificationsDoNotNavigateToOtherEntities (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:24), backed by CrossEntityNavigationFinder.
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); the Specification<TEntity, TIdentifierType> pattern is taught in Group 03. [Rubric §8, Data Architecture] assesses engine-portable query design.
  • Walkthrough: one [Fact] Specifications_ShouldNotNavigate_ToOtherEntities (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SpecificationConventionTestsBase.cs:16) delegating to the rule, which discovers specifications by base-type full-name prefix across the Application and Domain layers (ArchitectureRules.Specifications.cs:30-33) and resolves each one's entity type from the generic base (SpecificationEntityType, :79). The doc notes single-engine repos need not subclass it (lines 4-8).
  • Where it's used: subclassed in Store, ADC, and Helpdesk (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/SpecificationConventionTests.cs:7, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/SpecificationConventionTests.cs:8, MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:29). MMCA.Common exercises the same rule from the other side, through SpecificationFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:13) and its private SpecTestMap fixture (:40).

AccessibilityViolationException

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/AccessibilityViolationException.cs:7 · Level 0 · sealed class

  • What it is: the exception thrown when an axe-core accessibility scan finds one or more WCAG violations on the page under test.
  • Depends on: System.Exception (BCL) only. Its XML doc names the single thrower, PageExtensions.AssertNoAccessibilityViolationsAsync (MMCA.Common.Testing.E2E/Infrastructure/AccessibilityViolationException.cs:3-6).
  • Concept introduced, the accessibility gate as a hard failure. Rather than logging a warning or returning a result object, a violated a11y scan throws, so a consumer E2E [Fact] that calls AssertNoAccessibilityViolationsAsync goes red and names the offending elements. [Rubric §21, Accessibility] assesses whether accessibility is verified rather than assumed; a dedicated exception type makes an a11y regression a first-class, catchable build failure. [Rubric §28, Front-End Testing] assesses whether the UI is exercised through realistic automated checks; this is the failure primitive those checks throw.
  • Walkthrough: three constructors, the parameterless, message, and message-plus-inner overloads (AccessibilityViolationException.cs:10, :15, :21), the standard exception shape. It carries no extra state: the human-readable violation summary is baked into the message string the thrower builds.
  • Why it's built this way: a purpose-named exception (not a bare Exception or InvalidOperationException) lets a test that deliberately probes a known-inaccessible page assert on exactly this type, and it reads clearly in a failure log.
  • Where it's used: thrown only by PageExtensions.AssertNoAccessibilityViolationsAsync (MMCA.Common.Testing.E2E/Infrastructure/PageExtensions.cs:330-331), which is in turn called by the ScanAsync/ScanGridAsync helpers on E2ETestBase and by the *_ShouldHaveNoAccessibilityViolations facts on the workflow bases.

AdminCredentials

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:66 · Level 0 · nested static class

  • What it is: a nested static class on E2ETestConfiguration that resolves the seeded admin login (email and password) for E2E runs, with an environment-variable override in front of a per-app default.
  • Depends on: System.Environment (BCL). It is the admin half of the credential pair; UserCredentials is its structurally identical regular-user twin.
  • Concept: the environment-over-default resolution taught in E2ETestConfiguration. DefaultEmail/DefaultPassword have public setters so a downstream app seeds its own admin identity from a [ModuleInitializer], while E2E_ADMIN_EMAIL/E2E_ADMIN_PASSWORD win when set. [Rubric §11, Security] assesses how test credentials are handled; keeping them out of committed app code and injectable per environment is the safe end of that.
  • Walkthrough: DefaultEmail is "admin@localhost" and DefaultPassword is "Admin123!" (MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:68-69); Email and Password read E2E_ADMIN_EMAIL/E2E_ADMIN_PASSWORD and fall back to those defaults (:71-75).
  • Where it's used: read by E2ETestBase.LoginAsAdminAsync (MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:92-93). Both consumer suites overwrite the default from a module initializer: admin@mmca.com in Store (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Infrastructure/TestSetup.cs:10) and admin@adc.com in ADC (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Infrastructure/TestSetup.cs:14).

AuthOutcome

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/AuthOutcomeRules.cs:4 · Level 0 · internal enum

  • What it is: the three verdicts a post-submit login or registration attempt can reach: Succeeded, ErrorShown, and Silent (MMCA.Common.Testing.E2E/Infrastructure/AuthOutcomeRules.cs:7, :10, :13).
  • Depends on: nothing; a plain internal enum, reachable from the framework's own gallery suite through the InternalsVisibleTo in the package's csproj (MMCA.Common.Testing.E2E/MMCA.Common.Testing.E2E.csproj:12).
  • Concept introduced, naming the silent failure. Two of the three members are the obvious outcomes. The third, Silent, is the one worth teaching: a submit that produced neither a navigation nor a rendered error alert (a 500 that renders nothing, a dropped request, a JS exception mid-submit) is not a success and not a reported failure, and it needs its own name precisely so the wait can refuse to treat it as either (doc, :12-13). [Rubric §14, Testability] assesses whether a harness can tell "passed" from "never actually ran"; making the third state a first-class enum member is how this package does that.
  • Walkthrough: each member carries a doc comment stating the signal combination behind it: the page navigated away or a signed-in state is showing (:6-7), the auth page is still showing with an error alert on it (:9-10), or none of the three signals fired (:12-13).
  • Where it's used: returned by AuthOutcomeRules.Classify and branched on inside E2ETestBase.WaitForAuthResultAsync (MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:272, :253), and asserted directly by AuthOutcomeRulesTests.

AxeOptions

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/AxeOptions.cs:9 · Level 0 · static class

  • What it is: the shared axe-core run options that scope every accessibility scan to one documented target, WCAG 2.1 AA, so the framework gallery and all downstream apps scan against the same rule set.
  • Depends on: Deque.AxeCore.Commons (NuGet: AxeRunOptions, RunOnlyOptions, RuleOptions, MMCA.Common.Testing.E2E/Infrastructure/AxeOptions.cs:1; see primer §3).
  • Concept introduced, the scoped accessibility target. A raw axe run also emits "best-practice" advisories that are not conformance failures; pinning RunOnly to the WCAG tag set makes the gate fail only on real WCAG 2.1 AA violations (AxeOptions.cs:11-16). [Rubric §21, Accessibility] assesses whether the accessibility bar is explicit and enforced; freezing the target in one shipped object is how three repos stay honest to the same standard. [Rubric §22, Responsive/Cross-Browser] also applies through the pager exception below, which documents a specific third-party component limitation.
  • Walkthrough: two static presets, both read-only properties initialized once.
    • Wcag21Aa (AxeOptions.cs:17) sets RunOnly to Type = "tag" with the four WCAG A/AA tag values wcag2a, wcag2aa, wcag21a, wcag21aa (:19-23). This is the target for every strict scan.
    • Wcag21AaExceptMudPagerCombobox (:35) repeats that tag set and adds a Rules dictionary disabling aria-input-field-name (:42-45), for grid list pages whose only violation is MudBlazor's internal MudTablePager "rows per page" select. The XML doc (:26-34) records the detail: MudBlazor 9.6.0 mirrored combobox semantics onto the hidden-input presenter, the pager's own select gets no accessible name, and it is not reachable from app markup (no Label or aria-label parameter on MudTablePager), so this is an accepted upstream limitation. The doc warns it must be used only on a page whose sole combobox is a pager.
  • Why it's built this way: shipping the options in the package rather than re-declaring them per test guarantees every consumer scans the identical rule set; the narrowly scoped pager exception keeps one known third-party gap from forcing a blanket rule-disable across all scans.
  • Where it's used: passed to PageExtensions.AssertNoAccessibilityViolationsAsync through E2ETestBase.ScanAsync (strict Wcag21Aa, MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:368) and .ScanGridAsync (the pager exception, :329), and directly by the *_ShouldHaveNoAccessibilityViolations facts on UserLoginTestsBase (MMCA.Common.Testing.E2E/Workflows/Identity/UserLoginTestsBase.cs:82), UserRegistrationTestsBase (MMCA.Common.Testing.E2E/Workflows/Identity/UserRegistrationTestsBase.cs:91), ProfileManagementTestsBase (MMCA.Common.Testing.E2E/Workflows/Identity/ProfileManagementTestsBase.cs:178), and PasswordResetTestsBase (MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:88, :99).

E2ETestConfiguration

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:8 · Level 0 · static class

  • What it is: the single environment-variable-driven configuration surface for the whole E2E package: base URL, headless mode, timeouts, browser engine, slow-motion and trace capture, plus the nested AdminCredentials and UserCredentials.
  • Depends on: System.Environment (BCL) only.
  • Concept introduced, environment-driven test configuration. Every knob resolves as "read an E2E_* environment variable, else use a default", so the same compiled suite runs against localhost on a developer box and against a CI-provisioned host with no code change. A few Default* properties carry public setters so a consuming app supplies app-specific defaults through a [ModuleInitializer], while environment variables always take precedence (MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:3-7). [Rubric §17, DevOps] assesses whether the suite is CI-portable and configurable outside the binary; this class is that story. [Rubric §22, Responsive/Cross-Browser] applies because Browser selects the engine CI iterates over.
  • Walkthrough: teaching order.
    • DefaultBaseUrl (settable, https://localhost:7108, E2ETestConfiguration.cs:10) and BaseUrl, which prefers E2E_BASE_URL (:12-13). ADC's module initializer overrides that default to https://localhost:6002, the port its AppHost pins the Blazor UI to (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Infrastructure/TestSetup.cs:13).
    • Headless treats any value other than "false" as headless, compared case-insensitively (:15-16); SlowMo slows each Playwright action by an environment-set millisecond count for visual debugging, default 0 (:45-46).
    • DefaultTimeout (30_000 ms, from E2E_TIMEOUT, :18-19) is the general action timeout. AuthTimeout (:27-28) is a separately tunable ceiling for the slowest step, the post-auth wait, inheriting DefaultTimeout unless E2E_AUTH_TIMEOUT is set. AuthGraceTimeout (15_000 ms, E2E_AUTH_GRACE, :38-39) is the extra grace window that de-flakes the register/login success-detection race, the transient error-alert flash during a Server-mode forceLoad.
    • Browser selects chromium (default), firefox, or webkit from E2E_BROWSER (:53-54); TracePath returns a non-empty E2E_TRACE path or null, enabling full-speed Playwright trace capture (:63-64).
  • Why it's built this way: separating AuthTimeout and AuthGraceTimeout from the general DefaultTimeout is deliberate. The auth round-trip (full sign-in plus forceLoad reload plus re-render) can spike past a normal action budget on a contended CI runner, so it is tuned independently rather than by inflating every timeout in the suite. The doc ties the grace window to the TD-06/07 contention cluster and names the rejected alternative, forcing WASM, which broke login (:30-37).
  • Where it's used: read throughout PlaywrightFixture (engine, headless, slow-mo) and E2ETestBase (base URL, timeouts, trace path, credentials).

ForgotPasswordPage

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.PageObjects · MMCA.Common.Testing.E2E/PageObjects/ForgotPasswordPage.cs:6 · Level 0 · sealed class

  • What it is: the Page Object for the shared /forgot-password screen, the entry point of the password-recovery flow. It exposes the address field, the submit button, the confirmation alert, and the return-to-login link, plus a one-call RequestResetAsync action.
  • Depends on: Microsoft.Playwright (IPage, ILocator, AriaRole) and the PageExtensions helpers GotoAndWaitForBlazorAsync and FillAndVerifyAsync (MMCA.Common.Testing.E2E/PageObjects/ForgotPasswordPage.cs:1-2).
  • Concept: the Page Object Model taught in LoginPage. One locator carries a design decision rather than a selector detail: ConfirmationAlert targets the success alert unconditionally, and the inline comment states why, the page lands on the same success alert whether or not the address has an account (ForgotPasswordPage.cs:15-16). That is the anti-enumeration contract of ADR-091 expressed as a test affordance: there is deliberately no "unknown address" locator to assert on, because the UI must not render one. [Rubric §11, Security] assesses whether account enumeration is closed off; a Page Object that cannot express the enumerating assertion is a small structural guard on that. [Rubric §28, Front-End Testing] applies as with every Page Object here.
  • Walkthrough: a private IPage field set in the constructor (ForgotPasswordPage.cs:8-10); EmailField located by label and SubmitButton by its accessible name "Send a password reset link" (:12-13); ConfirmationAlert as MudBlazor's .mud-alert-text-success class (:16); BackToLoginLink located by link role, with the comment recording that "Back to Sign In" is a MudButton with Href and therefore renders as an <a> (:18-19). GotoAsync full-loads /forgot-password and waits for interactivity (:21-22). RequestResetAsync fills the address through PageExtensions.FillAndVerifyAsync and clicks submit (:24-28).
  • Where it's used: driven by PasswordResetTestsBase for the unknown-address confirmation fact and the a11y fact (MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:44-58, :79-89), and by the framework's own gallery suite, which exercises it against the backend-less gallery host including the confirmation state's own separate scan (ForgotPasswordPageE2ETests, MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Auth/ForgotPasswordPageE2ETests.cs:17, :28, :41, :50).

LoginPage

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.PageObjects · MMCA.Common.Testing.E2E/PageObjects/LoginPage.cs:6 · Level 0 · sealed class

  • What it is: the Page Object for the shared /login screen. It exposes the login form's controls as named ILocator properties and offers GotoAsync/LoginAsync actions, so a test says loginPage.LoginAsync(email, password) instead of hand-querying the DOM.
  • Depends on: Microsoft.Playwright (IPage, ILocator, AriaRole) and the PageExtensions helpers GotoAndWaitForBlazorAsync and FillAndVerifyAsync (MMCA.Common.Testing.E2E/PageObjects/LoginPage.cs:1-2).
  • Concept introduced, the Page Object Model. A Page Object wraps one screen behind an intention-revealing API, locating controls by their accessible name (GetByLabel("Email"), GetByRole(AriaRole.Button, Name = "Sign in to your account")) rather than by brittle CSS. That keeps tests coupled to what a user sees, not to MudBlazor's internal class names, and it centralizes each selector in one place. [Rubric §28, Front-End Testing] assesses whether E2E tests are maintainable; the Page Object is the canonical pattern for that. [Rubric §21, Accessibility] applies indirectly: locating by role and label only works if the component renders proper accessible names, so the test style pressures accessible markup.
  • Walkthrough: a private IPage field set in the constructor (LoginPage.cs:8-10); locator properties for EmailField, PasswordField, LoginButton, the ErrorAlert (MudBlazor's .mud-alert-text-error class), and the CreateAccountLink, which the inline comment explains is a MudButton with Href and therefore renders as an <a> located by link role (:12-18). GotoAsync navigates through GotoAndWaitForBlazorAsync("/login") (:20-21); LoginAsync fills both fields through the shared FillFieldAsync and then clicks (:23-28). The private FillFieldAsync delegates to PageExtensions.FillAndVerifyAsync (:31-32), guarding the Blazor re-hydration race without a fixed delay.
  • Where it's used: instantiated by UserLoginTestsBase for the invalid-password, create-account-link, and accessibility facts, and by PasswordResetTestsBase to reach the login screen before probing the recovery entry point (MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:28-29).
  • Caveats / not-in-source: the Page Object exposes no locator for the "Forgot your password?" link; the one test that asserts it locates it directly off the page (PasswordResetTestsBase.cs:31).

ProfilePage

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.PageObjects · MMCA.Common.Testing.E2E/PageObjects/ProfilePage.cs:6 · Level 0 · sealed class

  • What it is: the Page Object for the authenticated /profile screen, exposing the name, address, and password sections' fields and buttons as named locators.
  • Depends on: Microsoft.Playwright and PageExtensions.BlazorNavigateAsync (MMCA.Common.Testing.E2E/PageObjects/ProfilePage.cs:1-2).
  • Concept: the Page Object Model taught in LoginPage. One difference is load-bearing: GotoAsync uses BlazorNavigateAsync("/profile"), client-side routing (ProfilePage.cs:34-35), not a full page load, because /profile is [Authorize] and server-side rendering cannot read the JWT from browser storage, so a full load would bounce to /login. [Rubric §28, Front-End Testing] and [Rubric §11, Security] both apply: exercising the authenticated page correctly requires respecting the client-token boundary.
  • Walkthrough: three commented locator groups. Name (FirstNameField, LastNameField, SaveNameButton, :12-15); address (six fields plus SaveAddressButton, :17-24); password (CurrentPasswordField, NewPasswordField located with Exact = true so it does not also match "Confirm New Password", ConfirmNewPasswordField, ChangePasswordButton, :26-30). ErrorAlert is located by ARIA alert role rather than a MudBlazor class (:32). GotoAsync is the client-side navigation described above (:34-35).
  • Where it's used: by ProfileManagementTestsBase for all six of its facts. ADC's own ProfileManagementTests covers the same screen without deriving from the shared base or using this Page Object (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/ProfileManagementTests.cs:8).

PseudoLocalizedPage

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Globalization · MMCA.Common.Testing.E2E/Workflows/Globalization/PseudoLocalizationTestsBase.cs:23 · Level 0 · sealed record

  • What it is: the declaration of one page under the pseudo-localization gate: the Path to load, an EnglishProbe string owned by a resource rendered on that page, and an optional SettleSelector proving the page's async content arrived.
  • Depends on: nothing but the BCL. It is a three-parameter positional record with the third parameter defaulted to null (MMCA.Common.Testing.E2E/Workflows/Globalization/PseudoLocalizationTestsBase.cs:23).
  • Concept introduced, the probe string as a two-way canary. Under the qps-Ploc pseudo culture every letter gains a combining accent, so the plain en-US form of EnglishProbe appearing on the page can only mean some render path bypassed the localizer (doc, :14-18). The same probe is then re-used under the default culture to assert it IS present, which is what keeps the probe honest: a probe that drifted from its resource value would make the leak assertion pass vacuously. [Rubric §27, i18n] assesses whether localization is machine-verified rather than eyeballed; carrying the probe next to the path is what turns a per-repo page list into a data declaration instead of test code.
  • Walkthrough: Path is loaded through the culture endpoint by the base; EnglishProbe must be a string known to come from a resource rendered on the page (doc, :14-18); SettleSelector is a selector whose visibility proves the async content rendered (a seeded grid row, a card, a static section), and null waits only for the loading indicator to clear (doc, :19-22).
  • Where it's used: the element type of PseudoLocalizationTestsBase.ScannedPages (:60). ADC declares three, home with a .location-section settle selector, the speaker card grid, and the session grid (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/PseudoLocalizationTests.cs:39-44); Store declares three with no settle selectors at all (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/PseudoLocalizationTests.cs:41-46).

RegisterPage

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.PageObjects · MMCA.Common.Testing.E2E/PageObjects/RegisterPage.cs:6 · Level 0 · sealed class

  • What it is: the Page Object for the /register screen, exposing the registration form (name, email, password, plus the optional address panel) and a RegisterAsync action.
  • Depends on: Microsoft.Playwright and PageExtensions.GotoAndWaitForBlazorAsync/FillAndVerifyAsync (MMCA.Common.Testing.E2E/PageObjects/RegisterPage.cs:1-2).
  • Concept: the Page Object Model taught in LoginPage, applied to a longer form. PasswordField uses GetByLabel("Password", Exact = true) so it does not also match "Confirm Password" (RegisterPage.cs:15), and the optional address fields sit inside an expansion panel located by its text (:24-29). [Rubric §28, Front-End Testing] applies.
  • Walkthrough: locator properties for the five required fields plus RegisterButton and ErrorAlert (:12-18), the AlreadyHaveAccountLink sign-in link (:21), and the optional address panel and fields (:24-29). GotoAsync full-loads /register (:31-32); RegisterAsync fills the five required fields through the shared helper, reusing the same password for the confirm field, then clicks (:34-42); the private FillFieldAsync delegates to PageExtensions.FillAndVerifyAsync (:48-49).
  • Where it's used: instantiated by UserRegistrationTestsBase for all four of its facts.

ResetPasswordPage

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.PageObjects · MMCA.Common.Testing.E2E/PageObjects/ResetPasswordPage.cs:6 · Level 0 · sealed class

  • What it is: the Page Object for the shared /reset-password screen, the redemption half of the recovery flow. It exposes the address, token, and new-password fields, both outcome alerts, the return-to-login link, and two navigation entry points: the bare page and the prefilled emailed-link form.
  • Depends on: Microsoft.Playwright and the PageExtensions helpers GotoAndWaitForBlazorAsync and FillAndVerifyAsync (MMCA.Common.Testing.E2E/PageObjects/ResetPasswordPage.cs:1-2).
  • Concept: the Page Object Model taught in LoginPage. What is worth teaching here is GotoWithLinkAsync, which reproduces the way a real user arrives: the emailed link carries the address and the token as query parameters, so both fields land prefilled and the test exercises the same route the mail does (ResetPasswordPage.cs:30-36). [Rubric §24, Forms/Validation/UX Safety] assesses whether the recovery form's real arrival paths are covered; modelling both the bare and the linked entry is how this Page Object does it.
  • Walkthrough: a private IPage field set in the constructor (:8-10); EmailField and TokenField located by label (:12-13); NewPasswordField located with Exact = true, with the comment spelling out that "New Password" is a substring of "Confirm New Password" so the default substring match would resolve to both fields (:15-17), and ConfirmPasswordField beside it (:18). SubmitButton is located by the accessible name "Reset your password" (:20); ErrorAlert and SuccessAlert are the two MudBlazor alert classes (:21-22); GoToLoginLink is again a link-role locator over a MudButton with Href (:24-25). GotoAsync loads the bare page (:27-28); GotoWithLinkAsync builds the /reset-password URL with the address and token query parameters, escaping both with Uri.EscapeDataString (:34-36); ResetAsync fills all four fields through FillAndVerifyAsync and clicks submit (:38-45).
  • Where it's used: by PasswordResetTestsBase for the empty-form validation fact and the a11y fact (MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:61-76, :92-100), and by the framework's gallery suite, which additionally asserts the query-string prefill round-trip (ResetPasswordPageE2ETests, MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Auth/ResetPasswordPageE2ETests.cs:17, :31, :43).
  • Caveats / not-in-source: no test in this package submits a genuine token. PasswordResetTestsBase's doc states why (the token only reaches the user by email, so consuming one is an app-side integration-test concern, PasswordResetTestsBase.cs:10-16), so ResetAsync and SuccessAlert are shipped affordances that the framework's own suites do not currently drive end to end.

UserCredentials

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:78 · Level 0 · nested static class

  • What it is: the regular (non-admin) counterpart to AdminCredentials, a nested static class on E2ETestConfiguration resolving the seeded customer login, environment override in front of a per-app default.
  • Depends on: System.Environment (BCL).
  • Concept: identical in shape to AdminCredentials; only the environment-variable names and the defaults differ.
  • Walkthrough: DefaultEmail is "user@localhost" and DefaultPassword is "User123!" (MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:80-81); Email/Password prefer E2E_CUSTOMER_EMAIL/E2E_CUSTOMER_PASSWORD (:83-87).
  • Where it's used: read by E2ETestBase.LoginAsUserAsync (MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:95-96); the default is overridden to customer@mmca.com in Store and customer@adc.com in ADC (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Infrastructure/TestSetup.cs:11, MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Infrastructure/TestSetup.cs:15).

WebVitalsSample

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:76 · Level 0 · sealed record

  • What it is: an immutable record holding one page's measured Core Web Vitals: Lcp, Cls, Fcp, Ttfb, and Inp (milliseconds, except unitless CLS).
  • Depends on: System.Text.Json.Serialization.JsonPropertyName (BCL) for the lowercase wire names.
  • Concept introduced, the vitals value object. Each property is init-only with a short JSON name (lcp, cls, fcp, ttfb, inp, MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:78-86), so the record deserializes directly from the window.__vitals JSON the browser observers accumulate. [Rubric §23, Front-End Performance] assesses whether client-side performance is measured; this record is the typed shape those measurements land in.
  • Walkthrough: a sealed record with five init doubles and no behavior (WebVitalsCollector.cs:76-87). It is the deserialization target of WebVitalsCollector.CollectAsync, which falls back to a fresh all-zero instance when the JSON deserializes to null (:56).
  • Where it's used: produced by WebVitalsCollector.CollectAsync, wrapped by WebVitalsArtifact for the JSON artifact, asserted by WebVitalsBudget.AssertWithinBudget, and returned to the caller by WebVitalsPageExtensions.MeasureWebVitalsAsync for any further app-specific assertion.

AuthOutcomeRules

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/AuthOutcomeRules.cs:28 · Level 1 · internal static class

  • What it is: the decision behind E2ETestBase.WaitForAuthResultAsync, extracted as a single pure function that maps three observed browser signals onto an AuthOutcome.
  • Depends on: AuthOutcome only. No Playwright, no xUnit: that is the point.
  • Concept introduced, extracting the decision out of the wait. The surrounding wait is inherently browser-bound (three racing Playwright waits), which makes its verdict hard to test. Pulling the classification into a parameterless-of-browser function makes the interesting case, the silent failure, assertable in a unit test with no server that has to be staged into failing without rendering anything. The type doc spells out the bug this closes: a submit that failed with neither a navigation nor a rendered error alert used to leave the wait returning normally, the caller's follow-up interactivity wait was already satisfied by the still-rendered auth page, and login and registration then reported success on a sign-in that never happened (MMCA.Common.Testing.E2E/Infrastructure/AuthOutcomeRules.cs:20-26). [Rubric §14, Testability] assesses whether logic is separable from the infrastructure it runs on; [Rubric §15, Best Practices & Code Quality] applies because the precedence rule now lives in one readable place instead of inside a Task.WhenAny continuation.
  • Walkthrough: one method. Classify(bool navigatedAway, bool errorAlertVisible, bool logoutVisible) (:38) returns Succeeded when the page navigated away from the auth page OR the signed-in state's logout control is showing (:40-43), and otherwise ErrorShown if an error alert is visible, Silent if not (:45). The precedence is deliberate and documented (:30-34): a completed forceLoad is unambiguous, so an error alert flashed on the way out is not a failure, and a visible logout button is the same verdict reached through interactivity instead of through navigation.
  • Why it's built this way: the E2E package deliberately keeps its surface small, so this stays internal and is opened to exactly one assembly through InternalsVisibleTo on MMCA.Common.UI.E2E.Tests (MMCA.Common.Testing.E2E/MMCA.Common.Testing.E2E.csproj:12), which is the framework's own gallery test project. That is the narrow version of "make it testable" rather than widening a shipped public API for a test.
  • Where it's used: called once, by E2ETestBase.WaitForAuthResultAsync (MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:267-270), and covered directly by six browser-free facts in AuthOutcomeRulesTests (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Auth/AuthOutcomeRulesTests.cs:15, :22, :29, :34, :39, :44).
  • Caveats / not-in-source: both the class doc and the test class describe the rule as a "four-way classification" (AuthOutcomeRules.cs:18, AuthOutcomeRulesTests.cs:8), while AuthOutcome declares three members; the code is the authority here.

PageExtensions

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/PageExtensions.cs:23 · Level 1 · static class

  • What it is: the interactivity toolbox of the E2E package: C# extension(T) members over Playwright's IPage and ILocator that wait for Blazor to become interactive, navigate its InteractiveAuto pages correctly, fill and click through the re-hydration race, drive a MudDataGrid list page (search, delete-confirm, settle), and run an axe-core accessibility scan.
  • Depends on: Microsoft.Playwright (IPage, ILocator, Assertions), Deque.AxeCore.Playwright/Commons (RunAxe, AxeRunOptions), and System.Text.RegularExpressions (MMCA.Common.Testing.E2E/Infrastructure/PageExtensions.cs:1-5). It throws AccessibilityViolationException.
  • Concept introduced, waiting for Blazor InteractiveAuto interactivity. The apps render with InteractiveAuto plus prerendering, so a page first appears as static HTML before the WASM runtime (or the SignalR circuit) wires event handlers; a click or fill that lands in that window is silently ignored (PageExtensions.cs:9-14). The load-bearing detail is that "the runtime loaded" is a false green: window.Blazor._internal is set before components have attached handlers, so the wait has a second phase keyed on the data-mmca-interactive attribute that MmcaThemeProviders in MMCA.Common.UI stamps on the document element from its first interactive render (:41-48, remarks at :68-84). Signal-based waits instead of fixed sleeps are the difference between a flaky suite and a deterministic one. [Rubric §28, Front-End Testing] assesses whether the suite is reliable against real render timing; [Rubric §21, Accessibility] applies through the axe scan; [Rubric §22, Responsive/Cross-Browser] because the same waits must hold on all three engines. The type also demonstrates the extension(T) member syntax used across the framework (see primer §4).
  • Walkthrough: four private script constants and one public selector constant, then two extension blocks.
    • The constants. MudDataGridRowSelector is the public default data-row selector for a MudDataGrid in table layout (:29). BlazorRuntimeLoadedPredicate probes for any truthy window.Blazor._internal and is shared by the wait and by GotoProtectedAsync's readiness probe so the two cannot drift (:31-39). InteractiveMarkerPredicate probes for the data-mmca-interactive attribute (:47-48). TimedSettleScript is the marker-free settle, two animation frames plus a 500 ms delay, for a click that may land on a page with no marker at all (:50-57); SingleFrameSettleScript is the one-frame flush used once the marker has already confirmed interactivity (:60).
    • extension(IPage page) (:62). WaitForBlazorAsync runs the two-phase wait then flushes one frame (:85-97). GotoAndWaitForBlazorAsync navigates, waits for LoadState.Load rather than NetworkIdle (which never settles under a persistent SignalR socket), then waits for interactivity (:103-110). BlazorNavigateAsync drives Blazor's client-side router through Blazor.navigateTo, tolerating the context-destroyed race a forceLoad can cause, then polls window.location.pathname instead of WaitForURLAsync (whose default Load wait hangs on a same-document navigation) and re-asserts interactivity, retrying once if that itself races a reload (:118-151). GotoProtectedAsync reaches an [Authorize] page by first ensuring Blazor is up (loading a public page when it is not) and re-routing through / so the target always gets a fresh component lifecycle, then client-navigating (:160-191). WaitForPageAndBlazorAsync covers a full-page navigation's load-plus-timed-settle (:197-206). AssertNoAccessibilityViolationsAsync runs RunAxe (with optional AxeOptions), returns early on zero violations, and otherwise builds a per-node summary and throws AccessibilityViolationException (:307-332).
    • The list-page trio, also on IPage. SearchAndWaitForRowAsync settles the grid, fills the search field through FillAndVerifyAsync, then waits for a row containing the term (:233-249); its remarks record why each of the three steps replaced a hand-rolled per-page copy, one of which slept a fixed 1.5 seconds (:208-228). ConfirmDeleteAsync clicks a delete affordance, confirms the dialog by data-testid rather than by a MudBlazor filled-button class (which silently changed meaning when a dialog gained a second filled action), and settles the resulting grid reload (:266-285, rationale at :251-262). WaitForGridToSettleAsync waits for zero [role='progressbar'] elements and deliberately does NOT wait for a row, because an empty result set is a legitimate state (:287-299).
    • extension(ILocator locator) (:335). FillAndVerifyAsync fills, then auto-waits ToHaveValueAsync, and if the value was wiped by re-hydration it clears the field, re-types character by character with a 20 ms delay, and re-asserts (:347-366). This is the single shared fill helper the base and the Page Objects all call. ClickAndVerifyAsync waits for interactivity, then clicks and waits a third of the timeout for the expected effect, up to three clicks in total, so a genuinely applied click is never re-issued and only a no-op click is retried (:380-411). ClickAndWaitForUrlAsync clicks a navigating link and re-clicks until the URL matches the supplied regular expression, for grid rows whose cells wrap content in MudLink so a row-center click lands on padding (:423-446).
    • The private CompactHtml collapses a violating node's markup to one trimmed line, truncated at 220 characters, so the failure message points at the exact offending element (:455-464).
  • Why it's built this way: the fill and click helpers exist because InteractiveAuto's prerender-then-hydrate model makes a bare fill or click a race on a fast host; auto-waiting assertions with a bounded re-type or re-click are strictly safer than fixed delays, since they succeed as soon as the value or effect appears. The interactivity marker is the honest gate, so a page that never stamps it fails the wait rather than proceeding to click dead controls (:82-83). Two [SuppressMessage] attributes document analyzer false positives across the extension(T) boundary: CA1708 on the class, where the compiler-generated grouping members read as case-colliding (:15-18), and IDE0051 for the private constants and for CompactHtml, which the SDK 10.0.201+ analyzer cannot see being referenced from inside an extension block (:19-22, :451-454).
  • Where it's used: throughout the Page Objects (ForgotPasswordPage, LoginPage, ProfilePage, RegisterPage, ResetPasswordPage), inside E2ETestBase (FillFieldAsync, ScanAsync, ScanGridAsync, the navigation helpers), by WebVitalsPageExtensions, and directly by every workflow base in this group. The list-page trio is what the consumer Page Objects are built on: Store's ProductListPage and CategoryListPage and ADC's session, speaker, room, event, question, category and user list pages all delegate their search and delete to it (for example MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/PageObjects/ProductListPage.cs:21, :42, and MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/PageObjects/Conference/Sessions/SessionListPage.cs:39, :51).

PlaywrightFixture

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:6 · Level 1 · sealed class

  • What it is: the xUnit collection fixture that owns the Playwright driver and one launched browser for the whole E2E collection, selecting the engine from the environment.
  • Depends on: Microsoft.Playwright (IPlaywright, IBrowser, BrowserTypeLaunchOptions) and xUnit's IAsyncLifetime (MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:1-2). It reads E2ETestConfiguration.
  • Concept introduced, the shared browser fixture. Launching a browser is expensive, so one instance is created once per test collection and shared across every test rather than created per test. [Rubric §22, Responsive/Cross-Browser] assesses cross-engine coverage, and the inline comment names that rubric category explicitly (PlaywrightFixture.cs:15-16): environment-selecting the engine here is what lets CI run the identical suite once per browser. [Rubric §14, Testability] applies too, since sharing one costly resource keeps the suite fast.
  • Walkthrough: Playwright and Browser are public properties with private setters (:8-9). InitializeAsync creates the driver (:13), switches on E2ETestConfiguration.Browser.ToUpperInvariant() to pick Firefox, WebKit, or (for any unrecognized value) Chromium (:17-22), and launches it with Headless and SlowMo from configuration (:24-28). DisposeAsync suppresses finalization, then disposes the browser and the driver defensively (:31-44): the browser is disposed only through an is { } browser pattern match and the driver through a null-conditional call, because a failed InitializeAsync (missing browser binaries, a launch timeout) leaves both null despite the null! declarations, and the resulting NullReferenceException from disposal used to replace the real launch error in the run output (:35-37).
  • Where it's used: bound to the collection by E2ETestCollection and injected into every E2ETestBase subclass, which opens a fresh browser context per test off this shared Browser.

WebVitalsArtifact

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:90 · Level 1 · sealed record

  • What it is: the small envelope record written to disk as web-vitals-{label}.json: a Label, the page Path, and the measured WebVitalsSample.
  • Depends on: WebVitalsSample; serialized with System.Text.Json.
  • Concept: the citable-artifact wrapper. Pairing the raw vitals with the label and path they were taken on makes the JSON file self-describing for a CI reviewer. [Rubric §23, Front-End Performance] assesses whether performance evidence is captured and traceable; the envelope is what makes an uploaded artifact interpretable.
  • Walkthrough: a three-parameter positional sealed record (MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:90), constructed inside WebVitalsCollector.WriteArtifactAsync and serialized with the shared WriteIndented = true options (:37, :69-71).
  • Where it's used: only by WebVitalsCollector.WriteArtifactAsync.

WebVitalsBudget

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:103 · Level 1 · sealed record

  • What it is: a per-page Core Web Vitals budget (five ceilings) plus the assertion mechanics every consumer's budget test shares: format the sample as one citable line, then assert each metric is within its ceiling.
  • Depends on: WebVitalsSample, AwesomeAssertions (the Should().BeLessThanOrEqualTo(...) calls, MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:4), and System.Globalization for culture-invariant formatting (:1).
  • Concept introduced, the shared budget with consumer-owned numbers. WebVitalsCollector measures; this record decides what counts as too slow, and the split is deliberate: the mechanics (which metrics, how they are formatted, how a missing INP sample is treated) belong in the framework, while the numbers stay with each consumer, whose runners and hosting differ and therefore whose calibrated maxima differ (WebVitalsCollector.cs:92-97). The defaults are the Core Web Vitals "good" band, so a consumer that measured nothing tighter can take the record as-is. [Rubric §23, Front-End Performance] assesses whether front-end performance carries an enforced budget rather than an occasional audit; [Rubric §12, Performance & Scalability] is the tag the source itself uses for the collector pair. [Rubric §17, DevOps] applies because in both consumer repos these assertions ride the deploy-gating chromium E2E leg.
  • Walkthrough: a positional record with five defaulted parameters, Lcp = 2500, Fcp = 1800, Ttfb = 800, Cls = 0.1, Inp = 500 (WebVitalsCollector.cs:103-108), all milliseconds except the unitless CLS. Two members.
    • The static Describe(label, path, sample) (:118) renders one invariant-culture line, [web-vitals:{label}] path=... LCP=...ms FCP=...ms CLS=... TTFB=...ms INP-sample=...ms, with CLS at three decimals and the rest at zero (:122-124), which is the record a reviewer greps for next to the uploaded JSON artifact.
    • AssertWithinBudget(sample, label, path, writeLine = null) (:137) invokes the optional sink with that line (normally ITestOutputHelper.WriteLine, :141), then asserts LCP, FCP, TTFB, and CLS against their ceilings (:143-146). INP is asserted only when sample.Inp > 0 (:148-151), because no interaction clearing the collector's 16 ms event threshold leaves the sample at 0, and 0 must read as neither a pass-by-absence nor a failure. Failure text comes from the private Message helper, which names the metric, the measured value, the ceiling, and the page path (:154-157).
  • Why it's built this way: keeping the numbers consumer-side while shipping the assert body is what lets ADC and Store hold different calibrated budgets without either repo re-deriving the INP-zero rule or the message format. The 0-INP carve-out is the subtle one, and it is pinned by its own unit test rather than left to a comment (WebVitalsBudgetTests, MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/WebVitals/WebVitalsBudgetTests.cs:59-63).
  • Where it's used: ADC holds one static default instance and takes the framework numbers as-is (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/WebVitalsTests.cs:39, re-asserted at :93); Store constructs a default one per measurement (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/WebVitalsTests.cs:42, :89). The framework's own gallery suite instead constructs one from local constants tuned for the backend-less host (WebVitalsE2ETests, MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/WebVitals/WebVitalsE2ETests.cs:17-19, :29-30), and WebVitalsBudgetTests covers the record's mechanics without starting a browser (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/WebVitals/WebVitalsBudgetTests.cs:12).

E2ETestCollection

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:48 · Level 2 · sealed class

  • What it is: the xUnit [CollectionDefinition] that binds PlaywrightFixture to the named "E2E" collection, so every E2E test class shares the one launched browser.
  • Depends on: xUnit's ICollectionFixture<PlaywrightFixture> and the [CollectionDefinition] attribute (MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:47-48).
  • Concept introduced, the xUnit collection fixture binding. A collection fixture is instantiated once and shared by every test class that opts into the collection by name. This class carries a public const string Name = "E2E" (:50) used both in its own [CollectionDefinition(Name)] and in each test's [Collection(E2ETestCollection.Name)], so the string is declared once and cannot drift. [Rubric §14, Testability] assesses fixture design; a single named constant binding is the robust way to share a fixture.
  • Walkthrough: an otherwise empty class body carrying the collection definition and the Name constant (:47-51). It exists purely as an xUnit marker, and it lives in the same file as the fixture it binds.
  • Where it's used: referenced by E2ETestBase's [Collection(E2ETestCollection.Name)] attribute (MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:8), so every workflow base and every consumer subclass inherits collection membership.
  • Caveats / not-in-source: xUnit collection definitions do not cross assembly boundaries, so each consumer E2E assembly re-declares its own identically named definition over the same fixture type, and says so in its doc comment (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Infrastructure/E2ETestCollection.cs:3-11).

WebVitalsCollector

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:20 · Level 2 · static class

  • What it is: the measurement infrastructure for client-side Core Web Vitals. It installs browser PerformanceObserver scripts before first paint, reads the accumulated values back off a live page, and writes them as a citable JSON artifact.
  • Depends on: Microsoft.Playwright (IPage), System.Text.Json, and System.IO (MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:1-5). It produces WebVitalsSample and WebVitalsArtifact, and pairs with WebVitalsBudget.
  • Concept introduced, in-browser performance measurement with no third-party JS. Rather than shipping an analytics SDK, it injects a small init script that installs PerformanceObservers for LCP, CLS, FCP, and INP, each wrapped in try/catch so an engine lacking an entry type leaves that metric at 0 instead of throwing, and accumulates into window.__vitals (:26-35). The type doc is explicit that this is the client-side analogue of a backend load test, not a cross-engine field measurement: LCP and CLS are Chromium-only, so on Firefox and WebKit those fields stay 0 and budget assertions pass (:9-18). [Rubric §23, Front-End Performance] and [Rubric §12, Performance & Scalability] assess whether user-centric performance is measured; observing the vitals APIs directly, with no network egress, is a self-contained way to do it. The same doc states the class is only the measurement infrastructure, that WebVitalsBudget is the shared assert mechanics, and that consumers own which pages carry a budget and what the numbers are (:16-18).
  • Walkthrough: InstallAsync registers the observers through AddInitScriptAsync so they are active on the next navigation (:40-44). CollectAsync evaluates a script that stamps TTFB from Navigation Timing and returns window.__vitals as JSON, deserialized into a WebVitalsSample (:47-57). WriteArtifactAsync resolves the output directory from WEB_VITALS_OUTPUT_DIR or falls back to artifacts/ under the current directory, creates it, wraps the sample in a WebVitalsArtifact, and writes web-vitals-{label}.json indented (:63-72).
  • Why it's built this way: the observers install before the document's own scripts (through AddInitScript) so early metrics such as FCP are not missed, and the per-observer try/catch is what makes the same code run green on all three engines despite the Chromium-only metrics. The init script is kept as one concatenated string rather than a raw literal to stay clear of the MA0136 analyzer (:22-25).
  • Where it's used: through WebVitalsPageExtensions.MeasureWebVitalsAsync, which is the one call site that sequences install, navigate, collect, write and assert; the framework gallery, ADC and Store budget tests all go through it rather than calling the three methods themselves.

E2ETestBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:9 · Level 3 · abstract class

  • What it is: the shared base every E2E test class derives from. It gives each test a fresh isolated browser context and page off the shared PlaywrightFixture browser, plus the load-bearing auth helpers (login, register, deterministic session cleanup) and the accessibility scan helpers.
  • Depends on: PlaywrightFixture, E2ETestConfiguration, AxeOptions, AuthOutcomeRules, the PageExtensions helpers, xUnit's IAsyncLifetime and TestContext, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:1-4).
  • Concept introduced, the per-test browser context. The fixture launches one browser; this base opens a new IBrowserContext (an isolated cookie and storage jar) per test in InitializeAsync and disposes it in DisposeAsync, so tests cannot leak session state into each other. It is the E2E analogue of the integration-test base's per-test database reset. [Rubric §28, Front-End Testing] assesses realistic, isolated UI tests; [Rubric §21, Accessibility] applies through the scan helpers; [Rubric §14, Testability] through the shared, correctly sequenced auth helpers every workflow reuses. The auth-result handling also touches [Rubric §22, Responsive/Cross-Browser], since the same waits must survive Server-mode and WASM render timing on any engine.
  • Walkthrough: teaching order.
    • Lifecycle. The class carries [Collection(E2ETestCollection.Name)] (:8), the fixture arrives through the constructor, and the current Page is a protected property (:11-18). InitializeAsync creates a context with IgnoreHTTPSErrors and the base URL, sets the default timeout from configuration, optionally starts trace capture with screenshots, snapshots, and sources when TracePath is set, and opens the Page (:20-38). DisposeAsync stops tracing, closes the page, and disposes the context, each step guarded by a null check because a failed InitializeAsync leaves the fields null and the resulting NullReferenceException would otherwise mask the real setup error (:40-62). The private StopTracingAsync (:68-90) carries the per-test trace policy: a plain file path keeps the single-file behavior, while a directory path writes a trace named after the current test only when that test failed (TestContext.Current.TestState?.Result == TestResult.Failed, :79-85), so a full-suite run yields just the failing traces with no overwriting.
    • Auth entry points. LoginAsAdminAsync and LoginAsUserAsync (:92-96) delegate to LoginAsync with the AdminCredentials and UserCredentials pair. LoginAsync (:98-145) first clears any existing session when a sign-out button is visible, removing the auth_access_token and auth_refresh_token localStorage entries and issuing a DELETE /auth/session-cookie fetch, guarded against the context-destroyed race from an in-flight logout (:106-122); it then navigates to /login, fills through FillFieldAsync, clicks, and awaits WaitForAuthResultAsync followed by WaitForInteractiveOrReloadAsync. SignOutAsync (:162-173) clicks the sign-out button and then waits for the browser to actually land on /login, matching against the shared cached LoginUrlPattern regex on a 15 second budget (:175-176); a PlaywrightException whose message carries NS_BINDING_ABORTED repeats the same wait once. RegisterNewUserAsync (:178-211) generates a unique e2e-{id}@test.com email with the fixed password TestPass123!, fills the register form, submits, runs the same two post-auth waits, and returns the created credentials.
    • Post-auth robustness. WaitForInteractiveOrReloadAsync (:224-235) waits for interactivity and, on either a PlaywrightException or a TimeoutException, reloads once and re-waits rather than watching the same stuck boot; the comment records why both exception types are caught (Playwright's TimeoutException derives from System.TimeoutException, not PlaywrightException, so an earlier single catch skipped the retry entirely) and why a reload beats a re-wait (the framework assets are now HTTP-cached, :213-223). WaitForAuthResultAsync (:245-299) starts three signals through Task.WhenAny, leaving the auth page, the logout button appearing, or an error alert appearing (:250-257), observes the two losers so their timeout faults never resurface as unobserved-task exceptions in an unrelated test (:263-265, helper at :303-308), and then classifies the settled state through AuthOutcomeRules.Classify (:267-270). Succeeded returns; the other two outcomes get one grace window (:279), after which ErrorShown throws an InvalidOperationException carrying the alert text (:284-288) and Silent throws one naming the auth path, the total budget and the current URL (:294-298). AuthSucceededWithinGraceAsync (:313-331) implements that window, falling back to the logout-button signal when no navigation occurs.
    • Helpers. NavigateAndWaitAsync (:333-334), the shared static FillFieldAsync delegating to PageExtensions.FillAndVerifyAsync (:341-342), UniqueId (:344), and the two scan helpers. ScanGridAsync (:355-361) waits for a visible data row and for zero [role='progressbar'] elements, then scans with AxeOptions.Wcag21AaExceptMudPagerCombobox; ScanAsync (:365-369) applies the progressbar guard only and scans strictly with Wcag21Aa.
  • Why it's built this way: the auth helpers encode hard-won timing knowledge once (the forceLoad reload, the Server-versus-WASM hydration lag, the cookie-and-localStorage dual session store), so every consumer workflow inherits a deterministic sign-in instead of re-deriving the races. Clearing both token stores is essential: the Blazor Server host is cookie-only, so a localStorage clear alone would leave the next login authenticated as the wrong user (:100-105). Sign-out is a base helper for the same reason: a workflow that clicked the button and then waited for LoadState.Load was waiting on the CURRENT document, whose load event had already fired, so the next goto died with "execution context was destroyed" or "interrupted by another navigation"; the URL wait fixes that once for every caller, and the NS_BINDING_ABORTED retry covers Firefox, which surfaces the abandoned first request when the logout forceLoad is superseded mid-flight by the app's own redirect onto /login (chromium and webkit never raise it, :147-161). The scan split lets grid pages accept the documented pager-combobox exception while every other page stays strict, and the grid wait keys off a data row rather than the loading bar hiding, which would resolve instantly before the transient unnamed progressbar even appears (:346-354).
  • Where it's used: the base class of all eight workflow bases in this unit (AuthorizationTestsBase, LogoutTestsBase, PasswordResetTestsBase, ProfileManagementTestsBase, PseudoLocalizationTestsBase, UserLoginTestsBase, UserPreferencesTestsBase, UserRegistrationTestsBase) and, through them and directly, every E2E test class in the ADC and Store suites (for example ADC's own ProfileManagementTests, which derives from this base rather than the shared profile workflow, MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/ProfileManagementTests.cs:8).

WebVitalsPageExtensions

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/WebVitalsPageExtensions.cs:13 · Level 3 · static class

  • What it is: the one-call Core Web Vitals measurement, a single extension(IPage) member that installs the collector, loads the page, optionally drives one scripted interaction, collects, writes the artifact, and asserts the sample against a budget.
  • Depends on: WebVitalsCollector, WebVitalsBudget, WebVitalsSample, PageExtensions.GotoAndWaitForBlazorAsync, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Infrastructure/WebVitalsPageExtensions.cs:1).
  • Concept introduced, shipping the ORDER, not just the parts. The collector, the sample and the budget are each individually usable, and every consumer used to wire them together in a private MeasureAsync. The step order is the load-bearing part: the observers must be installed BEFORE the navigation or LCP, FCP and TTFB are never recorded for that load, which is exactly the kind of detail a per-repo copy gets wrong once and then carries (doc, :5-12). Lifting the sequence into the package turns an ordering convention into a compiler-checked call. [Rubric §23, Front-End Performance] assesses whether the performance gate is trustworthy; [Rubric §15, Best Practices & Code Quality] applies because three copies collapsed into one; [Rubric §33, Developer Experience] applies because a consumer now writes one line per measured page.
  • Walkthrough: MeasureWebVitalsAsync(label, path, budget, writeLine = null, interactionPlaceholder = null, interactionText = "test") (:32-38) null-checks the page and the budget (:40-41), installs the observers (:43), navigates through GotoAndWaitForBlazorAsync (:44), and, when an interaction placeholder is supplied, clicks and fills that input if it is visible and then waits 300 ms so the event-timing observer records what it just saw (:46-57). It then collects the sample (:59), writes the artifact (:60), calls budget.AssertWithinBudget(...) with the optional output sink (:62), and returns the sample for any further app-specific assertion (:63).
  • Why it's built this way: the interaction is best-effort by contract, an absent or invisible field is skipped, and if no event clears the collector's 16 ms threshold INP stays 0 and its assertion is skipped by the budget (doc, :24-29). That keeps a single helper usable both for load-only pages and for pages with a search box to drive.
  • Where it's used: by every budget test in the workspace. The framework gallery measures three gallery pages against a locally tuned budget (WebVitalsE2ETests, MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/WebVitals/WebVitalsE2ETests.cs:40, :48, :56); ADC funnels four pages through one private wrapper that re-asserts the returned sample at the call site (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/WebVitalsTests.cs:90-93); Store calls it directly for home, catalog (with a search-box interaction) and login (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/WebVitalsTests.cs:42, :52, :62).
  • Caveats / not-in-source: the type doc calls the output a "dated" artifact (:8); the file name WebVitalsCollector actually writes is web-vitals-{label}.json, with no date component (MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:70).

AuthorizationTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Identity · MMCA.Common.Testing.E2E/Workflows/Identity/AuthorizationTestsBase.cs:18 · Level 4 · abstract class

  • What it is: the reusable authorization workflow fitness base, authored once and re-run as a thin subclass per repo. It asserts that anonymous users are redirected off protected paths, that public paths stay reachable, that a registered non-admin can reach an authenticated page, and that a non-admin probing admin routes gets the Forbidden page.
  • Depends on: E2ETestBase, PageExtensions (GotoAndWaitForBlazorAsync, GotoProtectedAsync), AwesomeAssertions, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Workflows/Identity/AuthorizationTestsBase.cs:1-6).
  • Concept introduced, the authored-once workflow fitness base. This is the pattern shared by all eight bases in this unit: the framework owns the assertions and the SSR-versus-client-navigation mechanics, and each consumer supplies only its own route lists through abstract or virtual members, so identical security behavior is verified across repos without copying test bodies (:10-17). [Rubric §11, Security] assesses whether authorization is actually exercised; this base machine-checks both the anonymous-redirect and the authenticated-non-admin-escalation directions. [Rubric §25, Navigation & IA] applies because it pins which routes are public and which are gated.
  • Walkthrough: the subclass supplies ProtectedPaths and PublicPaths (abstract, :26, :29) and optionally AuthenticatedUserPath and AdminPaths (virtual, defaulting to null and an empty list, :35, :44). Four facts follow. AnonymousUser_ProtectedPages_ShouldRedirectToLogin asserts each protected path bounces to /login (:46-58). AnonymousUser_PublicPages_ShouldBeAccessible asserts each public path stays put (:60-72). RegisteredUser_AuthenticatedPage_ShouldBeAccessible registers a non-admin, then client-navigates through GotoProtectedAsync because SSR cannot read the JWT, passing vacuously when no path is declared (:74-93). RegisteredUser_AdminPages_ShouldBeForbidden registers a non-admin, then asserts each admin path renders the shared Forbidden page, matching h1[role='alert'] containing "Access Denied", with the comment noting that role denial is not a redirect so the page content is the only reliable signal (:95-120).
  • Why it's built this way: the two optional members use a no-dynamic-skip convention (an app with no such page simply passes) because the shipped library deliberately does not reference xunit.v3.assert for a declared skip (:77-78, :98-99). The non-empty assertions on ProtectedPaths and PublicPaths (:49-50, :63-64) are non-vacuity guards: a repo that declares no paths fails rather than passing silently.
  • Where it's used: subclassed in both consumer E2E suites with that app's route lists (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/AuthorizationTests.cs:11-21, MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/AuthorizationTests.cs:11-39, whose twelve-entry AdminPaths carries a comment explaining which authenticated-but-not-Organizer routes are deliberately excluded, :21-24); Store's subclass also adds one app-specific fact of its own, an anonymous order-detail deep link that must leak no order content (AuthorizationTests.cs:23-37).

LogoutTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Identity · MMCA.Common.Testing.E2E/Workflows/Identity/LogoutTestsBase.cs:9 · Level 4 · abstract class

  • What it is: the reusable logout workflow base. It verifies that sign-out returns the user to the login screen and that a logged-out user can no longer reach a protected page.
  • Depends on: E2ETestBase, PageExtensions (WaitForBlazorAsync), and Microsoft.Playwright (MMCA.Common.Testing.E2E/Workflows/Identity/LogoutTestsBase.cs:1-5).
  • Concept: the authored-once workflow base taught in AuthorizationTestsBase. [Rubric §11, Security] assesses session teardown; this base guards that logout genuinely revokes access rather than merely returning to a login screen visually.
  • Walkthrough: two facts. Logout_ShouldRedirectToLoginPage registers, confirms the sign-out button is visible, clicks it, waits for the load state, and asserts the sign-in button appears (:16-29). Logout_ShouldPreventAccessToProtectedPages registers, waits for interactivity (because RegisterNewUserAsync can return with the button visible before JS interop is ready, :37-40), then clicks sign-out inside RunAndWaitForResponseAsync so it blocks until the best-effort DELETE /auth/session-cookie response arrives (:47-51), confirms the sign-in button is visible (:54-55), and then re-requests /profile up to six times until the server redirects to /login (:64-72), falling back to a clear URL assertion if it never does (:75).
  • Why it's built this way: waiting for the cookie-clear response is the fix for a real full-speed race. At speed the test otherwise reaches /profile before the DELETE finishes, so the HttpOnly cookie is still present and SSR re-authenticates. The bounded re-request loop converges deterministically where any slowdown (slow-mo, or even trace capture) would have hidden the race entirely (:42-46, :57-63).
  • Where it's used: subclassed in both consumer E2E suites (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/LogoutTests.cs:5, MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/LogoutTests.cs:5).

PasswordResetTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Identity · MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:17 · Level 4 · abstract class

  • What it is: the reusable password-recovery workflow base covering the shared /forgot-password and /reset-password pages: the entry point is reachable from the login screen, an unknown address gets the same confirmation as a known one, the reset form's client-side validation blocks an empty submit, and both pages are accessibility-clean.
  • Depends on: E2ETestBase, LoginPage, ForgotPasswordPage, ResetPasswordPage, PageExtensions, AxeOptions, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:1-6).
  • Concept introduced, drawing the E2E boundary around what a browser can honestly observe. The type doc states outright that the real token round-trip is deliberately not exercised here: the token only reaches the user by email, so redeeming one is an app-side integration-test concern, and what E2E owns is the reachability of the flow, the anti-enumeration confirmation, client-side validation, and WCAG 2.1 AA conformance of both pages (:10-16). That is a scope decision worth internalizing: a browser test that cannot reach the mailbox should assert the contract it can see rather than fake the one it cannot. [Rubric §11, Security] assesses whether the recovery flow leaks account existence; the unknown-address fact is the machine check on ADR-091's anti-enumeration rule. [Rubric §24, Forms/Validation/UX Safety] covers the empty-submit validation, [Rubric §21, Accessibility] the two scans, and [Rubric §25, Navigation & IA] the entry-point fact, since a locked-out user has no other way in.
  • Walkthrough: five facts, no abstract members, so a consumer subclass is a single line.
    • LoginPage_ForgotPasswordLink_NavigatesToForgotPasswordPage (:24-41) opens the LoginPage, asserts the "Forgot your password?" link is visible at all (the comment notes a user locked out of their account has no other entry point, :33), clicks it, and asserts the URL ends at /forgot-password (:40).
    • ForgotPassword_WithUnknownEmail_ShowsTheSameConfirmation (:43-58) submits an unknown-{UniqueId()}@test.com address that certainly has no account, then asserts the positive path exactly: the success confirmation appears (:55), the URL stays on /forgot-password (:56), and the back-to-login link is visible (:57). There is no error alert and no navigation to distinguish it from a real address, which is the whole point.
    • ResetPassword_WithEmptyForm_ShowsClientValidationErrors (:60-76) clicks submit on an untouched form; DataAnnotations block OnValidSubmit so nothing is sent, and the fact asserts the field-level texts "Email is required" and "Reset token is required" plus staying on /reset-password (:73-75).
    • ForgotPasswordPage_ShouldHaveNoAccessibilityViolations (:78-89) and ResetPasswordPage_ShouldHaveNoAccessibilityViolations (:91-100) each load their page and scan with AxeOptions.Wcag21Aa.
  • Why it's built this way: the validation fact asserts the field-level text rather than a page-level alert, and the source gives the reason: those messages are present in both render modes (Server prerender and WebAssembly) while a page-level alert is not, the same reasoning as the mismatched-password registration test (:70-72, and UserRegistrationTestsBase.Register_WithMismatchedPasswords_ShouldShowError). The a11y scans pass the strict Wcag21Aa preset explicitly rather than running an unscoped RunAxe, keeping this workflow on the same documented target as the rest of Identity (:85-88).
  • Where it's used: subclassed with no additions in both consumer E2E suites (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/PasswordResetTests.cs:5, MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/PasswordResetTests.cs:5). The two Page Objects it drives are additionally exercised against the framework's own backend-less gallery host by ForgotPasswordPageE2ETests and ResetPasswordPageE2ETests (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Auth/ForgotPasswordPageE2ETests.cs:9, MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Auth/ResetPasswordPageE2ETests.cs:9).

ProfileManagementTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Identity · MMCA.Common.Testing.E2E/Workflows/Identity/ProfileManagementTestsBase.cs:10 · Level 4 · abstract class

  • What it is: the reusable profile workflow base. It verifies that name, address, and password changes persist, that the profile page loads pre-filled with the registered data, an opt-in email-change journey, and that the profile page is accessibility-clean.
  • Depends on: E2ETestBase, ProfilePage, PageExtensions, AxeOptions, AwesomeAssertions, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Workflows/Identity/ProfileManagementTestsBase.cs:1-6).
  • Concept: the authored-once workflow base taught in AuthorizationTestsBase, here driving a ProfilePage. [Rubric §24, Forms/Validation/UX Safety] assesses whether edit-and-persist journeys work end to end; [Rubric §21, Accessibility] applies through the a11y fact.
  • Walkthrough: one virtual switch, ProfileSupportsEmailChange, off by default (:23). Six facts follow. ChangeName_ShouldUpdateProfileName clears and fills both name fields, saves, re-navigates, and asserts the values persisted (:26-53); ChangeAddress_ShouldUpdateProfileAddress does the same for the five address fields and asserts on line 1 (:55-78). Both use Playwright's plain FillAsync rather than the re-hydration-safe helper, since the profile page is reached by client-side navigation on an already interactive runtime. ChangePassword_WithValidCurrentPassword_ShouldSucceed fills the three password fields through the shared FillFieldAsync, waits for the "Password changed successfully." snackbar, then signs out through the base's SignOutAsync and logs back in with the new password (:80-109, sign-out at :105). ChangeEmail_ShouldUpdateEmail is opt-in and returns immediately unless ProfileSupportsEmailChange is overridden true (:111-145). ProfilePage_ShouldLoadWithUserData asserts the form is pre-filled from registration (:147-165). ProfilePage_ShouldHaveNoAccessibilityViolations scans with AxeOptions.Wcag21Aa (:167-180).
  • Why it's built this way: the email-change fact is a declared opt-in rather than a DOM probe because the previous probing version passed vacuously when the field was absent, reporting coverage for a journey the app does not offer; overriding the flag makes a missing field fail loud (:17-22, :127-129). The sign-out-then-login step delegates to E2ETestBase.SignOutAsync instead of clicking and waiting inline, so this workflow inherits both halves of the fix (the /login URL wait rather than LoadState.Load, and the Firefox NS_BINDING_ABORTED retry) from one place; the retained comment records why the load-state wait raced the in-flight logout navigation (:99-104).
  • Where it's used: subclassed only by Store, with no additions and no override of ProfileSupportsEmailChange (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/ProfileManagementTests.cs:5), so the email-change fact passes without exercising a journey Store offers. ADC does not subclass this base: its profile page supports the avatar photo, password change, and account deletion but no name or address editing, so MMCA.ADC.E2E.Tests writes its own ProfileManagementTests directly on E2ETestBase with a password-change fact mirroring this one, a claims-page fact, and an avatar upload-replace-remove round trip that builds its two PNGs from base64 constants so the test needs no fixture file on disk (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/ProfileManagementTests.cs:8, :26, :56, :70, constants at :13-16).

PseudoLocalizationTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Globalization · MMCA.Common.Testing.E2E/Workflows/Globalization/PseudoLocalizationTestsBase.cs:52 · Level 4 · abstract class

  • What it is: the reusable pseudo-localization fitness gate. Each page a consumer declares is loaded under the qps-Ploc pseudo culture and asserted three ways (a visible bracket sentinel renders, the page's en-US probe does NOT render in plain English, and the page does not overflow horizontally), then re-loaded under the default culture to prove the sentinel never ships to a real locale.
  • Depends on: E2ETestBase, PseudoLocalizedPage, PageExtensions.GotoAndWaitForBlazorAsync, AwesomeAssertions, and System.Globalization (MMCA.Common.Testing.E2E/Workflows/Globalization/PseudoLocalizationTestsBase.cs:1-5).
  • Concept introduced, pseudo-localization as an executable gate. A pseudo locale transforms every localized string (here by bracketing it and adding a combining accent to each letter) without needing a translator. Three properties fall out of that, and this base asserts all three: the round trip from resource file through IStringLocalizer and the framework's PseudoStringLocalizer to markup is genuinely happening (the sentinel is visible), no string bypassed the pipeline (the plain en-US probe is absent), and the layout tolerates the roughly 40% text expansion a real translation brings (no horizontal document overflow), which is the rubric's layout-tolerance criterion (doc, :26-41). [Rubric §27, i18n] assesses whether localization is verified end to end rather than assumed; [Rubric §22, Responsive/Cross-Browser] covers the overflow half. The transform itself is taught with PseudoStringLocalizer; the supported-culture list including the pseudo locale is SupportedCultures.
  • Walkthrough: six virtual knobs and two facts.
    • The knobs. ScannedPages is the one abstract member (:60). PseudoLocale defaults to "qps-Ploc", restated here rather than referenced because the shipped fixture library deliberately does not reference MMCA.Common.Shared (:62-66). Sentinel defaults to "[!!" (:69), OverflowTolerancePx to 1 CSS pixel so sub-pixel rounding is absorbed without admitting a genuine sideways scrollbar (:71-75), ProbeVisibleTextOnly to false so the probe is searched in the whole document including attributes such as an input placeholder (:77-83), AssertProbePresentUnderDefaultCulture to true (:85-90), and Timeout to 15_000 ms (:93).
    • PseudoLocale_RendersSentinel_WithoutEnUsLeak_AndDoesNotOverflowHorizontally (:95-132) first guards non-vacuity on ScannedPages (:98-99), then per page activates the culture by loading /culture/set?culture=...&redirectUri=... (:106-107), settles (:109), asserts at least one VISIBLE sentinel (:112-113), asserts the probed text does not contain the en-US probe (:117-120), and evaluates scrollWidth - clientWidth on the scrolling element against the tolerance (:124-130).
    • DefaultCulture_DoesNotLeakPseudoSentinel (:134-157) re-loads each page with no culture cookie (each test gets a fresh browser context from E2ETestBase), asserts the page content contains no sentinel (:144-147), and, when AssertProbePresentUnderDefaultCulture holds, asserts the probe IS present so the leak check cannot pass vacuously (:149-155).
    • Two private helpers: ProbedTextAsync selects document content or body innerText per the flag (:164-167), and SettleAsync waits for the optional settle selector and then for zero [role='progressbar'] elements (:174-184).
  • Why it's built this way: activation goes through the culture COOKIE (GET /culture/set, the same endpoint the app's culture switcher uses) rather than a query-string culture provider, and the remarks explain why: a Blazor Server circuit takes its culture from the request that starts the circuit, which carries cookies but not the original page's query string, so a query-string-only activation would pseudo-localize the prerender and then revert on hydration (:42-51). qps-Ploc is allowlisted in Development only, which is what an Aspire-launched E2E stack runs, so the gate needs no host change; public pages are the deliberate subjects because skipping login keeps the gate robust. The mechanics reference ADR-027.
  • Where it's used: subclassed once per consumer, each contributing only its page list. ADC takes every default unchanged and declares three public pages (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/PseudoLocalizationTests.cs:29, list at :39-44); Store declares three and overrides ProbeVisibleTextOnly to true so an aria-label or serialized prerender payload carrying the plain form cannot fail the gate (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/PseudoLocalizationTests.cs:25, list at :41-46, override at :53). The framework's own gallery has a separate prerender-level equivalent in PseudoLocalizationE2ETests.

UserLoginTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Identity · MMCA.Common.Testing.E2E/Workflows/Identity/UserLoginTestsBase.cs:10 · Level 4 · abstract class

  • What it is: the reusable login workflow base. Valid credentials reach the home page and show the authenticated app bar, invalid credentials show an error and stay on /login, the create-account link navigates to /register, and the login page is accessibility-clean.
  • Depends on: E2ETestBase, LoginPage, PageExtensions, AxeOptions, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Workflows/Identity/UserLoginTestsBase.cs:1-6).
  • Concept: the authored-once workflow base taught in AuthorizationTestsBase, here driving a LoginPage. [Rubric §28, Front-End Testing] and [Rubric §11, Security] apply.
  • Walkthrough: four facts. Login_WithValidCredentials_ShouldNavigateToHomePage registers (which auto-logs in), signs out through the base's SignOutAsync (:27), logs back in, and asserts the URL left /login, the sign-out button is visible, and the "Sign In" link is not (:18-41). Login_WithInvalidPassword_ShouldShowError drives LoginPage.LoginAsync with a nonexistent account and asserts the error alert appears and the URL stays on /login (:43-57). Login_NavigateToCreateAccount_ShouldGoToRegisterPage clicks the create-account link and asserts /register (:59-71). LoginPage_ShouldHaveNoAccessibilityViolations scans with AxeOptions.Wcag21Aa (:73-84).
  • Why it's built this way: the sign-out step is a single call into E2ETestBase.SignOutAsync rather than an inline click plus wait, and the comment above it (:23-26) keeps the reason on the call site: waiting for the logout forceLoad's /login URL rather than LoadState.Load is the fix for the sign-out-then-login race, where the current document's load event had already fired so the wait returned immediately and the pre-login cleanup evaluate died with "execution context was destroyed".
  • Where it's used: subclassed in both consumer E2E suites (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/UserLoginTests.cs:5, MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/UserLoginTests.cs:5).

UserPreferencesTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Preferences · MMCA.Common.Testing.E2E/Workflows/Preferences/UserPreferencesTestsBase.cs:21 · Level 4 · abstract class

  • What it is: the reusable culture-switch and theme-toggle workflow base. It verifies that switching to Spanish localizes and persists, that toggling dark mode applies and persists, and that both controls are reachable on a mobile viewport.
  • Depends on: E2ETestBase, PageExtensions (GotoAndWaitForBlazorAsync), AwesomeAssertions, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Workflows/Preferences/UserPreferencesTestsBase.cs:1-5).
  • Concept introduced, the self-contained preferences fitness base. Unlike the identity bases, this one needs no app-specific overrides: the probe page is the shared /login, the probe string is the localized "Welcome Back" / "Bienvenido de nuevo", and persistence is the anonymous cookie pair (.AspNetCore.Culture plus mmca_theme), all owned by Common UI in every app (:9-20). [Rubric §27, i18n] assesses whether localization actually switches and persists; [Rubric §20, Design System & Theming] covers the theme toggle; [Rubric §22, Responsive/Cross-Browser] covers the mobile-parity fact. The source doc cites ADR-027 and ADR-028 for the localization and theming mechanics.
  • Walkthrough: a dark-background probe script that accepts either the raw hex #1a2027 or its rgba(26,32,39,1) form, whitespace-stripped (:25-28), plus desktop and mobile action-cluster locators scoped by container (.appbar-icon-actions and .toprow-actions, :37-39) to disambiguate the duplicated NavMenu controls. Three facts follow. CultureSwitch_ToSpanish_ShouldLocalizeAndPersist opens the Language menu and clicks the Spanish item (its label carries the accented spelling in source) located as .mud-popover-open .mud-menu-item (the popover carries that class only once Blazor interactivity attached, and the items are not .mud-list-item and carry no menuitem role), then asserts the Spanish probe survives a fresh full page load (:41-64). ThemeToggle_ToDark_ShouldApplyAndPersist clicks the title-stable toggle (its aria-label flips with state, its title does not), asserts the palette variable flipped through AssertDarkPaletteAsync, asserts localStorage holds mmca_theme set to dark, then reloads and re-asserts (:66-85). MobileViewport_CultureAndTheme_ShouldBeReachable sets a 390x844 viewport and asserts the controls come from NavMenu's top row, then actually toggles the theme there rather than only checking that it rendered (:87-101). The private AssertDarkPaletteAsync polls the probe script with a 15 s ceiling (:103-107).
  • Why it's built this way: the mobile fact pins the v1.103.0 regression where the controls lived only in the app bar, hidden below 1024px; the selectors mirror the gallery's own MobileTopRowE2ETests exactly because the MudMenu activator exposes no literal aria-label attribute, so raw CSS attribute selectors do not match it (:14-19).
  • Where it's used: subclassed with no additions in both consumer E2E suites (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Preferences/UserPreferencesTests.cs:10, MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Preferences/UserPreferencesTests.cs:10).

UserRegistrationTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Identity · MMCA.Common.Testing.E2E/Workflows/Identity/UserRegistrationTestsBase.cs:10 · Level 4 · abstract class

  • What it is: the reusable registration workflow base. Valid data navigates to the home page and logs the user in, mismatched passwords show the inline validation message and stay on /register, a duplicate email shows an error, and the register page is accessibility-clean.
  • Depends on: E2ETestBase, RegisterPage, PageExtensions (FillAndVerifyAsync), AxeOptions, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Workflows/Identity/UserRegistrationTestsBase.cs:1-6).
  • Concept: the authored-once workflow base taught in AuthorizationTestsBase, here driving a RegisterPage. [Rubric §24, Forms/Validation/UX Safety] assesses client-side validation and duplicate handling; [Rubric §21, Accessibility] applies through the a11y fact.
  • Walkthrough: four facts. Register_WithValidData_ShouldNavigateToHomePage registers a unique user through the Page Object and asserts the URL left /register and the sign-out button is visible (:17-34). Register_WithMismatchedPasswords_ShouldShowError fills every field through FillAndVerifyAsync, submits exactly once, and asserts the inline "Passwords do not match" validation text plus staying on /register (:36-63). Register_WithDuplicateEmail_ShouldShowError registers, then re-registers the same email and asserts the error alert (:65-79). RegisterPage_ShouldHaveNoAccessibilityViolations scans with AxeOptions.Wcag21Aa (:81-92).
  • Why it's built this way: the mismatched-passwords fact submits once and asserts the field-level validation text rather than re-clicking, because the [Compare] validation fires OnInvalidSubmit and a re-clicking helper would make the message flicker out from under the wait; the text is asserted (not the alert element) because it renders in both the Server-mode and WebAssembly paths, while the page-level alert appears only on the Server-mode prerender path (:43-47, :56-60).
  • Where it's used: subclassed in both consumer E2E suites (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/UserRegistrationTests.cs:5, MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/UserRegistrationTests.cs:5).

BunitInteractionExtensions

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/BunitInteractionExtensions.cs:12 · Level 0 · static class

  • What it is: a small set of intention-revealing helpers over bUnit's element API so a component test can say "click the button labelled Save" instead of hand-rolling a DOM query, deliberately preferring accessible text over brittle CSS-path selectors (MMCA.Common.Testing.UI/Infrastructure/BunitInteractionExtensions.cs:7-11).
  • Depends on: no first-party types. Three externals: AngleSharp's IElement (the DOM node type bUnit returns, :1), bUnit's IRenderedComponent<TComponent> (:2), and Microsoft.AspNetCore.Components.IComponent as the generic constraint (:3). See primer §3 for the external stack.
  • Concept introduced, extension(T) blocks used for something other than DI. Everywhere else in this codebase the C# preview extension-member syntax registers services (primer §4); here the same feature bolts query and interaction members onto a third-party generic type. The declaration is extension<TComponent>(IRenderedComponent<TComponent> cut) where TComponent : IComponent (:14-15), so the extension block is itself generic over the component type and the members inside read as instance methods on the rendered component (cut.ClickButtonByText("Save")). [Rubric §28, Front-End Testing] assesses whether UI behavior is exercised through realistic automated checks; querying by the text a user actually sees is what keeps a component test asserting on behavior rather than on markup structure. [Rubric §15, Best Practices & Code Quality] assesses how well the code resists churn; a CSS-path selector breaks on any wrapper change, while a text query survives it.
  • Walkthrough: three members, all inside the single extension block.
    • FindButtonByText(string text) (:18-25) materializes every <button> via cut.FindAll("button") (:20), then takes the first whose TextContent contains text under StringComparison.OrdinalIgnoreCase (:21). On no match it throws an InvalidOperationException whose message enumerates the trimmed text of every button present, pipe-separated (:22-24), so a failing test names the buttons it could see instead of reporting a bare null.
    • ClickButtonByText(string text) (:28-29) is the action form: it delegates to FindButtonByText and calls bUnit's Click(), which raises the component's onclick through the renderer.
    • HasText(string text) (:32-33) is the read form, a case-insensitive Contains over cut.Markup. It answers on the whole rendered markup, not only visible text, which is the trade-off for having no dependency beyond the rendered string.
  • Why it's built this way: the diagnostic message is the point. bUnit's own Find(selector) throws with the selector, which tells you nothing when the button simply rendered with different copy; listing the actual buttons turns a red test into a readable one on the first run.
  • Where it's used: by the shared UI's own page tests, for example MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Notifications/NotificationSendTests.cs:73,99,246 (the send and cancel buttons of the notification composer) and MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Notifications/NotificationInboxTests.cs, and across the ADC and Store component suites that derive from BunitComponentTestBase.
  • Caveats / not-in-source: HasText matches attribute values and element names too, since it searches raw markup; a test asserting on user-visible copy that happens to collide with a CSS class name would pass for the wrong reason.

CapturedRequest

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:158 · Level 0 · sealed record

  • What it is: the immutable snapshot of one HTTP request a UI service sent, recorded by CapturingHttpMessageHandler so a test can assert on what went out, not only on what came back (MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:154-157).
  • Depends on: BCL only, System.Net.Http.HttpMethod, System.Uri, and IReadOnlyDictionary<string, string>. No first-party types.
  • Concept introduced, capture-then-assert on the outbound side. A canned-response fake proves the service handles a given payload; it says nothing about whether the service built the right URL, attached the bearer token, sent the right If-Match, or serialized the right body. Recording every request as a value object makes those assertions possible without a server. [Rubric §14, Testability] assesses how much of a component's contract can be verified in isolation; capturing the request turns the client's outbound contract (route, query string, headers, body) into something a unit test can pin. [Rubric §9, API and Contract Design] applies indirectly: these captures are where a UI service's assumed route and header shape is asserted against the API's actual one.
  • Walkthrough: six positional members plus one init member.
    • The positional six (:159-164) are filled by the handler's CaptureAsync (:82-88). Method is the verb; Uri is the full request URI and is nullable because HttpRequestMessage.RequestUri is; Path is uri?.AbsolutePath with an empty-string fallback (:85); PathAndQuery keeps the query string that Path drops (:86), which is what a paging or filter assertion needs; Authorization is the header rendered back to a string, or null when absent (:87); Body is the content read as text, left null when the request had no content (:75-79).
    • Headers (:179-180) is an IReadOnlyDictionary<string, string> over an OrdinalIgnoreCase comparer, defaulting to an empty dictionary and set through an object initializer by CaptureAsync (:89-91). It flattens request headers and content headers into one lookup, which is what makes headers the service layer sets itself assertable.
    • The XML doc records why Headers is a non-positional init member rather than a seventh positional parameter (:172-177): adding one would rewrite the record's primary constructor and Deconstruct, breaking every consumer that constructs or deconstructs a captured request. Authorization therefore stays where it is even though the same value is also reachable through Headers.
  • Why it's built this way: being a record, structural equality and a readable ToString() come for free, which is what makes a failing assertion legible. The deliberate refusal to move Authorization into Headers is a source-compatibility decision written into the type rather than left implicit.
  • Where it's used: exposed as the ordered IReadOnlyList<CapturedRequest> on CapturingHttpMessageHandler.Requests (:42) and filtered by its RequestsFor(method, absolutePath) (:61-65); consumed by every UI HTTP-service test in the three repos, for example the optimistic-concurrency assertion captured.Headers["If-Match"].Should().Be("\"v7\"") and the content-header pair in MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/CapturingHttpMessageHandlerTests.cs:208-210,226,229.

ErrorSummaryExtensions

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/ErrorSummaryExtensions.cs:10 · Level 0 · static class

  • What it is: a reader that pulls the shared ErrorSummary component's current messages out of a rendered component under test, so a form test asserts on the validation text a user would see rather than on raw markup (MMCA.Common.Testing.UI/Infrastructure/ErrorSummaryExtensions.cs:6-9).
  • Depends on: no first-party types at compile time, but it is coupled by contract to MMCA.Common.UI's ErrorSummary.razor, which renders the marker class on its title (MMCA.Common/Source/Presentation/MMCA.Common.UI/Components/ErrorSummary.razor:13). Externals: bUnit's IRenderedComponent<TComponent> and AngleSharp's element query API, plus Microsoft.AspNetCore.Components.IComponent as the constraint (:1-2).
  • Concept introduced, testing a component that renders two different shapes for the same state. ErrorSummary renders several messages as a <ul> of <li> items but a single message as plain text inside the alert (ErrorSummary.razor:15-29). A test that only queried li would read an empty summary exactly when one rule is broken, which is the most common form-validation case, and the failure would look like the component not rendering at all. This helper reads both shapes and strips the alert's title so the caller gets one list either way; the remark on the member says so directly (ErrorSummaryExtensions.cs:22-26). [Rubric §24, Forms/Validation/UX Safety] assesses whether validation feedback is coherent and verified; this is the assertion primitive that makes the form suites able to check it. [Rubric §28, Front-End Testing] is the capability it serves. [Rubric §21, Accessibility] applies obliquely: the two rendering shapes exist because several independent failures must be announced as several list items, and the reader has to understand that choice to see through it.
  • Walkthrough: one constant and one extension member.
    • ErrorSummaryTitleClass (:13) is the public const "mmca-error-summary-title", the class the component puts on its title and therefore the marker that distinguishes an error summary's MudAlert from any other alert on the page. Exposing it as a constant lets a consumer's own assertion use the same literal.
    • The extension<TComponent>(IRenderedComponent<TComponent> cut) block (:15-16) mirrors the shape BunitInteractionExtensions introduces.
    • ErrorSummaryMessages() (:27-47) guards the rendered component (:29), then finds the first .mud-alert that contains an element carrying the title class (:31-32). No such alert means no summary is showing, and it returns an empty list (:33-36). It then queries li descendants: when any exist, it returns their trimmed text one entry per rule (:38-42). Otherwise it takes the single-message shape, reads the title's text and removes that substring from the alert's whole TextContent ordinally, trims, and returns either an empty list or that one message (:44-46).
  • Why it's built this way: matching the alert by the title class rather than by position keeps the reader working on a page that renders other MudAlert instances (an info banner, an offline notice). Stripping the title by string removal rather than by DOM surgery keeps the helper read-only against the rendered tree.
  • Where it's used: covered directly by MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Components/Forms/ErrorSummaryExtensionsTests.cs:22,33,43,54, which pins all four branches (no summary, single-message shape, list shape, and an alert without the title class), and used by the ADC create-form suites, for example MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Events/EventCreateTests.cs:55, .../Question/QuestionCreateTests.cs:57, and .../Activity/ActivityCreateTests.cs:91.
  • Caveats / not-in-source: the single-message branch removes the title text by ordinal string replacement, so a message whose text happens to contain the title string would have that fragment removed as well.

FreshApiClientFactory

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:73 · Level 0 · sealed class

  • What it is: an IHttpClientFactory test double that returns a brand-new HttpClient on every CreateClient call, whatever name is asked for (in practice "APIClient"), all wired to one shared handler with a fixed base address (MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:66-72).
  • Depends on: System.Net.Http.IHttpClientFactory and HttpMessageHandler (BCL). Constructed with a CapturingHttpMessageHandler in practice, by UiHttpServiceHarness (:47) and by HttpTestDoubles.ClientFactory (MMCA.Common.Testing.UI/Infrastructure/HttpTestDoubles.cs:23-24).
  • Concept introduced, a fake whose lifetime semantics are load-bearing. The shared UI services acquire a client per call and dispose it afterwards, so a factory double that caches one instance would hand the second call a disposed client and fail with an ObjectDisposedException that looks like a product bug. The XML doc states exactly that ("A fresh instance per call is load-bearing", :69-71). [Rubric §14, Testability] assesses whether test doubles reproduce the real collaborator's contract; matching IHttpClientFactory's ownership semantics, not just its signature, is the difference between a double that works and one that misleads.
  • Walkthrough: a primary-constructor class taking (HttpMessageHandler handler, Uri baseAddress) (:73) with one member. CreateClient(string name) (:79-80) ignores the name and returns new HttpClient(handler, disposeHandler: false) { BaseAddress = baseAddress }. The disposeHandler: false flag is the second load-bearing detail: it lets the handler outlive each client, so the recorded CapturedRequest list survives across calls and the harness alone owns handler disposal (:63).
  • Why it's built this way: ignoring the client name keeps the double usable from any repo without knowing the named-client key the service under test resolves, while the shared handler keeps one capture log per test.
  • Where it's used: exposed as UiHttpServiceHarness.ClientFactory (:47,58) and returned from HttpTestDoubles.ClientFactory (HttpTestDoubles.cs:23-24); the fresh-instance contract and the shared base address are asserted directly in MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/SharedHttpTestDoublesTests.cs:22,27-28,38.

IsAuthenticatedAuthorizationService

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:176 · Level 0 · private sealed nested class

  • What it is: the permissive-but-real IAuthorizationService that BunitComponentTestBase registers, so any policy succeeds for an authenticated principal and fails for an anonymous one.
  • Depends on: Microsoft.AspNetCore.Authorization (IAuthorizationService, AuthorizationResult, IAuthorizationRequirement, MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:3) and BCL ClaimsPrincipal. Nested privately inside its only consumer.
  • Concept introduced, collapsing policy evaluation to the authentication bit. A component test cares whether the authorized branch of <AuthorizeView> renders, not whether a named policy's requirement handlers are wired; re-hosting the real policy provider in every test project would couple UI tests to each app's policy catalogue. This double answers by identity alone, which keeps the render assertion honest about the branch while staying agnostic about policy names. [Rubric §11, Security] assesses how authorization is expressed and enforced; note the deliberate inversion here, this double grants every policy, so it proves a page renders when signed in and never proves a policy actually denies. [Rubric §14, Testability] assesses how cheaply a component renders in isolation; one class replaces a whole policy graph.
  • Walkthrough: two members, both IAuthorizationService overloads. The requirements overload (:178-182) returns AuthorizationResult.Success() when user.Identity?.IsAuthenticated == true and AuthorizationResult.Failed() otherwise, wrapped in Task.FromResult so no async machinery is allocated. The policy-name overload (:184-185) delegates to the first with an empty requirement collection, so a component gated on a named policy takes the same identity-only decision.
  • Where it's used: registered as a singleton IAuthorizationService in the BunitComponentTestBase constructor (:57), immediately after AddAuthorizationCore() (:56), so it wins over the core default for every derived component test.
  • Caveats / not-in-source: role-based <AuthorizeView Roles="..."> is evaluated by the Blazor component itself against the principal's role claims (which TestPrincipal supplies), not by this service, so role gating still discriminates in a component test while policy gating does not.

MarkupSnapshotResult

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/MarkupSnapshot.cs:104 · Level 0 · readonly record struct

  • What it is: the two-field outcome of a MarkupSnapshot comparison, IsMatch plus a human-readable Message (MMCA.Common.Testing.UI/Infrastructure/MarkupSnapshot.cs:101-104).
  • Depends on: nothing. That is the entire design point.
  • Concept introduced, returning a result instead of asserting. A shipped test-infrastructure package that called an assertion API would drag an assertion library into every consumer's dependency graph and pin them to its version. Returning a value and letting the caller assert keeps the package dependency-free, which the class doc states explicitly ("kept dependency-free so the shipped package pulls in no assertion library", :11-12). This mirrors the Result pattern the product code uses for expected failures, applied here to a test helper. [Rubric §32, Dependency and Supply-Chain] assesses how carefully the dependency surface of shipped packages is managed; a readonly record struct with two fields adds nothing to a consumer's transitive closure. [Rubric §28, Front-End Testing] is the capability this serves.
  • Walkthrough: a positional readonly record struct with bool IsMatch (true when the markup matched the committed baseline or was just refreshed) and string Message (:102-104). readonly plus struct means no allocation per comparison; the record shape gives value equality and a printable form. Callers use it as result.IsMatch.Should().BeTrue(result.Message), so the diff text that MarkupSnapshot.BuildDiffMessage produced becomes the assertion failure message.
  • Where it's used: returned from every branch of MarkupSnapshot.Match (:45,51-53,58-59) and asserted by the golden-markup regressions in MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Components/PrimitivesSnapshotTests.cs:21,31,41,51,61 and by the ADC component snapshot suites.

MudProviderHandles

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:157 · Level 0 · protected sealed nested record

  • What it is: the three handles returned by BunitComponentTestBase.RenderMudProviders(), one per rendered MudBlazor provider, so a test can query the popover, dialog, or snackbar markup after triggering it (MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:156).
  • Depends on: bUnit's IRenderedComponent<T> and MudBlazor's MudPopoverProvider, MudDialogProvider, and MudSnackbarProvider (:157-160, imports at :2,11-12).
  • Concept: the provider-root problem, taught with BunitComponentTestBase. MudBlazor renders overlays into provider components that normally live in the app layout; in a component test there is no layout, so a dialog or snackbar has nowhere to go. Rendering the providers as separate roots gives them somewhere, and this record is how the test keeps a reference to each root to query it afterwards. [Rubric §20, Design System and Theming] assesses how the shared component library is adopted and exercised; overlay behavior is only testable once the design system's provider contract is honored in the test host.
  • Walkthrough: a positional record with Popover, Dialog, and Snackbar (:157-160), constructed once in RenderMudProviders() after the three Render<T>() calls (:150-153). Dialog is the handle a test queries for MudMessageBox confirm buttons; Snackbar is the one it queries for toast text.
  • Where it's used: returned by RenderMudProviders() (:148-154). Most callers discard the return value and only need the providers rendered, for example MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.UI.Tests/Pages/ShoppingCart/ShoppingCartListTests.cs:74,89,104,118; a test that must click inside the dialog keeps it, as in MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Pages/Users/UserListTests.cs:114,122-124, which waits for the confirm button to appear in providers.Dialog and clicks it.

MutableAuthenticationStateProvider

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:162 · Level 0 · private sealed nested class

  • What it is: the AuthenticationStateProvider BunitComponentTestBase registers, holding one principal that the test can swap at any point, with change notification to listeners.
  • Depends on: Microsoft.AspNetCore.Components.Authorization (AuthenticationStateProvider, AuthenticationState, :5) and BCL ClaimsPrincipal.
  • Concept introduced, the two ways a Blazor page learns who the user is. Some components read the cascading AuthenticationState value; others inject AuthenticationStateProvider and call GetAuthenticationStateAsync() themselves. A hardcoded-anonymous provider serves neither well once a test needs the signed-in branch, so the base supplies a mutable one and drives both routes from the same principal. The class remark says exactly this: the provider is mutable because it is "a superset of a hardcoded-anonymous one" (:24-27). [Rubric §19, State Management] assesses how shared client state is owned and propagated; auth state is the canonical cascading state, and this is the test-side owner of it.
  • Walkthrough: a primary-constructor class taking the initial principal (:162), storing it in a mutable _principal field (:164). SetPrincipal(ClaimsPrincipal) (:166-170) assigns the field and then calls the base NotifyAuthenticationStateChanged with a fresh AuthenticationState, so an already-rendered <AuthorizeView> re-evaluates rather than keeping its first answer. GetAuthenticationStateAsync() (:172-173) returns Task.FromResult(new AuthenticationState(_principal)), so no async state machine is allocated per call.
  • Where it's used: instantiated as the base's _authProvider field seeded with Anonymous (:42), registered as a singleton AuthenticationStateProvider (:58), and mutated by SetUser (:121) and by every RenderAs call before rendering (:135).

Route

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:139 · Level 0 · private sealed nested record

  • What it is: one registered canned response inside CapturingHttpMessageHandler: an HTTP method, an absolute path, a status code, an optional JSON body, and the method that turns all four into a fresh HttpResponseMessage.
  • Depends on: BCL HttpMethod, HttpStatusCode, HttpResponseMessage, StringContent, and System.Text.Encoding (MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:1-2). No first-party types.
  • Concept introduced, why the canned response is a recipe rather than an object. HttpContent is consumed when read, so handing the same HttpResponseMessage to two calls fails the second one, and the shared UI services run behind a retry pipeline that can legitimately send the same request twice. Storing the body as a string and rebuilding the response per request removes that whole class of false failure; the handler's own doc calls this out ("Responses are built fresh per request so a Polly retry pipeline never reuses a consumed HttpContent", :14-16). [Rubric §29, Resilience and Business Continuity] assesses how retry and recovery behavior is handled; a fake that cannot be retried would make the retry pipeline itself untestable.
  • Walkthrough: a positional record (HttpMethod Method, string Path, HttpStatusCode StatusCode, string? JsonBody) (:139) with one method. ToResponse() (:141-150) news up an HttpResponseMessage(StatusCode) and, only when JsonBody is not null, attaches a StringContent(JsonBody, Encoding.UTF8, "application/json") (:144-147), so a body-less status (204, 404) round-trips as a genuinely empty response rather than as an empty JSON document.
  • Where it's used: appended by SetResponse (:57) and selected by the handler's Respond using LastOrDefault on method plus case-insensitive path (:124-125), which is what makes "last registration wins" true and lets a test override an earlier canned route mid-test.
  • Caveats / not-in-source: the match ignores the query string entirely (only AbsolutePath is compared, :121-125), so two routes that differ only by query parameters cannot be registered separately. Query assertions go through CapturedRequest.PathAndQuery instead.

CapturingHttpMessageHandler

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:19 · Level 1 · sealed class

  • What it is: the canned-response, request-recording HttpMessageHandler that lets an HTTP-backed UI service be unit tested with no server: it answers every request from registered routes or a responder delegate, and records what was sent (MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:7-18).
  • Depends on: its two companions Route (private nested) and CapturedRequest. Externals: System.Net.Http.HttpMessageHandler (the BCL extension point), System.Text.Json for body serialization, and System.Net/System.Text (:1-3).
  • Concept introduced, faking at the transport boundary instead of the service boundary. The alternative would be mocking the UI service's own interface, which proves nothing about the service. Subclassing HttpMessageHandler puts the fake one layer lower, so the service under test runs its real URL construction, its real serialization, its real header setting, its real status-code handling, and its real error mapping; only the wire is simulated. [Rubric §14, Testability] assesses how much real behavior a unit test covers; this is the difference between testing the service and testing a mock of it. [Rubric §9, API and Contract Design] applies because the registered routes are a written-down expectation of the API's shape, and [Rubric §12, Performance and Scalability] indirectly: the whole tier runs in-process with no sockets, which is what keeps the UI test tier in the fast unit run.
  • Walkthrough: state, then the two configuration modes, then the send path.
    • Static WebJson (:21) is a single JsonSerializerOptions(JsonSerializerDefaults.Web) instance, matching what the WebAPI actually emits (camelCase, case-insensitive reads), so a body serialized here deserializes in the service exactly as a real response would.
    • _respond (:23) is the optional responder delegate; _routes (:24) and _requests (:25) are the registration and capture lists.
    • The parameterless constructor (:31-33) selects route-registration mode; the delegate constructor (:39) selects responder mode. The two are not exclusive: routes always win, and the delegate is the fallback (:119-137).
    • Requests (:42) exposes the captures in order as an IReadOnlyList<CapturedRequest>.
    • SetResponse(method, absolutePath, statusCode, body = null) (:49-58) normalizes the body through a switch: null stays null (empty body), a string is treated as raw JSON and passed through untouched, and anything else is serialized with WebJson (:51-56). It then appends a Route (:57).
    • RequestsFor(method, absolutePath) (:61-65) filters the captures by verb and case-insensitive path via a collection expression, the assertion helper most tests use.
    • SendAsync (:67-71) is the override: capture first, then respond. CaptureAsync (:73-92) reads the request content to a string when present (:75-79) and builds the CapturedRequest from the URI, absolute path, path-and-query, and the Authorization header rendered to a string (:81-88), then sets its Headers from CaptureHeaders (:89-91). Reading the body here, before responding, is what makes the body assertable at all, since the content stream is consumed by the read.
    • CaptureHeaders (:99-117) builds one OrdinalIgnoreCase dictionary, copying every request header (:103-106) and then, when a body is present, every content header (:108-114), comma-joining multi-valued entries. Its doc gives the reason both halves are needed: If-Match lives on the request while Content-Type lives on the content, and a caller asserting either should not have to know which (:94-98).
    • Respond (:119-137) resolves the response in strict precedence: the last matching registered route (:121-129), else the responder delegate (:131-134), else 404 Not Found with an empty body (:136). The doc explains the 404 default as deliberate, mirroring the WebAPI's not-found behavior so an incidental call (a token refresh, say) does not have to be set up in every test (:12-14).
  • Why it's built this way: the two configuration modes serve two different test shapes. A service test that exercises one endpoint many ways wants the delegate ("answer everything this way"); a test that walks a multi-call flow wants named routes with last-registration-wins overrides. Supporting both in one handler avoids two parallel fakes drifting apart.
  • Where it's used: owned by UiHttpServiceHarness (MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:24,39,43-45), constructed directly by tests that wire the pieces themselves (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/SharedHttpTestDoublesTests.cs:21,35), and covered in its own right by MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/CapturingHttpMessageHandlerTests.cs:25,37,53,65.
  • Caveats / not-in-source: _requests and _routes are plain List<T> with no synchronization (:24-25), so a test issuing genuinely concurrent requests through one handler is outside what this type guarantees.

MarkupSnapshot

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/MarkupSnapshot.cs:21 · Level 1 · static partial class

  • What it is: a dependency-free golden-markup regression helper. It takes a component's rendered markup, normalizes the parts that change per render, compares it to a committed baseline .html file next to the calling test, and returns the outcome for the caller to assert on (MMCA.Common.Testing.UI/Infrastructure/MarkupSnapshot.cs:6-20).
  • Depends on: MarkupSnapshotResult as its return type. Externals: System.Text.RegularExpressions with source-generated regexes and System.Runtime.CompilerServices.CallerFilePathAttribute (:1-2), plus BCL file I/O.
  • Concept introduced, snapshot testing without screenshots. A pixel screenshot baseline is OS-dependent and needs per-platform golden management; normalized markup is deterministic and identical on every CI runner, which is why this comparison can live in the fast in-solution unit tier rather than in a browser job. The class doc states both halves of that trade (:13-18). Two policies make it safe to rely on: a baseline is refreshed only when UPDATE_SNAPSHOTS=1 is set, and a missing baseline is written but still reported as a non-match, so a regression can never pass silently on an absent snapshot. [Rubric §28, Front-End Testing] assesses whether UI regressions are caught automatically; this is the structural-regression half of that (axe scans in the E2E tier are the accessibility half). [Rubric §20, Design System and Theming] assesses how the shared component library is governed; snapshotting the shared primitives is what makes an unintended change to a reused component fail somebody's build.
  • Walkthrough:
    • Match(markup, snapshotName, [CallerFilePath] callerFilePath = "") (:31-60) is the whole public surface. It guards both string arguments (:33-34), normalizes the markup (:36), and locates the baseline as Snapshots/{snapshotName}.html in the directory of the calling test file, creating the folder if needed (:37-39). [CallerFilePath] is the mechanism: the compiler bakes the caller's source path in, so baselines live beside the tests that own them with no configuration.
    • Three outcomes follow. With UPDATE_SNAPSHOTS=1 (compared ordinally, :41) it writes the file and returns a match with a "refreshed" message (:42-46). With no existing baseline it writes the file and returns a non-match telling you to review and commit it (:48-54). Otherwise it reads the baseline, normalizes its line endings and trims (:56), and returns match or a built diff (:57-59).
    • Normalize(markup) (:64-70) does the work that makes the comparison stable: CRLF to LF and trim (:66), then replace both GUID forms with the literal {guid} (:67-68), then right-trim every line (:69). MudBlazor injects fresh GUIDs into element ids and ARIA associations on every render, so without this step no snapshot would ever match twice.
    • BuildDiffMessage (:72-92) walks both line arrays to the first difference and reports the line number with the expected and actual text plus the UPDATE_SNAPSHOTS=1 instruction (:84-86); if no line differs it reports a length mismatch (:90-91).
    • GuidRegex (:94-95) and Hex32Regex (:97-98) are [GeneratedRegex] partial properties, which is why the class is partial: the patterns compile at build time rather than being interpreted per call.
  • Why it's built this way: writing the baseline on first run but still failing the test is the deliberate part. Writing and passing would let a regression bless itself on any machine where the file happened to be missing; failing but not writing would make creating a new snapshot a manual chore. Writing plus failing gives the file for free and forces a human to review and commit it.
  • Where it's used: by MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Components/PrimitivesSnapshotTests.cs:21,31,41,51,61, which snapshots the shared UI primitives (EmptyState, PageHeader, PageErrorState, PageLoadingState) against baselines committed under that project's Snapshots/ folder, and by the per-module ADC component snapshot suites, for example MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Components/ComponentsSnapshotTests.cs:69,87,124,143, MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.UI.Tests/Components/ComponentsSnapshotTests.cs:122,135,147,160,175,197, and MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Components/ComponentsSnapshotTests.cs:33,43.
  • Caveats / not-in-source: the normalizer replaces any bare 32-character hex token, so a legitimate markup value of that shape (a content hash in an asset URL, for example) would also collapse to {guid} and stop being compared. Some ADC suites wrap the markup in a repo-local Stable(...) helper before calling Match, so the shipped normalizer is not always the only pass applied.

StubTokenStorageService

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/StubTokenStorageService.cs:13 · Level 1 · sealed class

  • What it is: the canned ITokenStorageService for UI HTTP-service tests. It returns fixed tokens with no platform storage behind it, lets the access-token read be swapped for a throwing delegate, and mutates its values on set and clear so login and logout flows are assertable (MMCA.Common.Testing.UI/Infrastructure/StubTokenStorageService.cs:5-12).
  • Depends on: ITokenStorageService from MMCA.Common.UI.Services.Auth (:1), the contract the real per-head storage implementations satisfy. That reference is one of the reasons the Testing.UI package carries a ProjectReference to MMCA.Common.UI, as its comment records (MMCA.Common.Testing.UI/MMCA.Common.Testing.UI.csproj:28-30).
  • Concept introduced, a stub with a failure switch. Most stubs only model the happy path, which leaves the interesting branch (what the service does when storage is unreachable) untested. Blazor has a real version of that branch: during prerender, JS interop is unavailable, so a browser-storage-backed implementation throws. Exposing the read as a replaceable delegate lets a test reproduce that exact failure without a browser. [Rubric §26, Front-End Security] assesses how tokens are stored and attached; the stub is what lets the bearer-attachment path be asserted at all. [Rubric §29, Resilience and Business Continuity] assesses how failure paths are handled; the swappable delegate is the extension point for testing them.
  • Walkthrough:
    • The constructor takes accessToken = "test-token" and refreshToken = "test-refresh-token" (:18-23), assigns both, and sets AccessTokenProvider to a closure returning the property AccessToken (:22), not the constructor argument, so later mutations are visible through the default provider.
    • AccessToken and RefreshToken (:26,29) are mutable auto-properties, readable by a test as post-condition assertions.
    • AccessTokenProvider (:36) is the swap point: replace it with a delegate that throws InvalidOperationException and GetAccessTokenAsync reproduces the prerender storage failure (:31-35).
    • The four interface members follow: GetAccessTokenAsync() invokes the provider (:39), GetRefreshTokenAsync() returns the property (:42), SetTokensAsync(access, refresh) assigns both and completes (:45-50), and ClearTokensAsync() nulls both (:53-58).
  • Why it's built this way: routing the read through a delegate while set and clear mutate plain properties keeps the common case zero-ceremony (construct it and go) and the failure case one assignment away, with no separate throwing subclass to keep in sync.
  • Where it's used: constructed by UiHttpServiceHarness and exposed as its TokenStorage property (MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:48,61), returned from HttpTestDoubles.TokenStorage (MMCA.Common.Testing.UI/Infrastructure/HttpTestDoubles.cs:28-29), and covered directly by MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/StubTokenStorageServiceTests.cs:19,28,38,51,65,76.

TestPrincipal

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/TestPrincipal.cs:7 · Level 1 · static class

  • What it is: the factory for the ClaimsPrincipal instances component tests render as, with the claim shape the shared pages actually read (MMCA.Common.Testing.UI/Infrastructure/TestPrincipal.cs:6).
  • Depends on: AuthClaimTypes from MMCA.Common.Shared.Auth (:2), for the sub claim name. Externals: BCL System.Security.Claims (:1).
  • Concept introduced, the authenticated-identity trap. A ClaimsIdentity is authenticated only when it was constructed with an authentication type; an identity built from claims alone reports IsAuthenticated == false and every <AuthorizeView> renders its anonymous branch, which is a confusing way for a test to fail. This factory always passes one (authenticationType: "TestAuth", :31), and the base class's Anonymous principal deliberately omits it (BunitComponentTestBase.cs:40), so the two states are produced by the same mechanism from opposite ends. [Rubric §11, Security] assesses how identity and claims are modelled; encoding the app-wide identifier claim names in one shared factory keeps every UI test honest to the claims the product code reads.
  • Concept, the dual-written user identifier. The identifier is written twice, under AuthClaimTypes.Subject (which is "sub", MMCA.Common/Source/Core/MMCA.Common.Shared/Auth/AuthClaimTypes.cs:27) and under ClaimTypes.NameIdentifier (TestPrincipal.cs:27-28), because a real principal reaches a page under either name: a token read directly carries the raw sub the token service emits, while the JWT bearer handler maps it onto NameIdentifier. A page reading through ClaimsPrincipalExtensions.FindUserIdValue resolves both, and writing both here also keeps a page that still reads a single claim type working under this double; the remark says exactly that (:14-21).
  • Walkthrough: two members.
    • AuthenticatedUser(string userId = "1", string name = "Test User", params string[] roles) (:22-32) builds a claim list with ClaimTypes.Name, the sub claim, and ClaimTypes.NameIdentifier (:24-29), appends one ClaimTypes.Role claim per supplied role (:30), and returns a ClaimsPrincipal over a ClaimsIdentity carrying "TestAuth" (:31).
    • Organizer(string userId = "1") (:35-36) is a named convenience over the first: an authenticated "Organizer User" carrying the Organizer role, the role ADC's organizer-only surfaces gate on.
  • Why it's built this way: role gating in Blazor is claim-matching, not policy evaluation (the test IsAuthenticatedAuthorizationService grants every policy), so getting the role claims right on the principal is what makes an authorized-branch assertion meaningful.
  • Where it's used: passed to BunitComponentTestBase.RenderAs across all three repos, for example MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.UI.Tests/Pages/Order/OrderListTests.cs:92,126 (a plain authenticated render and an Admin-role render of the same page), MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Layout/NavMenuTests.cs:48 (a named user), and MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Auth/SessionsTests.cs:80.
  • Caveats / not-in-source: Organizer is ADC vocabulary living in a framework package; nothing constrains a consumer to that role name, and Store's tests pass their own roles through AuthenticatedUser(roles: ...) instead.

BunitComponentTestBase

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:37 · Level 2 · public abstract class

  • What it is: the one shared base class for bUnit component tests across all three repos. It stands up MudBlazor services and the vendor-neutral UI facades over them, puts JS interop in loose mode, wires real-but-permissive auth doubles, registers localization and a clock, and adds the helpers derived tests actually call (RenderUnderTest, RenderAs, RenderMudProviders, SetUser, ConfigureDataGridListPageHost) (MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:16-36).

  • Depends on: its three nested types MutableAuthenticationStateProvider, IsAuthenticatedAuthorizationService, and MudProviderHandles, plus TestPrincipal as the intended source of authenticated principals. It also registers first-party UI services: the AddCommonUiFacades() pair from MMCA.Common.UI (:53, defined at MMCA.Common/Source/Presentation/MMCA.Common.UI/DependencyInjection.cs:162) and the two list-page state services ListPageStateService and ListPageQueryStateService (:105-106). Externals: bUnit's BunitContext base, JSRuntimeMode, AddBunitPersistentComponentState, and SetRendererInfo (:2), MudBlazor's AddMudServices, the three providers, and IBrowserViewportService (:11-12), Moq for the inert viewport double (:10), and Microsoft.Extensions.DependencyInjection plus its TryAdd extensions (:6-7).

  • Concept introduced, the component test harness. A Blazor component under test needs a renderer, a service provider, and whatever ambient services its markup injects; assembling that per test project is how three repos end up with three subtly different harnesses. This base assembles it once, in a shipped package, so a component that renders in Common's tests renders identically in ADC's and Store's. Several decisions carry the weight:

    • The vendor-neutral facades ship with the harness (:53): every migrated page and the shared Result helpers inject IToastService and the confirm-dialog facade. AddCommonUiFacades() wraps the MudBlazor services registered on the line above, so it belongs immediately after AddMudServices(); without it a consumer's component test fails to resolve IToastService and each repo re-registers the same pair in its own base. The registration uses TryAdd internally, so a test wanting a recording double registers one afterwards and last registration wins (:48-52).
    • Loose JS interop (:55): MudBlazor components probe JS during render (measurements, popover positioning). In bUnit's default strict mode every unplanned call throws, so a test would have to pre-plan interop it does not care about; loose mode returns defaults instead.
    • Real authorization, fake decision (:56-58): AddAuthorizationCore() registers the genuine Blazor authorization plumbing, then the two nested doubles replace only the decision inputs. <AuthorizeView> therefore runs its real code path.
    • Localization by default (:60-64): the comment ties this to ADR-027; components inject IStringLocalizer<T>, so registering the open generic once lets every component test render localized markup against the neutral resources in the component's own assembly with no per-test setup. [Rubric §27, i18n] assesses whether localization is systematic rather than incidental; making it the harness default is what keeps localized pages testable everywhere.
    • A clock, added with TryAdd (:66-70): TimeProvider.System backs the staleness measurements the notification bell's poll interval and the navigation-refresh window depend on. TryAddSingleton means a test that drives time deterministically registers a fake provider of its own and wins. The comment also notes that AddLogging above already brought in the options infrastructure, so IOptions<T> of an unconfigured settings class resolves to its defaults.
    • Anonymous by default (:40,42): the seed principal has no authentication type, so tests opt in to the authorized branch explicitly rather than inheriting it.

    [Rubric §28, Front-End Testing] assesses whether UI behavior is verified automatically; this base is the entry point for the entire component tier. [Rubric §14, Testability] assesses how cheaply a unit renders in isolation. [Rubric §15, Best Practices & Code Quality]: the class remark documents that the bUnit v2 symbols (BunitContext, Render<T>) are confined to this file, so a hypothetical downgrade to bUnit v1 changes this one class and no derived test, because derived tests only ever call RenderUnderTest/RenderAs (:28-35).

  • Walkthrough: fields first, then the constructor, then the helpers.

    • Anonymous (:40) is a static readonly ClaimsPrincipal over a bare ClaimsIdentity, unauthenticated precisely because no authentication type is supplied. _authProvider (:42) is the MutableAuthenticationStateProvider seeded with it.
    • The constructor (:44-71) runs the registrations described above, in order: AddMudServices() (:46), AddCommonUiFacades() (:53), loose JSInterop.Mode (:55), AddAuthorizationCore() (:56), the singleton IsAuthenticatedAuthorizationService (:57), the singleton provider instance (:58), AddLogging() and AddLocalization() (:63-64), and TryAddSingleton(TimeProvider.System) (:70).
    • ConfigureDataGridListPageHost(isInteractive = true, rendererName = "Server", stubViewport = true) (:100-118) is the one helper whose call ordering is load-bearing, and the reason it exists as a single method. It registers the two list-page state services with TryAddScoped (:105-106), substitutes an inert Mock.Of<IBrowserViewportService>() when stubViewport is true (:108-111), calls AddBunitPersistentComponentState() because the list-page base persists grid state across the prerender/interactive boundary (:113-114), and finally calls SetRendererInfo(new RendererInfo(rendererName, isInteractive)) (:116-117). The doc spells out the trap: SetRendererInfo builds and freezes the bUnit service provider, so any registration made after it is silently ignored and the page resolves the framework default instead of the test's double; fifteen call sites across the repos hand-rolled this block and each had to remember that rule (:77-83). Two parameters encode real branches: isInteractive because list pages bound their prerender fetches on RendererInfo.IsInteractive, so a non-interactive renderer means no data ever loads (:93-97), and stubViewport because in bUnit no browser answers MudBlazor's breakpoint subscription, so the real service would leave the card-versus-grid choice up to timing (:84-91). The viewport double is registered with a plain Add rather than TryAdd on purpose: AddMudServices already registered the real implementation and last registration wins. [Rubric §22, Responsive/Cross-Browser] assesses whether responsive behavior is deterministic and verified; pinning the viewport is what makes the mobile-versus-desktop branch of a list page assertable at all.
    • SetUser(ClaimsPrincipal) (:121) forwards to _authProvider.SetPrincipal, changing the injected provider's answer and notifying listeners without rendering a new root, which is how a mid-test sign-in or sign-out is simulated.
    • RenderUnderTest<TComponent>(parameters) (:124-127) is the anonymous-render entry point and simply calls RenderAs(Anonymous, parameters).
    • RenderAs<TComponent>(principal, parameters) (:130-141) is the one that matters: it sets the provider's principal (:135) and then renders with a cascading Task<AuthenticationState> value added ahead of the caller's parameters (:138-139). Driving both routes from one principal is what makes the cascading consumers and the injecting consumers agree.
    • RenderMudProviders() (:148-154) renders MudPopoverProvider, MudDialogProvider, and MudSnackbarProvider as separate roots and returns the MudProviderHandles triple. The doc is explicit that it must be called before the component under test (:143-147).
  • Why it's built this way: registering the auth doubles as concrete instances rather than mocks means the authorization pipeline under test is the framework's own; only the two decision inputs are substituted. Moq is a deliberate dependency for exactly one thing, the viewport double, because a hand-written stub would have to track every member MudBlazor adds to IBrowserViewportService (MMCA.Common.Testing.UI/MMCA.Common.Testing.UI.csproj:17-21). And keeping the version-specific bUnit symbols in one file is a blast-radius decision recorded in the class remark rather than left to be rediscovered.

  • Where it's used: subclassed by a thin repo-local BunitTestBase in each consumer, which adds only that repo's extra registrations: MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/BunitTestBase.cs:17 (theme service, culture applier, capability fallbacks, public-link builder) and MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/BunitTestBase.cs:19 (the ADR-042 device-capability defaults, the public-link builder, the public session schedule service, and an empty IConfiguration), plus the equivalent bases in ADC's Identity and Engagement suites and in Store's Sales, Catalog, and Identity UI test projects. ConfigureDataGridListPageHost is called from every list-page suite, for example MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Common/DataGridListPageBaseTests.cs:35, MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.UI.Tests/Pages/ShoppingCart/ShoppingCartListTests.cs:37, MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Sessions/SessionListEventFilterTests.cs:40 (stubViewport: false), and MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Common/EventFilteredListPagePrerenderTests.cs:41 (isInteractive: false, the prerender branch). The base's own registration contract is guarded by MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/BunitComponentTestBaseFacadeTests.cs:18, which derives from the shipped base rather than the repo-local one precisely so the facade registrations cannot be papered over by a consumer.

  • Caveats / not-in-source: the package does reference the shared UI: MMCA.Common.Testing.UI/MMCA.Common.Testing.UI.csproj:30 has a ProjectReference to MMCA.Common.UI, and the base now uses it for both the facades and the list-page state services. The harness is therefore not UI-agnostic, and a consumer cannot take the bUnit base without also taking MMCA.Common.UI.

UiHttpServiceHarness

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:12 · Level 2 · sealed class (IDisposable)

  • What it is: the one-line setup for a UI HTTP-service test. It owns the disposable plumbing every such test needs (the capturing handler, a fresh-client-per-call factory, and a token storage stub) so the test constructs one object and hands its parts to the service under test (MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:3-11).
  • Depends on: CapturingHttpMessageHandler, FreshApiClientFactory, and StubTokenStorageService, all first-party in this package. Externals: IHttpClientFactory and IDisposable (BCL).
  • Concept introduced, the aggregate fixture over the three-piece boundary. Assembling the same three collaborators by hand in each repo is exactly how the fresh-client rule (see FreshApiClientFactory) gets forgotten in one repo and not another. Collecting them behind one constructor makes the correct wiring the default, and gives one Dispose for the one thing that actually needs it. [Rubric §14, Testability] assesses how cheap a correct test setup is; two lines of setup is the practical bar this clears. [Rubric §15, Best Practices & Code Quality]: with the wiring shipped in a package, a fix to the plumbing reaches ADC, Store, and Common in one version bump rather than three edits.
  • Walkthrough:
    • DefaultBaseAddress (:15) is a static readonly Uri of https://gateway.test/, applied to every created client so services can build relative URIs the way they do against a real gateway.
    • Two public constructors mirror the handler's two modes and both funnel into one private constructor. The route-registration one (:23-26) takes accessToken = "test-token" and an optional base address and builds a route-registration handler; the responder one (:35-41) takes the delegate and builds a responder-mode handler.
    • The private constructor (:43-49) is where the wiring lives: store the handler, resolve the base address against DefaultBaseAddress (:46), build the FreshApiClientFactory over that handler and address (:47), and build the StubTokenStorageService with the canned token (:48).
    • Four read-only properties expose the parts: Handler (:52) for SetResponse registration and request assertions, BaseAddress (:55), ClientFactory (:58) typed as IHttpClientFactory to hand to the service, and TokenStorage (:61) typed concretely so a test can reach AccessTokenProvider.
    • Dispose() (:63) disposes only the handler. The clients are the caller's to dispose, which is consistent with disposeHandler: false on each created client, so the capture log outlives them.
  • Why it's built this way: passing accessToken: null produces an anonymous harness in one argument, which is how the "no bearer header when signed out" case is tested without a second fixture type. And keeping the two public constructors as thin forwarders means the mode choice is visible at the call site while the wiring exists once.
  • Where it's used: the default setup for the HTTP-backed UI service tests in all three repos. MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/UiHttpServiceHarnessTests.cs:19,33,45,48,56,67-68,78 covers the harness itself, including both modes, the default and custom base addresses, and the with-token versus anonymous pair.

HttpTestDoubles

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/HttpTestDoubles.cs:12 · Level 3 · static class

  • What it is: the a-la-carte counterpart to UiHttpServiceHarness: standalone factory helpers for tests that wire the pieces individually, plus the canned-response builders both styles share (MMCA.Common.Testing.UI/Infrastructure/HttpTestDoubles.cs:7-11).
  • Depends on: UiHttpServiceHarness (for the shared default base address), FreshApiClientFactory, StubTokenStorageService, and ITokenStorageService as the returned contract (:3). Externals: System.Net.Http.Json.JsonContent and System.Net.HttpStatusCode (:1-2).
  • Concept introduced, canned responses that match the real API's failure shape. The response builders are not generic conveniences: ProblemResponse emits the { title, detail } body the WebAPI actually returns for a domain failure, so the UI-side error mapping under test sees the shape it will see in production (:40-51). A fake that returned a bare 400 would let the mapping pass while being wrong. Its doc names the consumer on the other side, ProblemDetailsResultReader, and records what it does with this shape: the plain-ProblemDetails body becomes a single error coded "Http.{status}" typed from the status code, and a test that needs the individual error codes preserved sends an errors array through JsonResponse<T> instead (:41-46). [Rubric §9, API and Contract Design] assesses whether error contracts are consistent and honored on both sides; this helper is where the UI tier's assumption about the API's error body is written down. [Rubric §29, Resilience, Reliability & Business Continuity] applies because that ProblemDetails shape is produced by shared middleware rather than by any one endpoint.
  • Walkthrough: five members, all static.
    • BaseAddress (:15) is aliased to UiHttpServiceHarness.DefaultBaseAddress, so both wiring styles share one origin and a test can compare across them.
    • ClientFactory(handler, baseAddress = null) (:23-24) returns a FreshApiClientFactory over the given handler, defaulting the address to BaseAddress.
    • TokenStorage(accessToken = "test-token") (:28-29) returns a StubTokenStorageService typed as the interface; pass null for an anonymous client.
    • JsonResponse<T>(payload, statusCode = OK) (:33-34) builds a response whose content is JsonContent.Create(payload), which serializes with web defaults, matching what the WebAPI sends.
    • EmptyResponse(statusCode = NoContent) (:37-38) builds a body-less response, the 204 shape a command endpoint returns.
    • ProblemResponse(detail, title = "Domain Exception", statusCode = BadRequest) (:47-51) delegates to JsonResponse with an anonymous { title, detail } object.
  • Why it's built this way: the harness is the right default, but a test that needs two handlers, or a handler shared with a non-HTTP collaborator, would have to fight it. Keeping the same primitives available individually means neither style forks its own copies, and the response builders stay identical across both.
  • Where it's used: by the responder-delegate style of service test across the consumer UI suites, and covered directly by MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/SharedHttpTestDoublesTests.cs:22,27-28,38,47-49,56,67,75-76,86,97, which pins the shared base address, the fresh-client contract, the three token-storage cases, and all three response builders including the custom-status ProblemDetails form.
  • Caveats / not-in-source: the responses these builders return are single-use HttpResponseMessage instances. Handing one to a responder delegate that returns the same instance twice would fail on a retry, which is why the delegate is invoked per request (see Route) and why these helpers are normally called inside the delegate rather than before it.

GalleryFakeAuthenticationHandler

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:19 · Level 0 · class (sealed, internal)

  • What it is: a cookie-toggled fake ASP.NET Core authentication handler for the backend-less gallery. A request carrying gallery_auth=1 authenticates as a fixed "Gallery Visitor" principal; every other request stays anonymous (MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:28-40).
  • Depends on: Microsoft.AspNetCore.Authentication.AuthenticationHandler<AuthenticationSchemeOptions> (the abstract base), the base constructor triple IOptionsMonitor<AuthenticationSchemeOptions> / ILoggerFactory / System.Text.Encodings.Web.UrlEncoder (MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:19-23), and BCL System.Security.Claims. No first-party dependencies.
  • Concept introduced, the backend-less gallery stub pattern and its one non-inert member. The gallery host (GalleryHost) renders the real MMCA.Common.UI components with no live API behind them, so every consumer-supplied extension point the shared UI expects is replaced by a benign stub. This type is the exception that proves the rule: it is not inert, because the shared notification and sessions pages carry a real [Authorize]. MapRazorComponents surfaces that attribute as endpoint metadata, and the authorization middleware then needs a genuine authentication scheme registered (without one it throws) and a genuine authenticated principal (without one the pages redirect to /login instead of rendering for the scan). The doc comment records exactly that reasoning (MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:8-18). Rather than removing the guard for testability, the gallery supplies a real scheme whose only decision input is a cookie. [Rubric §28, Front-End Testing] assesses whether the UI has real-browser render and accessibility coverage; toggling sign-in per test with a cookie is what lets one host scan both the anonymous chrome (/login, /register, /components, /grid) and the signed-in guarded pages. [Rubric §11, Security] assesses how authentication is implemented; note the deliberate inversion here, the handler trusts an unsigned cookie value, acceptable only because this assembly is unpackaged test infrastructure (the doc comment closes with "never copy into a real host", MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:16-17).
  • Walkthrough: two internal constants pin the contract shared with the host and the tests, SchemeName = "GalleryFake" (:25) and CookieName = "gallery_auth" (:26). HandleAuthenticateAsync() (:28) is the single override. It short-circuits first: when Request.Cookies["gallery_auth"] is not exactly "1" it returns AuthenticateResult.NoResult() (:30-33), which means "this scheme has no opinion", leaving the request anonymous rather than failing it. Otherwise it builds a ClaimsIdentity with one ClaimTypes.Name claim of "Gallery Visitor" and, critically, passes SchemeName as the authentication type (:35-37): supplying an authentication type is what makes Identity.IsAuthenticated true. It wraps that principal in an AuthenticationTicket and returns AuthenticateResult.Success (:38-39). Everything is synchronous through Task.FromResult, so no I/O occurs.
  • Why it's built this way: keeping the guard real and faking only the credential means the E2E scan exercises the same authorization pipeline the deployed hosts run, so an accidental loss of [Authorize] on the shared notification or sessions pages cannot be papered over by a permissive test host.
  • Where it's used: registered as the default (and only) authentication scheme by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:70-73), followed by AddAuthorization() (MMCA.Common.UI.Gallery/GalleryHost.cs:74) and the UseAuthentication() / UseAuthorization() middleware pair (MMCA.Common.UI.Gallery/GalleryHost.cs:113-114). The cookie is seeded on the Playwright browser context by NotificationPagesE2ETests.SeedSignedInCookieAsync (MMCA.Common.UI.E2E.Tests/NotificationPagesE2ETests.cs:91-100) before each of its five guarded-page scans (:23, :38, :54, :67, :80), by SessionsPageE2ETests for the devices page (MMCA.Common.UI.E2E.Tests/Auth/SessionsPageE2ETests.cs:23, :39-40), and by MobileTopRowE2ETests for its signed-in top-row check (MMCA.Common.UI.E2E.Tests/Layout/MobileTopRowE2ETests.cs:93-97).

SampleGridRow

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Pages · MMCA.Common.UI.Gallery/Pages/SampleGridRow.cs:9 · Level 0 · record (sealed, public)

  • What it is: the row DTO behind the virtualized-grid gallery page. It is deliberately plain, because the point of /grid is the windowing behaviour of DataGridListPageBase<TDto>, not the shape of the data (MMCA.Common.UI.Gallery/Pages/SampleGridRow.cs:5-8).
  • Depends on: nothing first-party. Five BCL positional members only.
  • Concept introduced: cross-reference the DTO and manual-mapping conventions taught in 00-primer.md. What is worth noting here is the absence of the usual machinery: no identifier-type alias, no validator, no mapper, no audit fields. A test-fixture DTO exists to give the component under test something to bind, so it carries exactly the column types the grid needs to exercise, one integer, two strings, a DateTime, and a decimal. [Rubric §14, Testability] assesses how cheaply a component can be driven in isolation; a five-member positional record is the smallest possible stand-in for a real paged API contract.
  • Walkthrough: one line. public sealed record SampleGridRow(int Id, string Name, string Category, DateTime CreatedOn, decimal Amount) (:9). The member set is chosen to cover the four PropertyColumn formatting paths the page binds: a plain integer, ordinal strings, a DateTime rendered with Format="yyyy-MM-dd", and a decimal rendered with Format="F2" (MMCA.Common.UI.Gallery/Pages/GridGallery.razor:28-32). It is public rather than internal (unlike every stub in this unit) because it is the generic argument of the page's base class, @inherits DataGridListPageBase<SampleGridRow> (MMCA.Common.UI.Gallery/Pages/GridGallery.razor:4).
  • Where it's used: the element type of SampleGridData.All (MMCA.Common.UI.Gallery/Pages/SampleGridRow.cs:25), the T of the page's MudDataGrid (MMCA.Common.UI.Gallery/Pages/GridGallery.razor:17), and, through the page, the payload GridPageE2ETests asserts against by naming the exact rendered value "Row 0001" (MMCA.Common.UI.E2E.Tests/Layout/GridPageE2ETests.cs:39, :55, :80).

NullTokenRefresher

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/NullTokenRefresher.cs:9 · Level 1 · class (sealed, internal)

  • What it is: an ITokenRefresher that never has a session to refresh. The gallery has no API to refresh against; the stub exists only so the DI graph stays complete (MMCA.Common.UI.Gallery/Stubs/NullTokenRefresher.cs:5-8).
  • Depends on: ITokenRefresher from MMCA.Common.UI.Services.Auth. Nothing else.
  • Concept introduced: the purest instance of the stub pattern introduced in GalleryFakeAuthenticationHandler, collapsing an outbound extension point to a constant. [Rubric §14, Testability] assesses how cheaply a component renders in isolation; reducing token refresh to a constant is what keeps the scan from ever reaching a token endpoint.
  • Walkthrough: one member. AcquireAccessTokenAsync(CancellationToken = default) (:11-12) is an expression body returning Task.FromResult<string?>(null), which is the interface's own documented "no valid session exists" answer (MMCA.Common.UI/Services/Auth/Tokens/ITokenRefresher.cs:15-20), so any caller takes its null-token path.
  • Where it's used: registered scoped by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:62) in the pre-AddUIShared stub block. In the shared UI the only constructor taking an ITokenRefresher is AuthUIService (MMCA.Common.UI/Services/Auth/AuthUIService.cs:37-40), which the gallery replaces with NoOpAuthUIService, so this registration is graph-completeness insurance rather than a live collaborator.

NullTokenStorageService

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/NullTokenStorageService.cs:11 · Level 1 · class (sealed, internal)

  • What it is: an in-memory-empty ITokenStorageService: there is no stored session in the gallery. It exists so the AuthDelegatingHandler that AddUIShared registers resolves cleanly, and it is never actually invoked because the gallery makes no API calls (MMCA.Common.UI.Gallery/Stubs/NullTokenStorageService.cs:6-10).
  • Depends on: ITokenStorageService from MMCA.Common.UI.Services.Auth.
  • Concept introduced: the same stub pattern as NullTokenRefresher, covering storage rather than refresh, and the first place where a stub exists purely to satisfy a transitive constructor. AddUIShared registers AuthDelegatingHandler as transient (MMCA.Common.UI/DependencyInjection.cs:81) and attaches it to the named "APIClient" pipeline (MMCA.Common.UI/DependencyInjection.cs:85, :101), and that handler's primary constructor takes an ITokenStorageService (MMCA.Common.UI/Services/Auth/AuthDelegatingHandler.cs:10-11). Nothing in AddUIShared supplies one, so the host must. [Rubric §26, Front-End Security] assesses how auth tokens are stored and handled; this stub deliberately holds nothing, so the test host persists no credential material at all, not even in memory.
  • Walkthrough: four members, each inert but shape-complete so DI binds. GetAccessTokenAsync() (:12) and GetRefreshTokenAsync() (:14) return Task.FromResult<string?>(null); SetTokensAsync(accessToken, refreshToken) (:16) and ClearTokensAsync() (:18) discard their inputs and return Task.CompletedTask.
  • Where it's used: registered scoped by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:61), inside the block placed ahead of AddUIShared (MMCA.Common.UI.Gallery/GalleryHost.cs:56-64).
  • Caveats / not-in-source: unlike NoOpAuthUIService, this registration is not a TryAdd override. AddUIShared makes no ITokenStorageService registration anywhere in its body (MMCA.Common.UI/DependencyInjection.cs:34-140); the concrete storage services are supplied per host head instead, so in the gallery this stub is the only registration rather than a winning one.

SampleGridData

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Pages · MMCA.Common.UI.Gallery/Pages/SampleGridRow.cs:16 · Level 1 · class (static, internal)

  • What it is: the gallery's in-memory stand-in for a paged API. The rows are generated once from fixed inputs, with no randomness and no clock, so every E2E run, on every browser engine, scrolls over an identical data set and a row assertion can name an exact value (MMCA.Common.UI.Gallery/Pages/SampleGridRow.cs:11-15).
  • Depends on: SampleGridRow and BCL System.Globalization.CultureInfo. Nothing else.
  • Concept introduced, determinism as a precondition for real-browser assertions. The stubs above collapse an extension point to a constant; this one goes further and makes a whole data set a constant. A generated fixture is only usable in an E2E assertion if it produces byte-identical output on every run and every engine, so the builder takes its three inputs (index, a fixed five-item category array, a fixed UTC epoch) and derives everything else arithmetically. There is no Random, no DateTime.UtcNow, and the one string is formatted with CultureInfo.InvariantCulture so a CI runner's locale cannot change it. That is what lets GridPageE2ETests assert on the literal text "Row 0001" rather than on a shape. [Rubric §28, Front-End Testing] assesses real-browser coverage of the UI; a deterministic 1,000-row set is what makes "the DOM holds a small fraction of the data" a falsifiable claim. [Rubric §23, Front-End Performance] assesses whether the front end bounds the work it does per render; this data set exists specifically so the virtualization window can be measured instead of asserted by inspection.
  • Walkthrough:
    • RowCount = 1000 (:19) is a public constant, documented as "the row count the E2E windowing assertion is written against". The test mirrors it as its own private DataSetRowCount = 1000 (MMCA.Common.UI.E2E.Tests/Layout/GridPageE2ETests.cs:19) rather than referencing it, since the test assembly does not see this internal type.
    • Categories (:21) is a static readonly five-item array, and Epoch (:23) a static readonly 2026-01-01T00:00:00Z with an explicit DateTimeKind.Utc.
    • All (:25) is an IReadOnlyList<SampleGridRow> auto-property initialized once from Build(), so generation happens a single time per process and every request serves the same instances.
    • Build() (:27-42) fills a SampleGridRow[RowCount]. For index i it computes a one-based id (:32), formats Name as Row {id:D4} through string.Create(CultureInfo.InvariantCulture, ...) (:35) giving the zero-padded "Row 0001" the test asserts on, cycles Category with Categories[i % Categories.Length] (:36), spaces CreatedOn one hour apart with Epoch.AddHours(i) (:37), and derives Amount as id * 37 % 10000 / 100m (:38), an integer modulo followed by a decimal divide, which keeps every amount inside 0.00 to 99.99 with two decimal places.
  • Why it's built this way: a virtualization test needs enough rows that rendering all of them would be obviously wrong, and few enough that generation costs nothing at startup. 1,000 rows against the test's MaxRenderedRows = 200 ceiling (MMCA.Common.UI.E2E.Tests/Layout/GridPageE2ETests.cs:26) gives a fifth-of-the-data-set margin, which the test's own comment explains is generous headroom over the roughly 50 rows a 70vh viewport at 52px per row plus MudBlazor overscan actually renders (MMCA.Common.UI.E2E.Tests/Layout/GridPageE2ETests.cs:21-25).
  • Where it's used: MMCA.Common.UI.Gallery/Pages/GridGallery.razor reads SampleGridData.RowCount for its on-page caption (:12) and SampleGridData.All inside its static FetchPageAsync (:63), which sorts by the requested column and direction (:65-82), applies Skip((pageNumber - 1) * pageSize).Take(pageSize) (:84), and returns a Result<(IReadOnlyList<SampleGridRow> Items, int TotalItems)> (:86-87). That delegate deliberately matches IEntityService.GetPagedAsync's shape and 1-based page contract (comment, MMCA.Common.UI.Gallery/Pages/GridGallery.razor:52-53), so DataGridListPageBase<TDto>.LoadVirtualizedServerDataAsync (MMCA.Common.UI/Pages/Common/DataGridListPageBase.cs:606) maps the virtualization window onto it exactly as it would onto a real API.

GalleryUIModule

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/GalleryUIModule.cs:14 · Level 3 · class (sealed, internal)

  • What it is: a minimal IUIModule whose Assembly is the gallery itself, so the shared Blazor Router (Routes.razor, which scans UIModules.Select(m => m.Assembly)) discovers the gallery's own /components and /grid pages. Its nav links make the host browsable when run interactively (MMCA.Common.UI.Gallery/Stubs/GalleryUIModule.cs:9-13).
  • Depends on: IUIModule and NavItem from MMCA.Common.UI.Common, SharedResource from MMCA.Common.UI.Resources, plus MudBlazor.Icons and BCL System.Reflection.Assembly.
  • Concept introduced: the UI-module contribution pattern (taught with IUIModule in the Common UI framework group) reused for a test host: a module contributes route-bearing assemblies and nav links to the shared shell. [Rubric §18, UI Architecture] assesses how the front end composes independently-owned UI slices; the gallery participates in the exact module-discovery mechanism the real apps use, which is what makes the E2E evidence say something about that mechanism rather than about a bespoke test shell. [Rubric §25, Navigation & IA]: the four nav entries flow through the same NavItem record the deployed apps' menus are built from, supplying its first four positional members (title key, href, icon, resource type) and leaving RequiredRole, RequiredClaim, Section, and Group at their defaults (MMCA.Common.UI/Common/NavItem.cs:16). [Rubric §27, i18n]: passing typeof(SharedResource) as TitleResource opts the labels into the resource-key lookup ADR-027 defines, and because a key the resource type does not declare renders as the raw string, the untranslated gallery labels stay legible rather than blank (MMCA.Common.UI/Common/NavItem.cs:9-14).
  • Walkthrough: NavItems (:16-22) is a collection-expression IReadOnlyList<NavItem> of four entries, Login (/login), Register (/register), Components (/components), and Grid (/grid), each pairing a label, a route, a MudBlazor Material icon, and typeof(SharedResource) (:18-21). Assembly (:24) is an expression-bodied property returning typeof(GalleryUIModule).Assembly, so the Router additionally scans the gallery assembly for routable components, which is how MMCA.Common.UI.Gallery/Pages/ComponentsGallery.razor:1 (@page "/components") and MMCA.Common.UI.Gallery/Pages/GridGallery.razor:1 (@page "/grid") become reachable. The optional IUIModule members the interface declares beyond these two are left at their defaults.
  • Where it's used: registered as a singleton IUIModule by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:91); the pages it makes routable are what ComponentsPageE2ETests and GridPageE2ETests scan.

StubNotificationInboxUIService

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/StubNotificationInboxUIService.cs:11 · Level 4 · class (sealed, internal)

  • What it is: a canned INotificationInboxUIService returning fixed inbox data so NotificationBell and the notification inbox page render populated, real markup for the axe and render scans, with no backend (MMCA.Common.UI.Gallery/Stubs/StubNotificationInboxUIService.cs:7-10).
  • Depends on: INotificationInboxUIService from MMCA.Common.UI.Services.Notifications, the Result, PagedCollectionResult<T>, and PaginationMetadata types from MMCA.Common.Shared.Abstractions, the UserNotificationDTO contract, and the solution-wide UserNotificationIdentifierType alias.
  • Concept introduced: the stub pattern extended from inert no-ops to canned data. For components whose whole purpose is displaying content, an empty stub renders an empty (and therefore untested) tree, so this stub returns representative rows instead. Note that it still speaks the real Result contract on every member, so the pages take their success branches through the same unwrapping code the deployed apps run. [Rubric §28, Front-End Testing]: populated markup is what lets axe evaluate contrast, roles, and the read/unread affordances against a realistic notification list rather than an empty state.
  • Walkthrough: GetInboxAsync(pageNumber = 1, pageSize = 20, cancellationToken) (:13-14) builds a two-item UserNotificationDTO[] (:16-29): an unread "Welcome to MMCA" with a fixed UTC SentOn of 2026-01-02 09:00, and a read "Scheduled maintenance" carrying both ReadOn and SentOn, so the inbox exercises both visual states. It wraps them in a PagedCollectionResult<UserNotificationDTO> with new PaginationMetadata(items.Length, pageSize, pageNumber) (:30-31), so the pager renders from real metadata rather than a hardcoded count. GetUnreadCountAsync() (:34-35) returns a successful constant 3 so the bell badge renders non-empty. MarkReadAsync(id, ct) (:37-38) and MarkAllReadAsync(ct) (:40-41) are no-ops returning Result.Success(), so the buttons are present and clickable and their handlers take the success path without any state change.
  • Where it's used: registered scoped by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:80), alongside the scoped NotificationState (MMCA.Common.UI.Gallery/GalleryHost.cs:79). The scans that consume it are NotificationPagesE2ETests.NotificationInbox_Renders_AndHasNoWcag21AaViolations (MMCA.Common.UI.E2E.Tests/NotificationPagesE2ETests.cs:35-45) plus its two deep-link cases, .NotificationInboxDeepLink_ToAnUnknownNotification_DegradesToThePlainInbox (:47-62) and .NotificationInboxDeepLink_ToAKnownNotification_HighlightsThatCard (:64-75), each ending in an AxeOptions.Wcag21Aa scan.
  • Caveats / not-in-source: the badge count 3 (:34-35) is a hardcoded display value and does not reconcile with the two rows GetInboxAsync returns; it exists to render a non-empty badge for the scan, not to be internally consistent.

StubPushNotificationUIService

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/StubPushNotificationUIService.cs:11 · Level 4 · class (sealed, internal)

  • What it is: a canned IPushNotificationUIService so the notification history and compose pages render populated, real markup for the axe and render scans, with no backend (MMCA.Common.UI.Gallery/Stubs/StubPushNotificationUIService.cs:7-10).
  • Depends on: IPushNotificationUIService from MMCA.Common.UI.Services.Notifications, Result, PagedCollectionResult<T> and PaginationMetadata, plus the PushNotificationDTO and SendPushNotificationRequest contracts.
  • Concept introduced: the same canned-data variant of the stub pattern as StubNotificationInboxUIService, applied to the send and history side. [Rubric §24, Forms/Validation/UX Safety]: the compose page is a form, and echoing the submitted Title and Body back in the returned DTO lets the render smoke reach the post-submit state without a real send.
  • Walkthrough: SendAsync(request, cancellationToken) (:13-14) is an expression body returning a successful PushNotificationDTO that echoes request.Title and request.Body and fixes the rest, Id = 99, SentByUserId = 1, RecipientCount = 42, Status = "Sent", CreatedOn 2026-01-04 10:00 UTC (:15-19). GetHistoryAsync(pageNumber = 1, pageSize = 10, cancellationToken) (:21-22) builds a two-item history array (:24-36): one row with Status = "Sent" and one with Status = "Failed", both with RecipientCount = 128, so the history table renders both status treatments, then wraps them in a PagedCollectionResult<PushNotificationDTO> with PaginationMetadata (:37-38).
  • Where it's used: registered scoped by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:81). The scans that consume it are NotificationPagesE2ETests.NotificationHistory_Renders_AndHasNoWcag21AaViolations (MMCA.Common.UI.E2E.Tests/NotificationPagesE2ETests.cs:20-33) and .NotificationCompose_Renders_AndHasNoWcag21AaViolations (:77-87); the history scan runs under AxeOptions.Wcag21AaExceptMudPagerCombobox because MudBlazor's pager combobox has no accessible name and is not fixable from app markup (MMCA.Common.UI.E2E.Tests/NotificationPagesE2ETests.cs:32).

NoOpAuthUIService

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/NoOpAuthUIService.cs:14 · Level 6 · class (sealed, internal)

  • What it is: a no-op IAuthUIService for the backend-less gallery. The gallery renders the real Login, Register, and Sessions pages for accessibility and render-smoke scanning only, so every operation returns a benign default (MMCA.Common.UI.Gallery/Stubs/NoOpAuthUIService.cs:8-13).
  • Depends on: IAuthUIService from MMCA.Common.UI.Services.Auth, the Result and Error types from MMCA.Common.Shared.Abstractions, and the LoginRequest, RegisterRequest, AuthenticationResponse, and RefreshSessionSummaryResponse contracts from MMCA.Common.Shared.Auth.
  • Concept introduced, registration order as the override mechanism. This stub is registered before AddUIShared, whose TryAddScoped<IAuthUIService, AuthUIService>() (MMCA.Common.UI/DependencyInjection.cs:113) then defers to it, exactly as the class doc comment states (MMCA.Common.UI.Gallery/Stubs/NoOpAuthUIService.cs:11-12). TryAdd* is first-registration-wins, so a test host overrides only the extension points it names and inherits every other registration the shared UI makes, with no fork of the composition root. [Rubric §11, Security] assesses how authentication is handled; here the client-side auth boundary is neutralized entirely so the scan touches the real login and register markup without any credential flow. [Rubric §14, Testability]: substituting the top-level UI auth service for constants is what makes those pages renderable in isolation. [Rubric §1, SOLID]: the stub is only possible because the shared pages depend on the IAuthUIService abstraction rather than on AuthUIService, so swapping the implementation needs no change to any page.
  • Walkthrough:
    • Shared constants (:16-23): two static readonly session GUIDs, CurrentDeviceSessionId (all-ones) and OtherDeviceSessionId (all-twos), built from their component parts rather than parsed, with a comment recording why, the MA0176 analyzer rejects parsing a constant at runtime (:15). Unavailable (:22-23) is a single shared Error.Failure("Gallery.AuthUnavailable", "The gallery has no backend.") reused by every failing member.
    • Credential operations (:25-32): LoginAsync, RegisterAsync, and ExchangeOAuthCodeAsync each return Result.Failure<AuthenticationResponse>(Unavailable), so the pages render and submit but never establish a session.
    • Session lifecycle (:34-40): LogoutAsync() returns Task.CompletedTask, TryRefreshTokenAsync(ct) returns false, and ChangePasswordAsync(currentPassword, newPassword, ct) returns Result.Failure(Unavailable).
    • Password reset (:44-48): RequestPasswordResetAsync(email, ct) and ResetPasswordAsync(email, token, newPassword, ct) also fail with Unavailable. The comment at :42-43 records why that still produces useful scan coverage, the Forgot Password page shows its confirmation regardless of the result (anti-enumeration), so the post-submit state renders anyway. ForgotPasswordPageE2ETests asserts exactly that (MMCA.Common.UI.E2E.Tests/Auth/ForgotPasswordPageE2ETests.cs:27-38).
    • Device sessions (:53-74): GetSessionsAsync(ct) is the one member that returns real data. It builds two RefreshSessionSummaryResponse rows, a current device (all-ones id, a Chrome-on-Windows user agent, IpAddress: "203.0.113.7", IsCurrent: true) and another device (all-twos id, a Safari-on-iOS user agent, a null IpAddress, IsCurrent: false). The comment at :50-52 gives the reason, the axe scan has to reach the populated table, the current-device chip, and a live revoke button, which an empty state would hide, and one row is flagged current so both branches of the actions cell render.
    • Revoke (:76-77): RevokeSessionAsync(sessionId, ct) returns Result.Failure(Unavailable), so the button is present and wired without changing anything.
  • Why it's built this way: placing the stub ahead of AddUIShared exploits the shared UI's TryAdd* idempotence rather than requiring the shared registration extension to grow test hooks, which keeps the production DI code free of test-only branches. Returning a real failure Result rather than a null also keeps the pages on their genuine error-handling paths, which is what makes an error alert's markup part of the scanned surface.
  • Where it's used: registered scoped by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:60), the first of the pre-AddUIShared stub registrations. It is consumed indirectly by LoginPageE2ETests, RegisterPageE2ETests, ForgotPasswordPageE2ETests, ResetPasswordPageE2ETests, and SessionsPageE2ETests, the last of which asserts on the rendered form of these very rows, the visible texts "Chrome on Windows" and "This device" and the buttons "Sign out of Safari on iOS" and "Sign out of every device, including this one" (MMCA.Common.UI.E2E.Tests/Auth/SessionsPageE2ETests.cs:28-32).
  • Caveats / not-in-source: the two user-agent strings are supplied raw (MMCA.Common.UI.Gallery/Stubs/NoOpAuthUIService.cs:63, :69) while the test asserts on friendly names. The parsing that turns one into the other lives in the shared sessions page, not in this stub or this unit.

GalleryAuthenticationStateProvider

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/GalleryAuthenticationStateProvider.cs:16 · Level 8 · class (sealed, internal)

  • What it is: the gallery's Blazor AuthenticationStateProvider. It mirrors the request's authentication in both render phases, so AuthorizeView and CascadingAuthenticationState agree with whatever GalleryFakeAuthenticationHandler decided for the request. Without the gallery_auth cookie both phases yield anonymous, preserving the deliberate signed-out chrome of the login, register, components, and grid scans (MMCA.Common.UI.Gallery/Stubs/GalleryAuthenticationStateProvider.cs:6-15).
  • Depends on: Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider (the abstract base) and IHostEnvironmentAuthenticationStateProvider (the interface the Blazor Server host calls into) at :16-17, IHttpContextAccessor injected as a primary-constructor parameter (:16), and BCL ClaimsPrincipal / ClaimsIdentity.
  • Concept introduced, the two render phases of interactive-server Blazor. A page first renders as static SSR inside the HTTP request, then, once the circuit connects, re-renders interactively over a WebSocket where there is no ambient HttpContext. An auth-state provider therefore has to answer correctly in two different worlds. This class handles both: SSR reads the request user through IHttpContextAccessor, and for the interactive circuit the framework pushes the handshake user in through IHostEnvironmentAuthenticationStateProvider.SetAuthenticationState. The doc comment records that this replaced a former always-anonymous stub, which could not represent the signed-in state the guarded notification pages need (:7-9). [Rubric §19, State Management] assesses how client state is owned and propagated; auth state is the canonical cascading state, and this shows the two supply routes it has under interactive server rendering. [Rubric §28, Front-End Testing]: getting both phases right is what stops a guarded page from flipping to a signed-out tree mid-scan and producing a false pass on the wrong markup.
  • Walkthrough: Anonymous (:19-20) is a static readonly AuthenticationState wrapping an empty ClaimsPrincipal(new ClaimsIdentity()), unauthenticated precisely because no authentication type is supplied (contrast GalleryFakeAuthenticationHandler, which passes one). _hostState (:22) is the nullable task the framework may have pushed in. GetAuthenticationStateAsync() (:24) checks _hostState first and returns it verbatim when present (:26-29): the circuit's handshake user always wins. Otherwise it falls back to the SSR path, reading httpContextAccessor.HttpContext?.User (:31) and returning a new AuthenticationState(user) only when user?.Identity?.IsAuthenticated == true, else the shared Anonymous (:32-34). Both branches use Task.FromResult, so no async machinery is allocated per call. SetAuthenticationState(Task<AuthenticationState>) (:37-38) is the IHostEnvironmentAuthenticationStateProvider implementation and simply stores the task; it does not call NotifyAuthenticationStateChanged.
  • Why it's built this way: the gallery mirrors rather than fabricates. Deriving the component-tree state from whatever the request actually authenticated as keeps one source of truth (the cookie) for the middleware, the endpoint authorization, and the render tree, so a scan cannot land in a split state where the endpoint admitted the request but the tree still renders signed-out.
  • Where it's used: registered scoped as the AuthenticationStateProvider by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:64), immediately after AddHttpContextAccessor() (MMCA.Common.UI.Gallery/GalleryHost.cs:63), which supplies its dependency.
  • Caveats / not-in-source: the class implements IHostEnvironmentAuthenticationStateProvider but is registered only under the AuthenticationStateProvider service type (MMCA.Common.UI.Gallery/GalleryHost.cs:64). Whether the Blazor Server host resolves this same instance when it calls SetAuthenticationState, and at what point in the render sequence it does so, is framework behavior and is not determinable from this repository's source.

GalleryHost

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery/GalleryHost.cs:22 · Level 9 · class (public, static)

  • What it is: a static builder that assembles the entire backend-less Blazor gallery host. It renders the real MMCA.Common.UI auth pages (/login, /register, /profile/sessions), the shared notification pages, and the gallery's own primitives showcase (/components) and virtualized grid (/grid) against stub implementations of every consumer extension point, so a real-browser axe accessibility scan can run against the shared UI inside MMCA.Common's own CI (MMCA.Common.UI.Gallery/GalleryHost.cs:16-21).
  • Depends on: ASP.NET Core WebApplication / WebApplicationBuilder, MudBlazor (AddMudServices, MMCA.Common.UI.Gallery/GalleryHost.cs:54), the shared MMCA.Common.UI surface (AddUIShared, the gallery's own App root component in MMCA.Common.UI.Gallery/Components/App.razor, and MMCA.Common.UI._Imports as the additional-assembly marker), and SupportedCultures from MMCA.Common.Shared.Globalization. It wires in every stub in this unit: NoOpAuthUIService, NullTokenStorageService, NullTokenRefresher, GalleryAuthenticationStateProvider, GalleryFakeAuthenticationHandler, StubNotificationInboxUIService, StubPushNotificationUIService, and GalleryUIModule, plus the shared NotificationState and NullNotificationScopeProvider.
  • Concept introduced, a self-hostable test host as one buildable unit. The whole host build lives in BuildApp(string[] args) (:28) rather than in Program.cs, so two callers share the identical configured app: the dotnet run entry point, which RunAsync()s it (MMCA.Common.UI.Gallery/Program.cs:7-8), and the E2E collection fixture GalleryHostFixture, which StartAsync()s it on an ephemeral Kestrel port. [Rubric §28, Front-End Testing] assesses real-browser UI coverage; this host is the render target for CI's ui-e2e job (MMCA.Common/.github/workflows/ci.yml:228), whose chromium, firefox, and webkit matrix legs (:236-237) are all required merge gates (:238-240). [Rubric §33, Developer Experience]: MMCA.Common.UI.Gallery/Program.cs:3-6 records the rationale, one BuildApp for both entry points avoids the separate dotnet run plus health-poll that made ADC's e2e cold start fragile.
  • Walkthrough:
    • Assembly name and base dir (:33-34): typeof(GalleryHost).Assembly.GetName().Name is captured without a null-forgiving operator; the comment at :30-32 explains that CI's nullable analysis treats AssemblyName.Name as non-null and would flag ! as an unnecessary suppression (IDE0370), and the value is only interpolated into a filename, which is null-safe either way.
    • Static web assets (:45-48): the load-bearing fix. RCL _content/* CSS and JS plus _framework/blazor.web.js resolve from the entry assembly's manifests and auto-load only in Development; when the E2E suite self-hosts in-process the entry assembly is the test host and the environment is Production, so neither default holds. The loader is pointed explicitly at {galleryAssemblyName}.staticwebassets.runtime.json and forced on with UseStaticWebAssets(). Without it (comment, :38-44) the pages render unstyled and never become interactive, so axe's contrast checks would be meaningless and the page would never signal Blazor readiness.
    • Rendering services (:50-53): AddRazorComponents().AddInteractiveServerComponents(), then AddMudServices().
    • Extension-point stubs, before AddUIShared (:59-63): scoped IAuthUIService, ITokenStorageService, and ITokenRefresher, then AddHttpContextAccessor() and the scoped AuthenticationStateProvider. The ordering comment (:55-58) states the mechanism, AddUIShared's TryAdd* registrations defer to whatever is already present.
    • Real authentication and authorization (:69-73): AddAuthentication(GalleryFakeAuthenticationHandler.SchemeName) plus AddScheme<AuthenticationSchemeOptions, GalleryFakeAuthenticationHandler>(...), then AddAuthorization(), because the guarded pages' [Authorize] surfaces as endpoint metadata (comment, :65-68).
    • Canned notification services (:78-80): scoped NotificationState, INotificationInboxUIService, and IPushNotificationUIService, so the notification pages discovered from the MMCA.Common.UI assembly render populated markup (comment, :75-77).
    • Notification scope (:85): scoped INotificationScopeProvider bound to the shared NullNotificationScopeProvider, whose GetCurrentScopeKeyAsync returns null (MMCA.Common.UI/Services/Notifications/NullNotificationScopeProvider.cs:11-12). The comment (:82-84) explains the choice: the send page injects the scope provider to caption its auto-applied target, and the gallery is unscoped, so the null provider keeps the page rendering with the caption absent, which is the framework-default look the scan should measure.
    • Module contribution (:90): the singleton IUIModule, so the shared Router discovers the gallery's own /components and /grid pages (comment, :87-89).
    • Shared UI (:95): AddUIShared(builder.Configuration) registers the ApiSettings / LayoutSettings binding, the "APIClient" HttpClient, and the remaining shared services; the in-memory Api:ApiEndpoint from appsettings.json satisfies validation and deliberately points at the unroutable http://api.gallery.invalid (MMCA.Common.UI.Gallery/appsettings.json:2-4), and the client is never invoked because IAuthUIService is stubbed (comment, :92-94).
    • Build (:97): builder.Build(), after which only middleware and endpoints are configured.
    • Request localization (:104-108): builds galleryCultures as [.. SupportedCultures.All, SupportedCultures.PseudoLocale] and applies it as the supported and supported-UI culture set over SupportedCultures.Default. The comment (:99-103) is explicit that this mirrors the real hosts' ADR-027 allowlist but additionally enables qps-Ploc unconditionally, because this host is unpackaged test infrastructure that is never deployed and the pseudo pass here is a required CI gate (PseudoLocalizationE2ETests, the rubric §27 resource-round-trip and text-expansion evidence). Production keeps qps-Ploc Development-only via UseCommonRequestLocalization, and the comment closes with "Do not copy this into a real host". [Rubric §27, i18n] assesses whether localization is enforced rather than aspirational; this host is where the pseudo-locale evidence is produced.
    • Middleware (:112-117): UseAuthentication() then UseAuthorization() (WebApplication inserts UseRouting ahead of them automatically, comment :110-111), then UseAntiforgery(), required because Razor Component endpoints carry anti-forgery metadata even though the gallery's interactive forms never POST over HTTP (comment, :115-116).
    • Endpoints (:121-130): MapStaticAssets is given the gallery's own {galleryAssemblyName}.staticwebassets.endpoints.json for the same in-process self-host reason as above (comment, :119-120); a /health endpoint returns Results.Ok("Healthy") (:124); and MapRazorComponents<App>().AddInteractiveServerRenderMode().AddAdditionalAssemblies(typeof(MMCA.Common.UI._Imports).Assembly) (:128-130) makes the real shared pages routable alongside the gallery's own (comment, :126-127).
    • Return (:132): the built-but-not-started WebApplication, leaving the start mode to the caller.
  • Why it's built this way: keeping the whole build in BuildApp rather than Program.cs lets the E2E fixture host the identical configured app in-process on a real bound port via StartAsync, not WebApplicationFactory's in-memory TestServer, which Playwright cannot reach over the wire. GalleryHostFixture therefore clears the URLs, binds http://127.0.0.1:0, and reads the ephemeral address back from IServerAddressesFeature (MMCA.Common.UI.E2E.Tests/Infrastructure/GalleryHostFixture.cs:26-37). This is deliberate CI infrastructure and is never shipped: the csproj sets IsPackable=false and records that the project is deliberately outside MMCA.Common.slnx (MMCA.Common.UI.Gallery/MMCA.Common.UI.Gallery.csproj:3-6), so it builds only by csproj path.
  • Where it's used: consumed by MMCA.Common.UI.Gallery/Program.cs:7 (the dotnet run entry) and by every E2E test through GalleryHostFixture (MMCA.Common.UI.E2E.Tests/Infrastructure/GalleryHostFixture.cs:26), against which the axe and render suite runs: LoginPageE2ETests, RegisterPageE2ETests, ForgotPasswordPageE2ETests, ResetPasswordPageE2ETests, SessionsPageE2ETests, ComponentsPageE2ETests, GridPageE2ETests, NotificationPagesE2ETests, DarkModeE2ETests, MobileTopRowE2ETests, StickySidebarE2ETests, WebVitalsE2ETests, and PseudoLocalizationE2ETests.
  • Caveats / not-in-source: the class summary (MMCA.Common.UI.Gallery/GalleryHost.cs:16-21) lags the body in two ways. It names only /login, /register, and /components while BuildApp also serves the guarded notification and sessions pages and the /grid virtualization page, and it still describes the stub set as including an "anonymous auth state", whereas GalleryAuthenticationStateProvider mirrors the request rather than forcing anonymous.

ObservabilityConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/ObservabilityConventionTests.cs:7 · Level 1 · class (public, sealed)

  • What it is - the SLO alert-to-runbook pairing gate for ADC, and the shortest type in this unit: a bodyless class declaration, public sealed class ObservabilityConventionTests : ObservabilityConventionTestsBase; (MMCA.ADC.Architecture.Tests/Governance/ObservabilityConventionTests.cs:7). It overrides nothing at all.
  • Depends on - ObservabilityConventionTestsBase, plus two EmbeddedResource entries in the csproj that supply the files the base reads: infra/main.bicep under the logical name infra.main.bicep and infra/OPERATIONS.md under infra.OPERATIONS.md (MMCA.ADC.Architecture.Tests/MMCA.ADC.Architecture.Tests.csproj:17-:22).
  • Concept introduced, identity by inheritance alone. This is the thin-subclass pattern from BrandColorTokenTests reduced to its limit. The base defaults ResourceAssembly to GetType().Assembly (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs:51), so the derived type is the configuration: deriving in this assembly is what points the rule at ADC's embedded bicep and runbook. The file comment states exactly that ("this repo supplies only its identity", ObservabilityConventionTests.cs:3-:6). [Rubric §13 - Observability & Operability] assesses whether the system can be operated under failure, which means alerts that lead somewhere; this pairs each provisioned alert with a runbook section at build time instead of at 3am.
  • Walkthrough - no members. Everything runs from the base's three inherited facts: SloAlertSpecs_AreDiscovered_GateIsNotVacuous (ObservabilityConventionTestsBase.cs:54) enforces the non-vacuity floor of MinimumAlertSpecs, defaulted to 3 and not overridden here (:39); EveryProvisionedSloAlert_HasASeverityCorrectRunbookSection (:64) walks the alerts declared in the embedded bicep and requires a matching, severity-correct section in the embedded runbook; and EveryRunbookAlertSection_MapsToAProvisionedAlert (:92) closes the other direction, failing on an orphan runbook section for an alert that no longer exists. The resource names the base reads default to infra.main.bicep and infra.OPERATIONS.md (:42, :45), which is why the csproj logical names must match exactly.
  • Why it's built this way - alert definitions live in infrastructure-as-code and the response procedure lives in a Markdown runbook; nothing in either file references the other, so the pairing is exactly the kind of invariant that decays silently. Embedding both into the test assembly turns the pairing into a compile-and-run artifact.
  • Where it's used - runs with the rest of the suite in the build-and-test job (MMCA.ADC/.github/workflows/deploy.yml:189, :284).

BrandColorTokenTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/BrandColorTokenTests.cs:12 · Level 1 · class (public, sealed)

  • What it is - the ADC end of the brand-token drift guard. It is a six-line subclass of the shared BrandColorTokenTestsBase that names one embedded stylesheet, ADCHome.Shared.razor.css (MMCA.ADC.Architecture.Tests/Ui/BrandColorTokenTests.cs:14-:17); the rule body itself lives in MMCA.Common.
  • Depends on - BrandColorTokenTestsBase from the MMCA.Common.Testing.Architecture package (referenced at MMCA.ADC.Architecture.Tests/MMCA.ADC.Architecture.Tests.csproj:41), plus the EmbeddedResource item that maps the conference landing page's scoped stylesheet into this assembly under that logical name (MMCA.ADC.Architecture.Tests/MMCA.ADC.Architecture.Tests.csproj:11-:13). Externals: xUnit v3, AwesomeAssertions, and NetArchTest (MMCA.ADC.Architecture.Tests.csproj:25-:27), the last two reachable everywhere in the assembly through the global usings (MMCA.ADC.Architecture.Tests/GlobalUsings.cs:1-:5).
  • Concept introduced, the thin-subclass fitness function. Every type in this unit follows one shape, so learn it once here. A fitness function is an executable test that asserts an architectural property instead of a behavior. MMCA keeps the property's logic in exactly one place, an abstract *TestsBase in the shared MMCA.Common.Testing.Architecture package, and each repo derives a sealed subclass that supplies only its own identity: which assemblies to scan, which floors and allowlists apply, which files to read. xUnit discovers [Fact]s on inherited members, so the subclass needs no test method of its own; deriving the class is what makes the rule run in this repo (ADR-015). [Rubric §34 - Architecture Governance & Documentation] assesses whether architectural decisions are recorded and enforced rather than trusted to reviewers; here the decision is enforced by a build that goes red. [Rubric §20 - Design System & Theming] assesses whether a design system has one source of truth for its tokens; this rule is what stops a host copy of the landing page from re-hardcoding the brand hex.
  • Walkthrough - one member. EmbeddedCssLogicalNames (BrandColorTokenTests.cs:14-:17) is a collection expression with a single entry, "ADCHome.Shared.razor.css". That string is not a file path: it is the LogicalName the csproj assigns when it embeds Source/Modules/Conference/MMCA.ADC.Conference.UI/Pages/Home/ADCHome.razor.css as a manifest resource (MMCA.ADC.Architecture.Tests.csproj:11-:13), which is how a test assembly reads a file from a project it does not reference. The inherited fact LandingPageCss_SourcesBrandColorFromToken_NotHardcodedHex (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/BrandColorTokenTestsBase.cs:25) then loads each named resource and asserts the token rule over it; the abstract hook this class implements is declared at :22.
  • Why it's built this way - the class comment records the split (BrandColorTokenTests.cs:3-:11): MMCA.Common's own BrandColorTokenTests guards the C#-to-CSS token definition, and this one guards the ADC consumer of it. Embedding the stylesheet rather than reading it off disk means the guard travels with the compiled test assembly and cannot be defeated by a runner whose working directory differs.
  • Where it's used - the whole project is inside ADC's CI solution filter (MMCA.ADC/MMCA.ADC.CI.slnf:58), which the build-and-test job restores, builds, and tests on every PR and every push to main (MMCA.ADC/.github/workflows/deploy.yml:4-:7, :189, :264, :270, :284).
  • Caveats / not-in-source - the guard only covers stylesheets that are both embedded and listed. ADC lists exactly one, so a second landing-page stylesheet added later is invisible to the rule until someone adds it to both places.

AnonymousEndpointTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Api · MMCA.ADC.Architecture.Tests/Api/AnonymousEndpointTests.cs:21 · Level 6 · class (public, sealed)

  • What it is - the security gate that no endpoint loses its authorization unnoticed: every [AllowAnonymous] reachable in ADC's three module API assemblies and three module UI assemblies must appear as a reviewed line in this file (MMCA.ADC.Architecture.Tests/Api/AnonymousEndpointTests.cs:23-:31, :33-:103). It is the largest subclass in the unit, 48 allowlist entries long.
  • Depends on - AnonymousEndpointTestsBase, System.Reflection.Assembly (global-used at MMCA.ADC.Architecture.Tests/GlobalUsings.cs:1), and, as type references pinning the three API assemblies, IdentityModule, ConferenceModule, and EngagementModule (:25-:27). The three UI assemblies are loaded by name (:28-:30). Note it does not take the map: it names its own assembly set.
  • Concept introduced, the reviewed-allowlist gate. The rules elsewhere in this unit assert a structural property. This one asserts a review property: the set of anonymous endpoints is not wrong, it is simply not allowed to change silently. The base makes that stick in three directions at once: AnonymousEndpoints_AreAllowListed fails on an unlisted [AllowAnonymous] (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:54), ScannedEndpointSet_IsNotEmpty fails when fewer than MinimumScannedTypes endpoint types were discovered at all (:66), and AllowList_HasNoStaleEntries fails on a listed entry that no longer matches anything (:79), which is what stops the list from silently accumulating names for endpoints that were renamed or re-gated. [Rubric §11 - Security] assesses whether the authorization posture is deliberate and verifiable; an allowlist that must be edited, in a file a reviewer reads, is the mechanism. [Rubric §26 - Front-End Security] applies too, because routable Blazor components are scanned alongside controllers.
  • Walkthrough - three members.
    • TargetAssemblies (:23-:31) names six assemblies: the Identity, Conference, and Engagement API assemblies by anchor type, and the matching three UI assemblies via Assembly.Load. The base scans each for the two shapes it understands, MVC controllers (any type whose base chain reaches ControllerBase, AnonymousEndpointTestsBase.cs:101-:112) and routable Blazor components (any type carrying a RouteAttribute, :117-:118), both matched by attribute full name so the rule library keeps no ASP.NET reference (:26-:28, :32-:34).
    • AllowedAnonymousEndpoints (:33-:103) is the reviewed list, grouped by justification rather than alphabetically. Two Identity credential-exchange actions on AuthController, LoginAsync and RegisterAsync, because requiring a token to mint one would be circular; they are throttled by the auth-ip rate-limit policy instead (:35-:39). Then the Conference public-browse reads: the GetAllAsync / GetAllForLookupAsync / GetByIdAsync triple on thirteen agenda controllers (ActivitiesController, CategoryItemsController, EventsController, QuestionsController, RoomsController, SessionsController, SpeakersController, SponsorsController, and their category and join siblings, :45-:83), because the conference website is readable without an account and is output-cached per ADR-040; the comment is careful to note that every create/update/delete on those same controllers stays behind the class-level [HasPermission] (:41-:44). Then three smaller families with their own reasons: the Now/Next wayfinding reads (:85-:88), the ICS calendar exports a calendar client fetches without a bearer token (:90-:93), and the aggregate bookmark counts behind the popularity badge, counts only and never a per-user list (:95-:98). Last, the type-level entry for ServiceInfoController, which must answer before a caller has a token to negotiate with (:100-:102). Type-level and method-level attributes use different identifier shapes, a bare FullName versus FullName.MethodName (AnonymousEndpointTestsBase.cs:39-:44), which is why that last entry has no method suffix.
    • MinimumScannedTypes => 79 (:108) raises the base floor of 1 (AnonymousEndpointTestsBase.cs:51) to the exact count of controller and routable-component types across the six assemblies today, so a renamed assembly or a dropped surface is a failure rather than a quietly smaller scan (:105-:107).
  • Why it's built this way - the class comment explains what the absences mean, which is the part a reader cannot infer. The two password-recovery actions on PasswordResetController are anonymous for the same circularity reason as login, but ADC does not override them, so the framework base owns their allowlist entries and none appear here (:11-:15); the same is true of refresh (:37). The base reads attributes with DeclaredOnly and inherit: false precisely so an inherited framework action is reported once at its declaration site rather than once per derived controller in every consumer (AnonymousEndpointTestsBase.cs:140-:142). Notification is absent because that module ships no controller and no routable component, hosting only the SignalR hub, so it contributes nothing to the scan (:16-:19).
  • Caveats / not-in-source - the base states its own blind spot: minimal-API endpoints opt out through the .AllowAnonymous() builder call, which produces endpoint metadata at map time and is invisible to static reflection, so the framework's own small anonymous minimal-API surface (JWKS, OIDC discovery, app-association, session-cookie refresh, health) is not covered here (AnonymousEndpointTestsBase.cs:18-:24). Nothing recomputes the 79 floor, so it is a lower bound a human maintains.

FolderWidthTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/FolderWidthTests.cs:10 · Level 6 · class (public, sealed)

  • What it is - the layout gate: no folder under ADC's Source/ or Tests/ tree may hold more than 12 direct code files, so a folder keeps naming a feature instead of turning into a technical bucket. It is a four-line subclass whose only override supplies the repository root (MMCA.ADC.Architecture.Tests/Governance/FolderWidthTests.cs:10-:13).
  • Depends on - FolderWidthTestsBase and ArchitectureMapBase, whose static FindRepoRoot("MMCA.ADC.slnx") walks up from the test assembly until it finds the solution file, which is how the rule locates the working tree regardless of the runner's working directory (FolderWidthTests.cs:12).
  • Concept introduced, a fitness function that reads the filesystem rather than IL. Every other rule in this unit reflects over compiled assemblies. This one cannot: the defect is a layout defect, so the rule walks directories from disk (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.FolderWidth.cs:9-:11, :42-:63). That is also why the subclass supplies a path instead of a map. [Rubric §5 - Modular Monolith & Vertical Slices] assesses whether the code is organized by feature rather than by technical layer; this makes "module by project, feature by folder, use case by leaf" an assertion instead of a review habit (ADR-109).
  • Walkthrough - one member. RepoRoot (:12) is the single override. Everything else is inherited: the cap MaxDirectFiles => 12 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FolderWidthTestsBase.cs:23), an empty ExemptFolderSuffixes default that ADC does not override, so ADC carries no documented exemption (:29), and the single fact Folders_stay_narrow, which hands both to the rule (:31-:33). The counting rules are the interesting part: only files directly in the directory count, a .razor component and its co-located X.razor.cs code-behind count as one unit, .resx satellites never count, and generated files (*.g.cs, *.generated.cs, *.Designer.cs) never count because nobody chose to put them there (ArchitectureRules.FolderWidth.cs:13-:19). Directories with a path segment named bin, obj, Migrations, Platforms, Resources, node_modules, wwwroot or .git are skipped outright, being build output or tool-owned trees whose shape is not the author's decision (:22-:24). Violations are collected, sorted ordinally, and reported together through ArchitectureAssert.NoViolations with the remedy in the message, "split it by feature or aggregate" (:66-:70).
  • Why it's built this way - the class comment names the layouts the rule protects, Services/{Feature}/ beside Pages/{Feature}/, Controllers/{Aggregate}/, EntityConfiguration/{Aggregate}/, and states the failure mode it prevents: drifting back into flat technical buckets (FolderWidthTests.cs:3-:8). A cap enforced per repo rather than per project is deliberate, because the drift shows up wherever a folder is convenient, not only in the module the change belongs to.
  • Where it's used - the project is inside ADC's CI solution filter (MMCA.ADC/MMCA.ADC.CI.slnf:59), which the build-and-test job builds and tests on every PR (MMCA.ADC/.github/workflows/deploy.yml:205, :284, :300).
  • Caveats / not-in-source - the rule sees files, not namespaces, so a folder that stays under 12 while mixing unrelated features passes; the cap is a ceiling on width, not a check that the folder names one thing.

ProtoContractTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Contracts · MMCA.ADC.Architecture.Tests/Contracts/ProtoContractTests.cs:3 · Level 6 · class (public, sealed)

  • What it is - the frozen wire contract for ADC's synchronous cross-service API. It names the seven .proto files the four *.Contracts projects compile and commits a 76-line snapshot of everything they declare, so a renumbered field or a renamed rpc fails the build (MMCA.ADC.Architecture.Tests/Contracts/ProtoContractTests.cs:9-:18, :20-:98).
  • Depends on - ProtoContractTestsBase. Nothing else: the rule reads .proto files off disk from the repo root, so this class takes no map and the csproj needs no reference to the *.Contracts projects.
  • Concept introduced, pinning a contract that no compiler checks. A .proto file is a published contract between processes that are built separately, so nothing in a single repo's build notices when it changes incompatibly. The rule library rebuilds the live contract by parsing the files and diffs it against the committed list, reporting each side separately as "present in the .proto files but NOT frozen" and "frozen but NOT present in the .proto files" (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:67-:77). What gets pinned is exactly the wire surface: the package, every rpc with its request/response types and streaming flags, every message field with its declared type, label, and field number, and every enum value with its number (ArchitectureRules.Protos.cs:21-:24). What is deliberately not pinned is syntax, import, and option lines including csharp_namespace, because none of them changes a byte on the wire and failing on them is the fastest way to teach a team to update a snapshot without reading it (:27-:31). [Rubric §9 - API & Contract Design] assesses contract governance across the whole surface, not just REST. [Rubric §7 - Microservices Readiness]: this list is the synchronous coupling between ADC's four services, in the same way IntegrationEventContractTests is the asynchronous one (ADR-007).
  • Walkthrough - three members, all implementing abstract hooks.
    • SolutionFileName => "MMCA.ADC.slnx" (:5) implements ProtoContractTestsBase.SolutionFileName (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ProtoContractTestsBase.cs:22). The rule resolves the repo root from it via ArchitectureMapBase.FindRepoRoot (ArchitectureRules.Protos.cs:45), so the files are read from the working tree regardless of the runner's working directory.
    • ProtoFiles (:9-:18) lists seven repo-root-relative paths, and the comment states the scope rule: every .proto compiled by the four *.Contracts projects (:7-:8). Two from Conference (event_live_validation, session_bookmark_validation), two from Engagement (bookmark_count, user_engagement_export), one from Identity (attendee_query), and two from Notification (live_channel, user_notification_export).
    • FrozenProtoContracts (:20-:98) is the snapshot: 64 message ... lines (:22-:85), one per field, each ending in = <number> : <type>, and 12 service ... lines (:86-:97), one per rpc. The entries are sorted, which is what makes a regenerated snapshot diff line by line, and the base's <remarks> explains how to regenerate it (print ArchitectureRules.BuildProtoContract(...) and paste, ProtoContractTestsBase.cs:12-:17). Reading the list is the fastest way to see what ADC's services actually say to each other: live-window validation, current-room lookup and sponsor live info from Conference, bookmark counts and the engagement export from Engagement, the attendee user-id list from Identity, and channel push plus the notification export from Notification.
    • The single inherited fact is ProtoContracts_ShouldMatch_TheFrozenSnapshot (ProtoContractTestsBase.cs:33).
  • Why it's built this way - the base is explicit that this is consumer-facing only: MMCA.Common ships the gRPC plumbing but no .proto of its own, so the framework does not subclass it, and a repo with a *.Contracts project does (ProtoContractTestsBase.cs:9-:11). The four *.Contracts projects are not registered in AdcArchitectureMap at all: this rule works from file paths rather than mapped assemblies, which is why it reaches them anyway.
  • Caveats / not-in-source - the file list is hand-maintained, so a brand-new .proto added to a *.Contracts project is not pinned until someone lists it here. Nothing asserts that ProtoFiles covers every .proto in the tree.

SortableColumnConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/SortableColumnConventionTests.cs:11 · Level 6 · class (public, sealed)

  • What it is - the MudDataGrid sorting guard: a column marked sortable must be a PropertyColumn, not a TemplateColumn. The subclass supplies one scan root, the whole ADC Source tree (MMCA.ADC.Architecture.Tests/Ui/SortableColumnConventionTests.cs:14-:17).
  • Depends on - SortableColumnConventionTestsBase, and ArchitectureMapBase called statically for the repo root (:16). Like ProtoContractTests and TranslationCompletenessTests, it takes no map: the rule is a markup text scan, so a directory is all it needs.
  • Concept introduced, guarding a defect that renders correctly. Server-side sort reads the bound property off the column, and a TemplateColumn has none, so Sortable="true" on one produces a header that toggles a sort arrow without ordering the data (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/SortableColumnConventionTestsBase.cs:4-:8). The defect compiles, renders, and stays invisible until somebody checks the order, which is the exact profile of a bug a fitness function should own rather than a code review. [Rubric §24 - Forms/Validation/UX Safety] is the base's own stated target, since an affordance that lies to the user is a UX-safety failure; [Rubric §18 - UI Architecture] and [Rubric §12 - Performance & Scalability] are the neighbours, because the whole point of server-side sort is to keep the grid from pulling the full set into the browser.
  • Walkthrough - one member. MarkupRoots (:14-:17) is a single-element collection expression: Path.Combine(ArchitectureMapBase.FindRepoRoot("MMCA.ADC.slnx"), "Source"). Building the path from FindRepoRoot rather than a relative path is what makes the scan independent of the runner's working directory (SortableColumnConventionTestsBase.cs:19-:23), and pointing it at the whole Source tree rather than one project is what makes it cover the three module UI projects and both web heads, which the class comment states explicitly (SortableColumnConventionTests.cs:7-:9). The inherited fact is SortableColumns_ShouldNotBe_TemplateColumns (SortableColumnConventionTestsBase.cs:27), delegating to ArchitectureRules.SortableGridColumnsUsePropertyColumn(MarkupRoots). The base notes that @* *@ comments are ignored and that a root which does not exist fails the test rather than silently passing (:12-:13).
  • Why it's built this way - the class comment records that ADC already used PropertyColumn for every sortable column when the rule was adopted, so this subclass freezes an already-clean state rather than pinning debt (SortableColumnConventionTests.cs:7-:8). Compare RawQueryableConventionTests, which had to be adopted the other way, over an existing set of violations.
  • Caveats / not-in-source - detection is textual over .razor markup, so a grid composed at runtime, or a column whose Sortable value comes from a variable rather than a literal, is outside what a text scan can judge.

TranslationCompletenessTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/TranslationCompletenessTests.cs:12 · Level 6 · class (public, sealed)

  • What it is - the internationalization completeness gate: every base *.resx under Source/ must have a complete, non-empty Spanish .es.resx sibling, so adding an English key without its translation fails CI instead of shipping a half-translated UI (MMCA.ADC.Architecture.Tests/Ui/TranslationCompletenessTests.cs:3-:11).
  • Depends on - LocalizationResourceTestsBase. Note the deliberate name divergence: the ADC subclass is named for what it guarantees (translation completeness), not for the base it derives from.
  • Concept introduced, the non-vacuity floor. A convention scan that discovers nothing passes trivially, which is the failure mode that makes fitness functions untrustworthy over time. The MMCA bases answer it with a minimum-count floor that the subclass raises to the repo's real magnitude, so a broken scan root (a moved directory, a renamed convention, a case-sensitivity slip on the Ubuntu runner) fails loudly instead of going green while checking zero files. You have already seen the floor in AnonymousEndpointTests, and you will see it again in CommandValidatorCoverageTests, ErrorCatalogTests, FormsConventionTests, and LocalizedTextConventionTests. [Rubric §27 - i18n] assesses whether localization is enforced rather than aspirational; the gate is the enforcement, and ADR-027 (which supersedes the single-locale ADR-011) is the decision it executes.
  • Walkthrough - two members. RequiredCultures => ["es"] (TranslationCompletenessTests.cs:14) implements the base's abstract culture list (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizationResourceTestsBase.cs:13), so Spanish is the one culture ADC contractually completes. MinimumBaseResources => 40 (TranslationCompletenessTests.cs:16) raises the base's default of 0 (LocalizationResourceTestsBase.cs:21), which would otherwise let an empty scan pass. The inherited fact is Translations_AreComplete_ForEveryRequiredCulture (LocalizationResourceTestsBase.cs:24).
  • Why it's built this way - the class comment justifies the floor from the repo's real shape: ADC has 40 or more localized resource sets across the three module UIs, the UI hosts' landing page, the nav-item module descriptors, and the API error-resource sets, so a near-zero discovery count means the scan path is wrong (TranslationCompletenessTests.cs:8-:10).
  • Caveats / not-in-source - the floor is a lower bound stated in the subclass, not a count computed from the tree, so it stays correct only as long as someone raises it when the resource set grows materially.

DecoratorPipelineOrderTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/DecoratorPipelineOrderTests.cs:29 · Level 9 · class (public, sealed)

  • What it is - the one type in this unit that builds a real DI container instead of reading metadata. It asserts that ADC's genuine registration sequence produces the ADR-014 decorator nesting at runtime, exercised against a real Identity command/query pair (MMCA.ADC.Architecture.Tests/Cqrs/DecoratorPipelineOrderTests.cs:19-:28).
  • Depends on - DecoratorPipelineOrderTestsBase<TCommand, TCommandResult, TQuery, TQueryResult> from the MMCA.Common.Testing package (MMCA.ADC.Architecture.Tests.csproj:43), closed over ChangePreferencesCommand / Result and GetUserPreferencesQuery / Result<UserPreferencesResponse> (DecoratorPipelineOrderTests.cs:30). Externals: Microsoft.Extensions.DependencyInjection, Microsoft.FeatureManagement, NullLogger<>, and Moq (:1-:13).
  • Concept introduced, an object-graph assertion. Scrutor's TryDecorate applies decorators in reverse registration order, so the last decorator registered becomes the outermost wrapper. That makes an innocent-looking reorder of the AddApplicationDecorators() lines, or a module handler scan that runs after it instead of before, a silent change in runtime behavior: the code still compiles, the container still resolves, and the pipeline quietly runs validation after the transaction opens. The base turns that into a test failure by resolving the handler and walking the constructed graph via reflection over each decorator's private inner-handler field, so it verifies the objects that actually exist rather than the registration list (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:29-:32, :100-:125). [Rubric §2 - Design Patterns] assesses whether patterns are applied deliberately and correctly; the decorator chain is the framework's central pattern and this is the only test that proves its composition. [Rubric §14 - Testability]: the fact that a production registration sequence can be replayed in a bare ServiceCollection with seven mocked dependencies is itself the evidence that the composition root is not entangled with hosting.
  • Walkthrough - one member, ConfigureServices(IServiceCollection) (DecoratorPipelineOrderTests.cs:32), which implements the base's single abstract hook (DecoratorPipelineOrderTestsBase.cs:46) and reads in two halves.
    • Test doubles for the decorator constructor dependencies (:33-:39): Mock.Of<IFeatureManager>(), Mock.Of<ICurrentUserService>(), Mock.Of<IPermissionRegistry>(), Mock.Of<ICorrelationContext>(), and Mock.Of<ICacheService>() as singletons, a scoped IUnitOfWork factory, and the open generic ILogger<> mapped to NullLogger<>. These exist only so the decorators can be constructed; the test never invokes a handler. The base names the same seven dependencies as the contract for a subclass (DecoratorPipelineOrderTestsBase.cs:22-:25).
    • The real registration sequence (:43-:45): AddApplication(), then ScanModuleApplicationServices<MMCA.ADC.Identity.Application.ClassReference>(), then AddApplicationDecorators() last. The comment states the load-bearing constraint plainly (:41-:42): TryDecorate can only wrap handlers already registered.
    • The two inherited facts then assert the chains. CommandPipeline_NestsDecorators_InAdr014Order (DecoratorPipelineOrderTestsBase.cs:72) expects FeatureGate, Authorization, Logging, Caching, Validating, Timeout, Transactional, then the concrete handler (:49-:58); QueryPipeline_NestsDecorators_InAdr014Order (:76) expects FeatureGate, Authorization, Logging, Caching, Validating, Timeout, then the handler (:61-:69). The query chain now carries a ValidatingQueryDecorator too (:67), so the two lists differ only by the transactional stage. Both are asserted by AssertPipeline (:79-:98), which compares every element except the last against the expected list (:93-:94) and then requires the innermost element not to end in "Decorator" (:96-:97), so a truncated chain cannot pass. ADC overrides neither expected list, so it accepts the framework default order as its contract.
  • Why it's built this way - the pair was chosen for realism rather than convenience. ChangePreferencesCommand and its handler are shipped ADC Identity use cases, while GetUserPreferencesQuery is declared in MMCA.Common.Application and its concrete GetUserPreferencesHandler lives in ADC on top of Common's GetUserPreferencesHandlerBase<TUser>. So the scan-then-decorate ordering is exercised across the framework and app boundary rather than against a fixture, which is exactly what the base asks a subclass to supply (DecoratorPipelineOrderTestsBase.cs:26-:27).
  • Where it's used - an independent class in ADC's architecture suite; nothing consumes it.
  • Caveats / not-in-source - the chain is unwrapped by reading compiler-generated private fields (DecoratorPipelineOrderTestsBase.cs:105-:125), so a future decorator that stores its inner handler somewhere other than a field (a property-only or captured-closure design) would be invisible to the walk. The base flags the reflection strategy explicitly (:29-:32).

AdcArchitectureMap

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests/AdcArchitectureMap.cs:14 · Level 12 · class (internal, sealed)

  • What it is - the single declaration of what "the ADC architecture" is, in assembly terms: five MMCA.Common framework layers, the Identity, Conference, and Engagement modules at six layers each, and the thin Notification module at three, 26 entries in all (MMCA.ADC.Architecture.Tests/AdcArchitectureMap.cs:18-:55). Every map-driven rule in this unit scans exactly the assemblies listed here.
  • Depends on - ArchitectureMapBase (which implements IArchitectureMap), the Layer enum and the LayerRef record, and System.Reflection.Assembly (global-used at MMCA.ADC.Architecture.Tests/GlobalUsings.cs:1). Through its anchor types it also depends on Result, BaseEntity<TIdentifierType>, EntityQueryService<TEntity, TEntityDTO, TIdentifierType>, ApplicationDbContext, and ApiControllerBase on the framework side, and on User, Event, UserSessionBookmark, UserNotificationExportItemDTO, their sibling DTOs, and the four IdentityModule / ConferenceModule / EngagementModule / NotificationModule entry points on the app side.
  • Concept introduced, the architecture map as data. NetArchTest works on Assembly objects, so a fitness function needs some way to know which assembly plays which role. Rather than hard-coding assembly names inside each rule, MMCA reifies the answer in one object: a flat list of (module, layer, assembly) triples. ArchitectureMapBase derives everything else from that list: the module-name set, per-layer projections, namespace derivation, and the module-isolation target sets (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:28-:72). The consequence is that adding a module to ADC is a three-line change here, after which roughly three dozen rules start covering it. [Rubric §3 - Clean Architecture] assesses whether layer boundaries are real rather than aspirational, and this is the machine-readable statement of those boundaries. [Rubric §7 - Microservices Readiness]: the map's Module grouping is what lets the isolation and extraction rules ask "could this module leave the process", which for ADC is not hypothetical, since every module already runs as its own service host.
  • Walkthrough
    • RepoToken => "MMCA.ADC" (AdcArchitectureMap.cs:16) is more load-bearing than it looks. The base composes namespaces from it ({RepoToken}.{Module}.{Segment}, ArchitectureMapBase.cs:63-:66, :98-:99), and the doc/config rules resolve the repository root by walking up from the test binary until they find {RepoToken}.slnx (ArchitectureMapBase.cs:79-:91), which is how DataResidencyTests, FormsConventionTests, and RawQueryableConventionTests read committed files regardless of the runner's working directory.
    • Framework layers (AdcArchitectureMap.cs:21-:25) are declared with the Framework(layer, assembly) helper, which records an empty module name (ArchitectureMapBase.cs:94-:95), so they are excluded from the per-module projections. Each is pinned by an anchor type rather than a string: Result for Shared, BaseEntity<> for Domain, EntityQueryService<,,> for Application, ApplicationDbContext for Infrastructure, and ApiControllerBase for API. A rename or a package move breaks the compile instead of silently producing an empty scan.
    • Module layers (:28-:54) use the instance Module(name, layer, assembly) helper. Identity, Conference, and Engagement each declare six layers; Domain, Shared, and API are pinned by anchor type (Identity.Domain.Users.User, Conference.Shared.Events.EventDTO, Engagement.API.EngagementModule, and their siblings) while Application, Infrastructure, and UI are loaded by name through Assembly.Load (for example :29, :30, :33), because those assemblies have no convenient public anchor type (:5-:6). Assembly.Load succeeds here only because the csproj takes a ProjectReference on all 21 module projects (MMCA.ADC.Architecture.Tests.csproj:46-:71), which is what puts the DLLs beside the test binary.
    • Notification (:52-:54) declares three layers, not six: Application (loaded by name), Shared (pinned by Notification.Shared.UserNotifications.UserNotificationExportItemDTO), and Api (pinned by Notification.API.NotificationModule). The class comment states why (:7-:12): Notification is a deliberately thin module that owns no aggregate and no persistence, so it ships no Domain, Infrastructure, or UI assembly, and LayerDependencyTests records that shape as a per-module required-layer override rather than letting the whole repo drop the requirement.
    • Laziness is inherited: DefineLayers() is materialized once through a Lazy<IReadOnlyList<LayerRef>> built in the base constructor (ArchitectureMapBase.cs:13-:16), so the three dozen subclasses that each construct their own map instance still pay the Assembly.Load cost only on first use.
  • Why it's built this way - centralizing every namespace and assembly string in one file also fixes Ubuntu CI case sensitivity in one place, which the base states as an explicit goal (ArchitectureMapBase.cs:8-:9). Compare CommonArchitectureMap, the same abstraction for a repo with no business modules.
  • Where it's used - instantiated as a field initializer by every map-driven subclass in this unit (34 of the 43 types here, all of them at Level 13), for example ConcurrencyConventionTests.cs:5. It is internal, so it never leaves this assembly. The nine non-map types are the two embedded-resource guards, AnonymousEndpointTests, ProtoContractTests, SortableColumnConventionTests, TranslationCompletenessTests, the two pipeline-order tests, and the map itself.
  • Caveats / not-in-source - the four *.Contracts service projects under Source/Services/ are not registered, so no map-driven rule scans the gRPC adapters directly; ProtoContractTests covers that surface by file path instead. Nothing in this repository asserts that the map lists every module that exists, so a fifth module would have to be added here by a human.

MiddlewarePipelineOrderTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Api · MMCA.ADC.Architecture.Tests/Api/MiddlewarePipelineOrderTests.cs:16 · Level 12 · class (public, sealed)

  • What it is - the HTTP-edge counterpart of DecoratorPipelineOrderTests, and, like ObservabilityConventionTests, a bodyless declaration: public sealed class MiddlewarePipelineOrderTests : MiddlewarePipelineOrderTestsBase; (MMCA.ADC.Architecture.Tests/Api/MiddlewarePipelineOrderTests.cs:16). Deriving it is the whole assertion.
  • Depends on - MiddlewarePipelineOrderTestsBase from the MMCA.Common.Testing package (using MMCA.Common.Testing;, :1), and transitively on MiddlewarePipelineBuilder and MiddlewarePipelineStepNames.
  • Concept introduced, an empty subclass as a conformance claim. The class comment states what the emptiness means (:5-:14): every ADC REST and gRPC service host calls the zero-argument UseCommonMiddlewarePipeline(), so the framework's default step order is ADC's contract and the base needs no overrides. The two hooks that exist are the escape hatch a host would use if it customized the pipeline: Configure, which defaults to null (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:35), and ExpectedStepNames (:38-:58). Leaving both alone is a positive statement, not an omission. [Rubric §12 - Performance & Scalability] assesses whether cross-cutting behavior is applied uniformly rather than per host; [Rubric §11 - Security] is the sharp edge, because the ordering invariants below are authentication and rate-limiting invariants.
  • Walkthrough - no members. Two inherited facts run against a builder seeded from MiddlewarePipelineBuilder.CreateDefault() (MiddlewarePipelineOrderTestsBase.cs:79-:84).
    • EdgePipeline_OrdersSteps_InDocumentedOrder (:61) compares builder.StepNames against the 18-step default sequence, outermost first: exception handler, correlation id, request localization, pre-forwarded capture, forwarded headers, HTTPS redirection, response compression, routing, CORS, authentication, tenant resolution, rate limiting, soft-deleted-user filter, authorization, output cache, JWKS endpoint, OIDC discovery endpoint, controllers (:40-:57).
    • EdgePipeline_SatisfiesLoadBearingInvariants (:70) calls Build() and requires it not to throw, because Build() re-checks the load-bearing adjacencies at startup, so a pipeline that failed here would have thrown while the host was starting (:73-:76).
    • Four adjacencies are named as load-bearing in both the base and the ADC comment: the pre-forwarded capture immediately before the forwarded-headers rewrite (or jwks_uri stops being reachable), authentication immediately before tenant resolution (so the claim strategy sees HttpContext.User), authentication before the rate limiter per ADR-019 (so the per-user cap engages), and forwarded headers before the HTTPS redirect (MiddlewarePipelineOrderTests.cs:10-:13, MiddlewarePipelineOrderTestsBase.cs:66).
  • Why it's built this way - a reorder here fails at runtime in ways that look like configuration bugs: an unreachable jwks_uri, a tenant that never resolves, a per-user rate cap that never engages (MiddlewarePipelineOrderTestsBase.cs:16-:18). Making it a red test in the consumer repo is what turns a framework-side reorder into a build failure rather than a silent production behavior change (ADR-079). No WebApplication is built: the steps are pure data until they are applied, so this runs in the fast unit tier with no database and no host (:24-:27).
  • Caveats / not-in-source - the test asserts the framework default, seeded from CreateDefault(). It does not read any ADC Program.cs, so the claim that every ADC host calls the zero-argument overload is asserted by the comment, not by this test. A host that started passing a customization would silently fall outside this gate unless someone also overrode Configure here.

CascadeSoftDeleteConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:12 · Level 13 · class (public, sealed)

  • What it is - the guard that an aggregate root which owns auditable children deletes them in its own Delete() override, carrying exactly one reviewed exemption (MMCA.ADC.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:17-:26).
  • Depends on - CascadeSoftDeleteConventionTestsBase and AdcArchitectureMap.
  • Concept introduced, the consequence of soft delete not being a delete. MMCA never removes a row: an entity sets IsDeleted = true and the EF global query filter hides it. That means nothing cascades for free, because a soft delete is an ordinary UPDATE. Without an explicit cascade the root vanishes behind the filter while its child rows stay active, orphaned and unreachable through the root, yet still present for exports, reports, and erasure requests (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/CascadeSoftDeleteConventionTestsBase.cs:4-:9). The framework's DeleteChildren<TChild, TChildId>(...) helper is the one-line fix, and a hand-rolled loop over the children counts too. [Rubric §8 - Data Architecture] assesses whether the persistence model's invariants hold end to end; [Rubric §30 - Compliance/Privacy/Data Governance] is the sharper edge, since an orphaned child row is data a subject-access or erasure request has to account for (ADR-005).
  • Walkthrough - two members. Map (:14), and AllowedCascadeExemptTypes (:17-:26), which overrides the base's empty default (CascadeSoftDeleteConventionTestsBase.cs:27) with a single entry, MMCA.ADC.Conference.Domain.Speakers.Speaker. The inherited fact is AggregatesWithChildCollections_MustCascadeSoftDelete_InDelete (:30).
  • Why it's built this way - the exemption is justified in place rather than in a wiki (CascadeSoftDeleteConventionTests.cs:19-:24): Speaker's junction rows (SpeakerCategoryItems, SpeakerQuestionAnswers) deliberately outlive the soft-deleted root per BR-70/BR-71, because the Sessionize import reactivates them in place when the speaker returns (BR-135) and no cascade-restore counterpart exists, so cascading here would silently drop a returning speaker's categories and answers. The safety argument is the second half: junction reads follow the parent's visibility (BR-132), so the surviving children are not observable while the speaker is deleted. The base frames the whole exemption list this way, as the point of the rule rather than a concession: it turns "delete cascades, mostly" into a reviewed inventory of every aggregate that leaves children behind on purpose (CascadeSoftDeleteConventionTestsBase.cs:11-:15).
  • Caveats / not-in-source - the entry is a type full name, so the exemption is matched by string; the reactivation behavior it depends on (BR-135) is asserted by the Sessionize import tests, not here.

CommandValidatorCoverageTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/CommandValidatorCoverageTests.cs:18 · Level 13 · class (public, sealed)

  • What it is - the gate that every data-carrying command has something for the Validating decorator to run. It freezes the 23 entries that were exempt when the rule was wired, and fails on any new one (MMCA.ADC.Architecture.Tests/Cqrs/CommandValidatorCoverageTests.cs:23-:79).
  • Depends on - CommandValidatorCoverageTestsBase and AdcArchitectureMap.
  • Concept introduced, the allowlist grouped by reason rather than by name. The list is not alphabetical: it is split into three commented buckets, because the entries are not equally benign and a reviewer needs to see which kind an entry is (:10-:15). That is the difference between an inventory and a mute button. [Rubric §6 - CQRS & Event-Driven] assesses whether the pipeline stages actually do work; the Validating decorator is present for every command whether or not a validator resolves, so a missing validator is a stage that runs and asserts nothing. [Rubric §11 - Security] and [Rubric §24 - Forms/Validation/UX Safety]: the base's framing is that a command with no validator carries whatever the caller sent straight into the handler, and the gap is invisible until bad input reaches the domain (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/CommandValidatorCoverageTestsBase.cs:4-:7).
  • Walkthrough - three members.
    • Map (:20).
    • AllowedUnvalidatedCommands (:23-:79) overrides the base's empty default (CommandValidatorCoverageTestsBase.cs:31) with 23 entries in three groups. (1) Identifier-only commands (:25-:60), twenty-one entries whose whole payload is a server-supplied or route-supplied id plus, in a few cases, a RowVersion or an enum the compiler already constrains: sixteen remove/publish/link commands across Conference and Engagement (:31-:46), ModerateQuestionCommand with its own note that every field is an id or type-constrained and the authorization decision is the handler's (:48-:50), SetLeaderboardParticipationRequest because "there is no invalid value of a bool" (:52-:53), two Identity deletes (:55-:56), and the framework's generic MMCA.Common.Application.UseCases.Crud.DeleteEntityCommand closed over ADC aggregates, which ADC does not author (:58-:60). (2) False positives of the scan (:62-:70): ForgotPasswordCommand and ResetPasswordCommand both carry an ICommandWithRequest<T> whose IValidator<T> is real but lives in MMCA.Common.Application, and the rule reads only the repo's per-module Application assemblies, so it cannot see the framework half of the bridge. The comment ends with an instruction to a future maintainer: these two are validated at runtime, do not "fix" them by writing a duplicate. (3) Frozen debt (:72-:78): empty, and the comment records what left it (the two question-answer updates, ChangePreferences, and SetUserAvatar each now carry a validator in their own slice).
    • MinimumCommands => 55 (:86) raises the base floor of 1 (CommandValidatorCoverageTestsBase.cs:38). The comment states the measurement it sits under: the scan found 62 handled, data-carrying commands at adoption, so the floor leaves room for normal churn while still failing loudly if a module drops out of the map (:81-:85).
    • Two facts are inherited: Commands_ShouldHave_ValidationCoverage (CommandValidatorCoverageTestsBase.cs:41) and the non-vacuity check CommandInventory_ShouldNotBe_Empty (:45).
  • Why it's built this way - the base explains why both halves of the request bridge are required for a command to count as covered: the framework registers the bridge validator for every ICommandWithRequest<TRequest> whether or not a request validator resolves, and one that resolves none adds no rules (CommandValidatorCoverageTestsBase.cs:9-:15). Counting the bridge without the request validator would have made the gate pass on exactly the commands it exists to catch.
  • Caveats / not-in-source - group (2) is a stated limitation of the scan rather than a property of ADC, so an ADC command that legitimately binds a framework request validator in future would land in the same bucket and would need the same hand-written note.

ConcurrencyConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the guard that no *UpdateRequest carries a concurrency token in its body. It is also the plainest example of the Level 13 shape in this unit: a sealed class whose entire body is one line supplying the map (MMCA.ADC.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:5).
  • Depends on - ConcurrencyConventionTestsBase and AdcArchitectureMap.
  • Concept introduced, the map-only subclass. Eighteen of the 34 map-driven types in this unit are exactly this: protected override IArchitectureMap Map { get; } = new AdcArchitectureMap(); and nothing else. Note the property is an auto-property with an initializer, not an expression body, so each class constructs its map once per test-class instance rather than per fact. Everything else (the rule bodies, the [Fact] attributes, the failure messages) is inherited, which is precisely the point: MMCA.Common, MMCA.Store, and MMCA.ADC run byte-identical rule logic and differ only in what they point it at. The sections below for the other map-only subclasses do not repeat this explanation; they name the base and list what it asserts. [Rubric §15 - Best Practices & Code Quality] assesses duplication and change cost: a new rule ships to all three repos by adding a base and one derived line per repo.
  • Walkthrough - one member, Map (:5), implementing the base's abstract property (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ConcurrencyConventionTestsBase.cs:11). The inherited fact is UpdateRequests_ShouldNotImplement_IConcurrencyAware (:14), which delegates to ArchitectureRules.UpdateRequestsAreNotConcurrencyAware(Map). [Rubric §8 - Data Architecture] and [Rubric §9 - API & Contract Design]: the rule enforces a single source for the token. The version a conditional update is checked against is read from the If-Match request header, so a token in the request body would give the same check a second, competing source (ConcurrencyConventionTestsBase.cs:4-:7). That is the inverse of what the same-named rule asserted before the header became the only path, and it is why IConcurrencyAware must now be absent from request bodies rather than present. The base notes that modules with no mutable aggregate are legitimately vacuous here (:7).
  • Where it's used - runs with the whole suite in the build-and-test job (MMCA.ADC/.github/workflows/deploy.yml:189, :284). The same is true of every remaining type in this unit and is not repeated below.

ConstructorDependencyCountTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/ConstructorDependencyCountTests.cs:20 · Level 13 · class (public, sealed)

  • What it is - a single-responsibility ceiling: no *Service in a mapped module Application assembly may take more than nine constructor dependencies (MMCA.ADC.Architecture.Tests/Cqrs/ConstructorDependencyCountTests.cs:22-:24).
  • Depends on - ConstructorDependencyCountTestsBase and AdcArchitectureMap.
  • Concept introduced, the ratchet set to the real high-water mark, and its history kept in the file. A ceiling is only a ceiling if it sits at the current maximum, so the class comment keeps a ledger of every move (:9-:18), on the stated principle that "a ceiling only means something if every move of it was deliberate". It was 8 while the comment claimed AuthenticationService had 8 dependencies; it had 7, so the gate carried a phantom slot of headroom and was tightened to the real mark on 2026-07-28. It moved to 9 when refresh tokens became multi-device sessions: the framework's AuthenticationServiceBase constructor took on the session store and its options, and ADC's subclass passes both through on top of the IExternalLoginEmailVerifier its OAuth auto-link gate needs. The comment then makes the judgement explicit: both additions are framework-imposed collaborators of the same facade rather than a second responsibility, so the honest response is a raised ceiling and not an artificial bundle. [Rubric §1 - SOLID] assesses single responsibility among other things; constructor arity is the cheapest mechanical proxy for a class that has accumulated too many jobs, and the comment is careful to distinguish a cohesive facade from a bundle.
  • Walkthrough - two members. Map (:22), and MaxConstructorDependencies => 9 (:24), which implements the base's abstract ceiling (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:22). The inherited fact is ApplicationServices_DoNotExceedConstructorDependencyCeiling (:25), which scans every non-abstract class whose name ends in "Service" in the map's module Application assemblies (:27-:31), asserts the set is non-empty so the guard cannot pass vacuously (:33-:34), and reports each offender with its own arity (:36-:53). The subclass comment names the two types at or near the mark today, AuthenticationService at 9 and CreateSessionHandler next at 7 (ConstructorDependencyCountTests.cs:6-:8).
  • Why it's built this way - the ceiling is meant to be raised consciously rather than drifted past, which is what the comment asks for and what its own history demonstrates.
  • Caveats / not-in-source - the sibling rule HandlerConventionTests raises the base's separate arity check to the same 9 (HandlerConventionTests.cs:18), and its <remarks> states the coupling directly: the two guards must move together, or the stricter one turns the other into a comment (:11-:12). Note the scan here is name-based (*Service), so a handler or a builder with the same arity is outside this particular gate.

ContractImplementationTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Contracts · MMCA.ADC.Architecture.Tests/Contracts/ContractImplementationTests.cs:19 · Level 13 · class (public, sealed)

  • What it is - the encapsulation half of the [ServiceContract] boundary: the concrete class serving a published contract interface must not be public. Map-only shape (ConcurrencyConventionTests): the body is one line (MMCA.ADC.Architecture.Tests/Contracts/ContractImplementationTests.cs:21).
  • Depends on - ContractImplementationTestsBase and AdcArchitectureMap.
  • Concept introduced, guarding a boundary from both sides. This rule is the twin of ServiceContractPurityTests, and the base says so in as many words: purity keeps the producer's internals out of the contract, this keeps the contract's implementation out of the consumer's reach (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ContractImplementationTestsBase.cs:11-:13). A public implementation lets a consumer reference, construct, or subclass the type, and each of those references is a coupling the extraction has to sever later, because an interface can be answered over a wire and a class cannot (:5-:8). [Rubric §7 - Microservices Readiness] assesses whether extraction is reversible in practice; [Rubric §1 - SOLID]: this is dependency inversion enforced at the assembly boundary rather than only at the call site (ADR-007).
  • Walkthrough - one member, Map (:21), implementing the base's abstract property (ContractImplementationTestsBase.cs:22). One inherited fact, ServiceContractImplementations_ShouldNotBe_Public (:33), and the AllowedPublicImplementations hook (:30) is left at its empty default. The subclass documentation is where the real teaching is (ContractImplementationTests.cs:3-:18): ADC's six contracts (IAttendeeQueryService, IBookmarkCountService, IEventLiveValidationService, ISessionBookmarkValidationService, IUserEngagementExportService, IUserNotificationExportService) each have three implementations, the in-process one in *.Application, the Disabled* stub in *.Shared, and the gRPC adapter in Services/*.Contracts, and all of them are internal sealed. The <remarks> explains why the allowlist stays empty: nothing in ADC ships a contract implementation a consumer is meant to construct, and the assemblies that register or exercise these classes reach them through InternalsVisibleTo on the producing projects rather than through a public type (:14-:18).
  • Caveats / not-in-source - the rule is scoped to the assemblies AdcArchitectureMap registers, and the Services/*.Contracts projects are not among them, so the gRPC adapters' internal visibility is asserted by the class comment rather than by this test.

ControllerConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Api · MMCA.ADC.Architecture.Tests/Api/ControllerConventionTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the API-layer convention guard, with a two-entry exemption list for the controllers that legitimately do not route through the framework's base controller (MMCA.ADC.Architecture.Tests/Api/ControllerConventionTests.cs:11-:15).
  • Depends on - ControllerConventionTestsBase, AdcArchitectureMap, and, by name only (they are strings, not type references), OAuthController and ServiceInfoController.
  • Concept introduced, the documented exemption. A rule with no escape hatch gets deleted the first time reality disagrees with it; a rule with an undocumented escape hatch rots. The middle path here is an allowlist of fully qualified names, each justified in the comment above it (:7-:10): the OAuth controller drives a redirect, challenge, and cookie flow with an out-of-band token exchange, and the service-info controller is an anonymous version-discovery diagnostic, so neither returns a domain Result and neither has anything to gain from the Result-to-HTTP mapping in ApiControllerBase. [Rubric §9 - API & Contract Design] assesses consistency of the HTTP surface; the rule keeps the default consistent while naming the two deliberate outliers.
  • Walkthrough - two members. Map (:5) and ControllersExemptFromApiControllerBase (:11-:15), which overrides the base's empty default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/ControllerConventionTestsBase.cs:12). Four facts are inherited: controllers do not depend on Infrastructure (:15), do not depend on EF Core (:18), are sealed (:21), and inherit ApiControllerBase except for the exempt names (:24).
  • Caveats / not-in-source - the exemptions are matched as strings, so renaming or moving either controller silently drops it from the list and turns the rule red rather than passing wrongly, which is the safe direction.

DataResidencyTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/DataResidencyTests.cs:12 · Level 13 · class (public, sealed)

  • What it is - a compliance drift guard: the data-residency statement published in ADC's PRIVACY.md must match the Azure region where personal data is actually provisioned, parsed out of the deployment workflow (MMCA.ADC.Architecture.Tests/Governance/DataResidencyTests.cs:3-:11).
  • Depends on - DataResidencyTestsBase, AdcArchitectureMap, System.IO.File/Path, and AwesomeAssertions (used directly inside the override, :26).
  • Concept introduced, a test as the join between a document and an infrastructure fact. Most of the rules in this unit compare code to code. This one compares prose to infrastructure: it reads the deployed region out of the source of truth (SQL_LOCATION="${SQL_LOCATION_OVERRIDE:-westus2}", MMCA.ADC/.github/workflows/deploy.yml:1158) and then requires PRIVACY.md to say the same thing, comparing whitespace-insensitively and case-insensitively so "West US 2" matches the westus2 region token. [Rubric §30 - Compliance/Privacy/Data Governance] assesses whether privacy claims are true and stay true; a policy that names a region the data never lived in is a compliance defect that no code review would catch, and this is the mechanism that closes it.
  • Walkthrough - three members.
    • Map (:14) exists only so the base can resolve the repo root through ArchitectureMapBase.FindRepoRoot($"{Map.RepoToken}.slnx"); no assembly scanning happens in this rule.
    • ForbiddenResidencyClaims => ["central United States"] (:16) overrides the base's empty default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DataResidencyTestsBase.cs:23) and blocks a specific stale statement from returning, one that once contradicted the deployed region (DataResidencyTests.cs:9-:10).
    • ExtractDeployedRegion(string repoRoot) (:20-:31) implements the base's abstract hook (DataResidencyTestsBase.cs:53). It reads .github/workflows/deploy.yml (:22), locates the literal marker SQL_LOCATION_OVERRIDE:- with an ordinal IndexOf (:24-:25), asserts the marker exists with a because explaining what the workflow must declare (:26-:27), then takes the alphanumeric run that follows as the region (:29-:30). Assert-then-parse rather than return-empty is what the base asks implementations to do.
    • The inherited fact PrivacyPolicy_DataStorageRegion_MatchesDeployedRegion (DataResidencyTestsBase.cs:26) then asserts the normalized policy contains the normalized region and contains none of the forbidden claims.
  • Why it's built this way - the account data and session bookmarks live in the Azure SQL database, and the QiMata Sponsorship subscription forces that SQL server into a different region from the Container Apps (DataResidencyTests.cs:5-:9), so "where the app runs" is genuinely not "where the personal data sits". Parsing the SQL region default rather than the app region encodes that distinction.
  • Caveats / not-in-source - the parse is positional: it takes the first occurrence of the marker in deploy.yml. The workflow contains both a job-level SQL_LOCATION_OVERRIDE env binding (deploy.yml:1111) and the shell default (:1158), so the rule depends on the marker string SQL_LOCATION_OVERRIDE:- appearing only in the latter form.

DomainEventHandlerSaveTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/DomainEventHandlerSaveTests.cs:11 · Level 13 · class (public, sealed)

  • What it is - the rule that a domain-event handler mutates state and lets the owning unit of work flush it, rather than persisting on its own. It carries two allowlisted entries (MMCA.ADC.Architecture.Tests/Cqrs/DomainEventHandlerSaveTests.cs:16-:28).
  • Depends on - DomainEventHandlerSaveTestsBase and AdcArchitectureMap.
  • Concept introduced, the transitive call-graph rule. Every other assembly-scanning rule in this unit judges a type by its own declarations. This one walks the call graph out of every handler method, so the common real shape (handler to a domain service to SaveChangesAsync) is caught rather than only a save typed directly into the handler (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/DomainEventHandlerSaveTestsBase.cs:10-:14). The walk depth is bounded by MaxCallDepth, defaulted to 6, which the base describes as covering the realistic handler-to-service-to-helper-to-repository chain while keeping the scan fast (:35-:40); ADC does not override it. [Rubric §6 - CQRS & Event-Driven] assesses the discipline around dispatch; [Rubric §8 - Data Architecture]: dispatch runs after SaveChangesAsync, and after commit inside an ITransactional command, so a handler that saves opens a second write in the middle of the first one, re-entering the change tracker, potentially raising a fresh event cascade, and persisting work the outer transaction may still roll back (:4-:9).
  • Walkthrough - two members. Map (:13), and AllowedSavingTypes (:16-:28), which overrides the base's default (DomainEventHandlerSaveTestsBase.cs:33) with two entries. The first, "MMCA.Common" (:20), is the base's own default carried forward: the framework's outbox event bus persists by design, and a handler publishing an integration event is not the defect the rule hunts (:18-:19). The second is PointsAwarder (:27), labelled an accepted deliberate trade-off: the gamification points ledger is written by the awarder that the point-scoring domain-event handlers call, so awarding points is a second write that follows the first, and the comment states plainly that the flow is intentional and is not to be refactored onto the outbox, because a point award that lost its triggering write would be worse than one that trails it (:22-:26). The inherited fact is DomainEventHandlers_ShouldNotReach_SaveChanges (DomainEventHandlerSaveTestsBase.cs:43).
  • Why it's built this way - an allowlist entry both silences the type and stops the walk from descending into it (DomainEventHandlerSaveTestsBase.cs:16-:19), which is why the class comment insists the two accepted ADC cascades are named explicitly rather than silenced module-wide (DomainEventHandlerSaveTests.cs:8-:9): a namespace-wide entry would have hidden whatever lies beyond it too.
  • Caveats / not-in-source - the depth bound is 6, so a save reached through a deeper chain is outside the walk. The base names that as a tuning knob rather than a guarantee.

DomainPurityTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Layering · MMCA.ADC.Architecture.Tests/Layering/DomainPurityTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the Clean Architecture purity guard, plus one repo-specific addition: RabbitMQ is added to the forbidden-dependency list for Domain and Shared (MMCA.ADC.Architecture.Tests/Layering/DomainPurityTests.cs:9).
  • Depends on - DomainPurityTestsBase and AdcArchitectureMap.
  • Concept introduced - the map-only shape from ConcurrencyConventionTests, extended by a one-line hook. The extra entry is not decoration: ADC runs on a broker (RabbitMQ locally, Azure Service Bus in production, :7-:8), and a broker client reference inside Domain would tie the model to a transport. [Rubric §3 - Clean Architecture] assesses inward-only dependencies; [Rubric §7 - Microservices Readiness]: keeping the transport out of the core is what makes IMessageBus substitutable between in-process and broker delivery.
  • Walkthrough - two members: Map (:5) and ExtraForbiddenDomainDependencies => ["RabbitMQ"] (:9), overriding the base's empty default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/DomainPurityTestsBase.cs:12). Four facts are inherited: Domain is framework-free (:15), Shared is framework-free (:18), Application does not depend on EF Core (:21), and Application does not depend on ASP.NET Core (:24). The extra token is passed to the first two only.

DomainThrowTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC.Architecture.Tests/Domain/DomainThrowTests.cs:9 · Level 13 · class (public, sealed)

  • What it is - the Result-pattern purity guard: no throw in ADC's Domain assemblies except an argument guard. The allowlist is present and empty (MMCA.ADC.Architecture.Tests/Domain/DomainThrowTests.cs:14-:17).
  • Depends on - DomainThrowTestsBase and AdcArchitectureMap.
  • Concept introduced, the cost of a thrown business failure. The framework's whole error model is that a business outcome comes back as a Result (ADR-013), and the base names the three concrete consequences of throwing one instead: it skips Result.Combine invariant composition, it arrives at the API as a 500 rather than an outcome the caller can act on, and it pays an exception unwind on a path that is not exceptional (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/DomainThrowTestsBase.cs:4-:9). Argument guards stay allowed because they report a caller bug rather than a business outcome, and the modern guard style always passes anyway: a bare throw; rethrow is ignored, and the ArgumentNullException.ThrowIfNull family emits no throw in the caller at all (:11-:12). [Rubric §15 - Best Practices & Code Quality] assesses whether a stated convention actually holds; [Rubric §9 - API & Contract Design]: which failures a caller can handle is part of the contract.
  • Walkthrough - two members. Map (:11), and AllowedThrowingTypes (:14-:17), which overrides the base's empty default (DomainThrowTestsBase.cs:31) with a collection expression containing only a comment, TRIAGE PLACEHOLDER: filled from the first run (DomainThrowTests.cs:16). Since the list is empty and the rule is in the CI suite, that comment is now a historical note rather than an open item: ADC's Domain assemblies throw nothing beyond the argument guards, or the gate would be red. The inherited fact is Domain_ShouldNotThrow_ExceptArgumentGuards (DomainThrowTestsBase.cs:34).
  • Why it's built this way - the base spells out the adoption path the placeholder anticipated (:14-:19): subclass, run once, then either convert each reported site to a Result.Failure or move it into the list with a comment. ADC took the first branch everywhere, which is why the list stayed empty.
  • Caveats / not-in-source - the exception filter is by type (ArgumentException, ArgumentNullException, ArgumentOutOfRangeException), so a custom guard exception outside that family would be reported.

EntityConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC.Architecture.Tests/Domain/EntityConventionTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the DDD entity-shape guard for ADC's three module domains: aggregate roots exist, each has a Result-returning static factory and no public constructor, domain entities are sealed and live in the Domain layer, entity properties carry no public setters, and DTOs or requests do not live in Domain or Infrastructure.
  • Depends on - EntityConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:5). Eight inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/EntityConventionTestsBase.cs:15, :18, :21, :24, :27, :30, :33, :36) cover aggregate-root exposure, the Result-returning Create factory, constructor visibility, factory return types, sealing, layer placement, non-public property setters, and the DTO/request exclusion. [Rubric §4 - DDD] assesses whether the tactical patterns are actually applied; this is the rule that keeps the factory-plus-Result construction contract from being optional in a new module, and the setter check is what keeps an entity's state from being reachable without going through a behavior method.

ErrorCatalogTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Contracts · MMCA.ADC.Architecture.Tests/Contracts/ErrorCatalogTests.cs:20 · Level 13 · class (public, sealed)

  • What it is - the guard over ADC's error-code vocabulary: one literal code means one thing, and a code carries the prefix of the vocabulary that owns it. It is the second-largest subclass in the unit, at 107 lines, and almost all of it is justification (MMCA.ADC.Architecture.Tests/Contracts/ErrorCatalogTests.cs:28-:58, :64-:80).
  • Depends on - ErrorCatalogTestsBase and AdcArchitectureMap.
  • Concept introduced, treating error codes as a public contract. The class comment states the constraint that shapes every decision in this file (:12-:18): error codes reach clients in problem-details payloads and get quoted in support tickets, so nothing already shipped is renamed to satisfy the rule. Today's catalog is locked exactly as it stands and only new drift is gated. [Rubric §9 - API & Contract Design] assesses whether the error surface is governed like the rest of the API; [Rubric §15 - Best Practices & Code Quality]: a vocabulary in which one code means two things is a vocabulary a client cannot switch on.
  • Walkthrough - three members plus three inherited facts.
    • Map (:22).
    • AllowedCodePrefixes (:28-:58) overrides the base's default of the map's module names (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ErrorCatalogTestsBase.cs:29) because ADC namespaces its codes by aggregate (Session.Room.DoubleBooked) rather than by module. It spreads Map.ModuleNames (:30) and then adds 21 literal aggregate prefixes in three commented groups: nine Conference aggregates and children (:33-:41), nine Engagement ones (:44-:52), and three Identity ones (:55-:57). The comment frames the list as an inventory: a code under a new prefix fails until the prefix is reviewed and added, which is the drift the gate exists to catch (:8-:10).
    • AllowedSharedCodes (:64-:80) spreads the base's three generic statics (ErrorCatalogTestsBase.cs:35-:36) and freezes seven ADC codes that two types both construct today (:73-:79). Each carries an inline note naming the pair, for example CheckIns.EventNotPublished reached from CheckInProcessor, RecordRoomCheckIn and RecordSponsorVisit, and SessionQuestion.InvalidTransition reached from ModerateQuestionHandler and the SessionQuestion aggregate. The reasoning is stated once above them (:68-:72): each pair is the same failure reported at two entry points, so the sharing is the correct behavior, a client switching on the code wants one answer rather than three near-identical ones, and renaming them would break that client. The rule is therefore told the sharing is deliberate rather than told to look away.
    • MinimumErrorCodes => 57 (:106) raises the base floor of 1 (ErrorCatalogTestsBase.cs:43), and its documentation is the most instructive passage in the file (ErrorCatalogTests.cs:82-:105). The catalog held 73 distinct non-generic codes at adoption; the floor has since been lowered twice, and both times for the same non-alarming cause: a code stopped being a literal the IL scan can see, because it moved from an Error factory call in an ADC module into an argument of a framework member that builds the Error inside MMCA.Common. The comment records each move (70 to 66 when five X.NotDeleted codes moved into RestoreChild(notDeletedErrorCode: ...) arguments; 66 to 57 when eight more moved into ICurrentUserService.RequireUserId(code, ...) and CommonInvariants members), notes that the codes themselves ship unchanged, and states that the scanned count after the change is 60, measured from a run rather than modelled.
    • The three inherited facts are ErrorCodes_ShouldBe_Unique (ErrorCatalogTestsBase.cs:46), ErrorCodes_ShouldCarry_TheOwningModulePrefix (:50), and the non-vacuity check ErrorCodeCatalog_ShouldNotBe_Empty (:54). Prefix matching is prefix + ".", ordinal (:62-:67).
  • Why it's built this way - codes are read out of IL at the Error factory call sites, so a code that is not a literal cannot be judged statically; the base lists such codes as UNVERIFIABLE in the failure message rather than passing or failing them (ErrorCatalogTestsBase.cs:9-:12). That design is exactly what makes the floor drift downward as code construction moves into framework arguments, and why the subclass documents each drop instead of quietly editing the number.
  • Caveats / not-in-source - a scanner follow-up that would also read framework-member code arguments is described in the comment as tracked in the Wave 6 plan (ErrorCatalogTests.cs:88-:89); nothing in this repository implements it today. Also note the floor of 57 now sits below a measured 60, so it is a margin rather than an exact count.

EventConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Contracts · MMCA.ADC.Architecture.Tests/Contracts/EventConventionTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the integration-event shape guard: every integration event declares a schema version, inherits the framework's base integration event, and lives in an *.IntegrationEvents namespace under Shared; and every event upcaster is unique and moves the version forward.
  • Depends on - EventConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:5). Five inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/EventConventionTestsBase.cs:14, :17, :20, :23, :26). The first three enforce SchemaVersion, base-type inheritance, and namespace placement (ADR-010). The last two guard the upcaster machinery that ADR-010 depends on: EventUpcasters_ShouldHave_UniqueSourceTypes (:23), so two upcasters cannot claim the same source shape, and EventUpcasters_ShouldIncrease_SchemaVersion (:26), so an upcaster cannot map an event onto the same or an earlier version. [Rubric §6 - CQRS & Event-Driven] assesses the discipline around asynchronous contracts; this rule handles the shape, while IntegrationEventContractTests freezes the content.

FormsConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests/Ui/FormsConventionTests.cs:17 · Level 13 · class (public, sealed)

  • What it is - the UX-safety guard over ADC's admin forms, and the largest subclass in the unit at 150 lines. It configures the shared rule for the eight Conference create forms, then adds two hand-written facts for the coverage that shared rule cannot reach (MMCA.ADC.Architecture.Tests/Ui/FormsConventionTests.cs:6-:15, :66-:103, :105-:136).
  • Depends on - FormsConventionTestsBase, AdcArchitectureMap, ArchitectureMapBase (called statically for the repo root, :69, :108), System.IO, System.Globalization, System.Text, and AwesomeAssertions.
  • Concept introduced, replacing a marker set when the property moved, and writing the coverage a shared rule cannot reach. Two mechanisms appear here. First, RequiredMarkers is a full replacement of the base list rather than an extension of it, and the long <summary> above it explains why (:23-:39): the base's Required="true" / RequiredError pair is deliberately not inherited, because every ADC create form now sources its field rules from a form model through the MMCA.Common.UI.Validation bridge, so requiredness is declared once as a [Required] attribute on the model and read back into the markup as ModelValidation.IsRequired. A form that still spelled the old pair on every field would be the regression, not the convention. Second, when a real surface falls outside the shared rule's reach, the subclass writes the missing coverage itself rather than loosening the shared rule. [Rubric §24 - Forms/Validation/UX Safety] assesses whether users are protected from losing work and from unclear validation; [Rubric §18 - UI Architecture] is the second axis, because the field extraction the second fact chases is a component-composition change.
  • Walkthrough
    • Map (:19) and MinimumCreateForms => 8 (:21), raising the base floor of 1 (FormsConventionTestsBase.cs:24) to ADC's real count: Activity, ConferenceCategory, Event, Question, Room, Session, Speaker, and Sponsor (FormsConventionTests.cs:8-:9).
    • RequiredMarkers (:40-:50) lists eight literals, each with an inline note: UnsavedChangesGuard, IsDirtyAccessor (bound through the live accessor, not the lagging parameter), _isDirty, <MudForm, Model="_model" (the form declares the model its rules come from), Validation="@_validate" (fields run the model's rules through the bridge), <ErrorSummary (the shared deduplicating summary from MMCA.Common.UI), and the localized heading key Validation.CorrectFollowing. The inherited fact that consumes them is AdminCreateForms_KeepUnsavedChangesGuardAndValidation (FormsConventionTestsBase.cs:38).
    • AdminCreateForms_ReadRequirednessOffTheirModel (:66-:103) is the requiredness half, and it exists because the field block itself became a shared component. It enumerates *Create.razor under Source/Modules, skipping obj and bin (:72-:77), re-asserts the MinimumCreateForms floor (:79-:80), and then, for each form, appends every sibling *FormFields.razor in the same directory into one scanned buffer before looking for ModelValidation.IsRequired (:85-:98). The <summary> states why that widening is not a weakening (:52-:64): every Conference create form now shares its field block with the matching detail page's inline editor so the two cannot drift apart, and a form that inlines its fields again is still covered, because its own markup is scanned first.
    • ProfileForm_KeepsErrorSummaryAndPasswordValidation (:105-:136) covers the one form the base glob cannot see. It builds the path to Source/Modules/Identity/MMCA.ADC.Identity.UI/Pages/Users/Profile/Profile.razor (:109-:110) and asserts the file exists first, with a because explaining that a form which is not discovered is a convention that is not verified (:112-:113). It then requires four markers (:117-:123): the error summary, Messages="_passwordForm?.Errors" (the summary fed from the live MudForm error list), and the ValidateNewPassword / ValidateConfirmPassword client-side wiring, reporting every missing marker at once rather than failing on the first (:125-:130). Finally it counts occurrences of Required="true" and RequiredError, requiring at least three of each so all three password fields stay required and keep a user-facing message (:132-:135), using the local CountOccurrences helper (:138-:149).
  • Why it's built this way - the Profile form is a single-section password and delete form with no navigate-away step, so it carries no unsaved-changes guard by design and does not match the base's *Create.razor glob (:11-:15); the base documents exactly that exclusion (FormsConventionTestsBase.cs:11-:13). Rather than weakening the shared rule to accommodate it, ADC asserts the markers that do apply.
  • Caveats / not-in-source - the Profile fact hardcodes one file path, so moving Profile.razor fails the test (again, the safe direction) but adding a second self-service form gains no coverage automatically. The two ADC-authored facts also duplicate the base's enumeration and floor logic rather than reusing it.

FrameworkVersionConsistencyTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/FrameworkVersionConsistencyTests.cs:9 · Level 13 · class (public, sealed)

  • What it is - the lockstep-versioning gate: every MMCA.Common.* package pinned in ADC's Directory.Packages.props must carry one and the same version, so a partial sweep fails CI instead of producing a subtly mismatched framework surface at runtime (MMCA.ADC.Architecture.Tests/Governance/FrameworkVersionConsistencyTests.cs:3-:8).
  • Depends on - FrameworkVersionConsistencyTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests); the map is used for its repo token, to find the props file from the repo root.
  • Concept introduced - a policy made executable. ADR-016 says consumers bump every MMCA.Common.* entry together, with no phased rollout. A policy that lives only in a document is enforced by memory; this rule enforces it by build. [Rubric §32 - Dependency & Supply-Chain] assesses how dependency versions are governed; [Rubric §15 - Best Practices & Code Quality]: a half-swept pin set is the kind of defect that surfaces as an unrelated runtime error weeks later.
  • Walkthrough - one member, Map (:11). The inherited fact is AllMmcaCommonPackages_ArePinnedToOneVersion (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FrameworkVersionConsistencyTestsBase.cs:25), and the base's non-vacuity floor MinimumCommonPackageCount stays at its default of 13 (:22). ADC currently pins 16 MMCA.Common.* packages in MMCA.ADC/Directory.Packages.props, so the floor is met with room to spare.

HandlerConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/HandlerConventionTests.cs:14 · Level 13 · class (public, sealed)

  • What it is - the CQRS handler placement and composition guard: handlers and validators live in the Application layer, handlers do not inject other handlers, application services do not inject handlers, domain event handlers are sealed and live in Application, and application services respect a constructor-arity limit that ADC raises to nine (MMCA.ADC.Architecture.Tests/Cqrs/HandlerConventionTests.cs:18).
  • Depends on - HandlerConventionTestsBase and AdcArchitectureMap.
  • Walkthrough - two members. Map (:16) and MaxServiceConstructorParameters => 9 (:18), raising the base default of 8 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerConventionTestsBase.cs:12). Six facts are inherited (:15, :18, :21, :24, :27, :30). [Rubric §6 - CQRS & Event-Driven] assesses whether the command/query split is structural rather than nominal; a handler that injects another handler is the classic way that split quietly becomes a call graph. [Rubric §1 - SOLID] covers the arity half.
  • Why it's built this way - the <remarks> (:6-:13) records both the reason for the raise and the coupling it creates. The shared AuthenticationServiceBase constructor took on the refresh-session store and its options, and ADC's subclass passes both through alongside the IExternalLoginEmailVerifier its OAuth auto-link gate needs; the rationale lives in ConstructorDependencyCountTests, which sets the same 9. The last sentence is the operative one: the two guards must move together, or the stricter one turns the other into a comment.
  • Caveats / not-in-source - ADC therefore has two overlapping arity limits with the same value but different scans (this one over application services generally, the sibling over *Service types in module Application assemblies). Whether they cover an identical type set is decided inside ArchitectureRules and is not determinable from these subclasses.

HandlerResultConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:8 · Level 13 · class (public, sealed)

  • What it is - the gate that turns a runtime constraint into a build-time one: every ADC command and query handler's TResult must be Result or Result<T> (MMCA.ADC.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:3-:7).
  • Depends on - HandlerResultConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Concept introduced - shifting a failure left. The decorator pipeline can short-circuit (a feature flag off, an authorization denial, a validation failure, a cache hit), and to do that it must manufacture a failed result of the handler's TResult; the comment names the mechanism, ResultFailureFactory (:5-:6). A handler returning a bare DTO therefore compiles and registers cleanly and only explodes the first time a short-circuit fires in production. [Rubric §6 - CQRS & Event-Driven] and [Rubric §14 - Testability]: an invariant the type system cannot express is exactly what a fitness function is for.
  • Walkthrough - one member, Map (:10). Three inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerResultConventionTestsBase.cs:21, :24, :27): the Application layers declare at least one handler (a non-vacuity check), command handlers return result types, and query handlers do too. Opt-in from v1.120.0 (HandlerResultConventionTests.cs:4).

IdempotencyConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the rule that every POST action in ADC's API layer states, in code, whether a retried request replays the original response or deliberately does not. Map-only shape (ConcurrencyConventionTests): the body is one line (MMCA.ADC.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:5).
  • Depends on - IdempotencyConventionTestsBase and AdcArchitectureMap, and, by attribute name, IdempotentAttribute and NonIdempotentAttribute.
  • Concept introduced, forcing a decision rather than a default. POST is the one verb HTTP does not define as idempotent, so a client retrying a timed-out POST cannot know whether the first attempt landed. The framework's answer is the Idempotency-Key filter, and it costs an existing client nothing, because the filter no-ops for a request that carries no key header. That makes the only failure mode worth gating an omission nobody noticed: an action that should replay but silently does not. The rule therefore does not demand [Idempotent]; it demands that the author write down which of the two applies, with [NonIdempotent("why")] carrying its own justification string (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:14-:18, :22-:28, :58-:60). [Rubric §9 - API & Contract Design] assesses retry semantics as part of the contract; [Rubric §29 - Resilience & Business Continuity]: a retry that double-writes is precisely the failure a resilience policy creates when the endpoint has not thought about it.
  • Walkthrough - one member, Map (:5), implementing the base's abstract property (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/IdempotencyConventionTestsBase.cs:12). One inherited fact, PostActions_ShouldDeclare_IdempotencyIntent (:15), delegating to ArchitectureRules.PostActionsDeclareIdempotencyIntent(Map) (ArchitectureRules.Idempotency.cs:44-:61), which walks the concrete controllers in every Layer.Api assembly the map declares (:48-:54). Attributes are read with inherit: true and abstract controller types are skipped, so an ADC controller that inherits a POST action from AuthControllerBase or AggregateRootEntityControllerBase already satisfies the rule through that base rather than needing its own attribute (:31-:35).
  • Why it's built this way - the base states the subclassing rule: derive it in a repo whose map declares an Api layer, and a repo with no API layer simply does not subclass (IdempotencyConventionTestsBase.cs:6-:8). ADC declares an Api layer for all four mapped modules, Notification included (AdcArchitectureMap.cs:54), so the gate is live across the whole REST surface.
  • Caveats / not-in-source - detection is by attribute type name, keeping the rule library free of an ASP.NET reference, and only [HttpPost] is recognised: an action routed through [AcceptVerbs("POST")] or a conventional route is out of scope (ArchitectureRules.Idempotency.cs:38-:41).

ImmutabilityTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC.Architecture.Tests/Domain/ImmutabilityTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the immutability guard across five categories of type: DTOs, commands and queries, domain events, integration events, and value objects (the last also required to be sealed and to live in Shared).
  • Depends on - ImmutabilityTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:5). Five inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ImmutabilityTestsBase.cs:13, :16, :19, :22, :25). [Rubric §15 - Best Practices & Code Quality] assesses whether the codebase holds its stated conventions; required/init immutability is a workspace-wide convention, and an event whose properties can be mutated after publication is a correctness hazard, not a style preference.

IntegrationEventContractTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Contracts · MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventContractTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the frozen wire contract for ADC's cross-service asynchronous API. It commits a seven-line snapshot of every integration event's full name and property shape, and the build fails if the live contract differs (MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventContractTests.cs:9-:20).
  • Depends on - IntegrationEventContractTestsBase and AdcArchitectureMap.
  • Concept introduced, the approval snapshot. The rules above check shape rules; this one checks identity. A consumer in another service deserializes by shape, so a renamed, removed, or retyped property (or a brand-new event shipped without a consumer) breaks the contract at runtime with no compile error anywhere. The base rebuilds the live contract from the map and asserts it against the committed list (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventContractTestsBase.cs:20-:26), so any change surfaces as a diff in this file that a reviewer must consciously accept. [Rubric §9 - API & Contract Design] assesses contract governance, and the asynchronous contract is as much an API as the REST surface; [Rubric §7 - Microservices Readiness]: with all four ADC modules already running as separate services, this list is the actual coupling between them. Its synchronous twin is ProtoContractTests.
  • Walkthrough - two members. Map (:5), and ExpectedContract (:9-:20), a collection expression of seven strings in FullName { Prop:Type, ... } form: EventFeedbackSubmitted and SessionFeedbackSubmitted from Conference, SpeakerLinkedToUser and SpeakerUnlinkedFromUser from Conference (the pair Identity consumes to set and clear the linked-speaker reference on the user), AttendeeCheckedIn from Engagement, and UserDeleted plus UserRegistered from Identity. The properties are listed in sorted order, which is how a rebuilt contract stays comparable line by line. The inherited fact is IntegrationEventContracts_ShouldMatch_TheFrozenSnapshot (IntegrationEventContractTestsBase.cs:26).
  • Why it's built this way - the comment states the rule of engagement (:7-:8): update the snapshot deliberately, and version the event or coordinate the consumer rollout in the same commit. The AttendeeCheckedIn entry carries its own inline justification (:15-:16): SponsorId is additive, optional, defaults to null, and is declared last precisely so a payload written before the sponsor scope existed still deserializes (confirmed in the event itself, MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/CheckIns/IntegrationEvents/AttendeeCheckedIn.cs:22, :31).
  • Caveats / not-in-source - the snapshot proves that the shape has not changed, not that any consumer actually handles it. Consumer-side behavior is exercised by the cross-service Testcontainers tier, not here.

LayerDependencyTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Layering · MMCA.ADC.Architecture.Tests/Layering/LayerDependencyTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the Clean Architecture layer-flow guard, and the highest-fact-count rule in the unit: fifteen inherited facts covering which layer may reference which, plus one override that records Notification's deliberately thin shape (MMCA.ADC.Architecture.Tests/Layering/LayerDependencyTests.cs:15-:19).
  • Depends on - LayerDependencyTestsBase and AdcArchitectureMap.
  • Concept introduced, the per-module exception recorded where it is true. Two of the fifteen facts are map-completeness checks: LayerMap_DeclaresEveryExpectedLayer and LayerMap_ModulesDeclareEveryExpectedLayer (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/LayerDependencyTestsBase.cs:53, :56) require the five core layers Shared, Domain, Application, Infrastructure, and Api to be declared, both overall and by every mapped module (:16-:17, :27). ADC has one module that legitimately cannot satisfy the module half, so it uses the narrow hook rather than the broad one. The base is explicit about why that matters (:22-:25): trimming RequiredModuleLayers would apply to every module, so one thin module would stop the rule from catching a forgotten assembly anywhere in the repo. [Rubric §3 - Clean Architecture] is the whole point of the rule; [Rubric §34 - Architecture Governance & Documentation]: the exception is written down, in code, at the one place it is true.
  • Walkthrough - two members. Map (:5), and ModuleRequiredLayerOverrides (:15-:19), which overrides the base's empty dictionary (LayerDependencyTestsBase.cs:49-:50) with one entry mapping "Notification" to [Layer.Shared, Layer.Application, Layer.Api]. Its <summary> states the justification (LayerDependencyTests.cs:7-:14): Notification owns no aggregate and no persistence, so there is no Domain and no Infrastructure assembly to declare, and it has no pages of its own so there is no UI assembly either; its API project composes MMCA.Common's notification infrastructure (the SignalR hub and sender) directly rather than wrapping it in an ADC Infrastructure layer. The remaining thirteen facts (LayerDependencyTestsBase.cs:60 through :96) assert the directed rules: Domain depends on neither Application, Infrastructure, nor Api; Application on neither Infrastructure nor Api; Infrastructure not on Api; Shared on nothing above it; and Ui on none of Domain, Application, or Infrastructure. Note the two-gate model described in MMCA.Common/CLAUDE.md: the same boundaries are enforced at compile time in the framework by MMCA.Common.LayerEnforcement.targets, and here at test time against compiled assemblies.

LocalizedTextConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:14 · Level 13 · class (public, sealed)

  • What it is - the companion to TranslationCompletenessTests. Where that one asks "is every key translated", this one asks "does every user-visible string go through a key at all": no hard-coded literals in .razor or .razor.cs under Source/ (MMCA.ADC.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:3-:13).
  • Depends on - LocalizedTextConventionTestsBase and AdcArchitectureMap.
  • Concept introduced - the per-line escape marker. Some literals genuinely should not be translated (the conference brand name, content data), so the rule exempts them with an i18n: allow comment on the offending line rather than an allowlist file (:9-:10). Keeping the exemption physically next to the literal is what makes it reviewable. [Rubric §27 - i18n], executing ADR-027.
  • Walkthrough - two members. Map (:16) and MinimumScannedFiles => 60 (:18), raising the base default of 1 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizedTextConventionTestsBase.cs:21); the comment sizes the floor against roughly 77 razor files across the three module UIs and the UI hosts (LocalizedTextConventionTests.cs:11-:12). The inherited fact is UserVisibleText_IsLocalized (LocalizedTextConventionTestsBase.cs:31), and the base's AllowedFiles hook (:28) is left empty, so ADC exempts nothing at file granularity. The scan covers snackbar messages, page Title properties, <PageTitle> markup, breadcrumb labels, and NavItem titles, the last of which must carry a TitleResource (LocalizedTextConventionTests.cs:6-:9).

MicroserviceExtractionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Layering · MMCA.ADC.Architecture.Tests/Layering/MicroserviceExtractionTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the single-fact guard that transport never leaks into the core layers, so a module behaves identically in-process or extracted.
  • Depends on - MicroserviceExtractionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:5). One inherited fact, CoreLayers_ShouldNotDependOn_Transport (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/MicroserviceExtractionTestsBase.cs:13). [Rubric §7 - Microservices Readiness] assesses whether extraction is a configuration change or a rewrite. ADC is the repo where this rule has already been cashed in: all four modules run as separate service hosts behind a YARP gateway (ADR-007, ADR-008), and the rule is what keeps the core layers clean enough for the next one.

ModuleIsolationTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Layering · MMCA.ADC.Architecture.Tests/Layering/ModuleIsolationTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the guard that Identity, Conference, Engagement, and Notification do not reach into each other: module Domains, Applications, Infrastructures, and APIs are each isolated from their siblings, neither Domain nor Application may reach another module's Infrastructure, and a seventh rule closes every remaining internal-layer pair.
  • Depends on - ModuleIsolationTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:5). Seven inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleIsolationTestsBase.cs:13, :16, :19, :22, :25, :28, :36). The last, ModuleInternalLayers_ShouldNotReach_OtherModuleInternalLayers (:36), exists to close the coverage the first six leave open: every remaining cross-module internal-layer pair (Domain to another Application or Api, Application to another Domain or Api, Infrastructure and Api to anything but their own module), with UI excluded on purpose (:30-:34). The targets are computed by the map: for each module and layer it derives the other modules' root namespaces (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:69-:72), which is why adding a module to the map immediately expands what every other module is forbidden to touch. [Rubric §7 - Microservices Readiness] and [Rubric §5 - Vertical Slice]: cross-module collaboration in ADC goes through interfaces satisfied by gRPC clients, and this rule is what stops a direct reference from quietly becoming the cheaper option.

NamingConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/NamingConventionTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the ten-fact naming and sealing guard: handler, command, query, validator, DTO, specification, repository, and EF configuration suffixes, plus domain events sealed in a *.DomainEvents namespace and invariant classes static.
  • Depends on - NamingConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:5). Ten inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/NamingConventionTestsBase.cs:13 through :40). [Rubric §15 - Best Practices & Code Quality], and more practically [Rubric §15 - Best Practices & Code Quality]: several framework mechanisms (the Scrutor handler scan, the DTO/mapper registration, the decorator wrapping) find their targets by convention, so a naming slip is not cosmetic, it silently un-registers a type.

PiiConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/PiiConventionTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the privacy structural guard: every domain entity declaring a PiiAttribute-marked property must implement IAnonymizable, so an entity that holds personal data always has an erasure path.
  • Depends on - PiiConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:5). One inherited fact, EntitiesWithPiiProperties_ShouldImplement_IAnonymizable (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/PiiConventionTestsBase.cs:12). [Rubric §30 - Compliance/Privacy/Data Governance] assesses whether privacy obligations are structural rather than procedural: soft delete is the default everywhere in MMCA, so erasure has to be an explicit, tested capability (ADR-005). Unlike its counterpart in MMCA.Common, this instance is non-vacuous: ADC's Identity domain holds real attendee data.

RawQueryableConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:11 · Level 13 · class (public, sealed)

  • What it is - the rule that Application-layer code must not use the repository's raw IQueryable surfaces (Table / TableNoTracking*), carrying an eight-file allowlist that pins ADC's existing deliberate uses (MMCA.ADC.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:3-:10, :33-:52).
  • Depends on - RawQueryableConventionTestsBase, AdcArchitectureMap, ArchitectureMapBase (statically, for the repo root at :28), and System.IO.Path.
  • Concept introduced, the adoption ratchet. A convention introduced into a codebase that already violates it in eight places has two bad options: fail the build on day one, or exempt the whole layer. The ratchet is the third: enumerate the existing violations explicitly so that new code is what the rule blocks, then shrink the list over time. The comment states the discipline directly, "Shrink it over time; never grow it without the same scrutiny" (:8-:9). [Rubric §8 - Data Architecture] assesses how query access is layered; [Rubric §7 - Microservices Readiness] is the stated motivation: a raw-queryable handler is EF-coupled and cannot move behind a gRPC boundary (:5-:6).
  • Walkthrough - three members.
    • Map (:13).
    • ApplicationSourceDirectories() (:21-:30) overrides the base's virtual enumeration (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/RawQueryableConventionTestsBase.cs:45) by yielding everything the base produces (:23-:26) and then appending one more path: Source/Modules/Notification/MMCA.ADC.Notification.Application, resolved from FindRepoRoot("MMCA.ADC.slnx") (:28-:29).
    • AllowedFiles (:33-:52) overrides the base's empty default (RawQueryableConventionTestsBase.cs:38) with eight file names, each grouped under a comment saying why it is exempt: the Engagement live layer's conference-day hot-path aggregations, which need GROUP BY and COUNT shapes the focused repository surface cannot express (LivePollResultsBuilder, SessionQuestionViewBuilder, GetModerationQueueHandler, GetSessionQuestionsHandler, :35-:40); the Engagement bookmark count and page projection (BookmarkCountService, GetUserBookmarksHandler, :42-:45); Identity's server-side user list paging and sorting projection (GetUsersHandler, :47-:48); and Notification's GDPR export joins (UserNotificationExportService, :50-:51). Every entry is annotated as intra-module, which is the actual test for whether an exemption is safe: the queries never cross a module boundary, so they would travel with the module if it moved.
    • The inherited fact is ApplicationLayer_DoesNotUseRawQueryableSurfaces (RawQueryableConventionTestsBase.cs:61), a textual scan rather than an assembly scan, which is why it needs directories rather than the map's assemblies; it also asserts the directory set is non-empty first (:65-:66).
  • Caveats / not-in-source - AllowedFiles matches by file name, not by path, so two files with the same name in different modules would both be exempted. The <remarks> on the directory override still says the thin Notification module "is not a mapped module" (:16-:20), but AdcArchitectureMap now registers it (AdcArchitectureMap.cs:52-:54) and the base derives its directories from Map.ModuleNames (RawQueryableConventionTestsBase.cs:50-:57), so the appended path is a duplicate of one the base already yields and the comment no longer matches the map. Nothing here enforces the "shrink it over time" discipline the class comment asks for.

ServiceContractPurityTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Layering · MMCA.ADC.Architecture.Tests/Layering/ServiceContractPurityTests.cs:9 · Level 13 · class (public, sealed)

  • What it is - the purity rule for the published gRPC wire surface: a type marked ServiceContractAttribute must not depend on the producing service's Domain, Application, or Infrastructure (MMCA.ADC.Architecture.Tests/Layering/ServiceContractPurityTests.cs:3-:8). Map-only shape (ConcurrencyConventionTests).
  • Depends on - ServiceContractPurityTestsBase and AdcArchitectureMap.
  • Concept introduced, the attribute-driven ratchet. The other purity rules in this unit iterate layers. This one cannot: no repo registers Layer.Contracts in its map today, so a layer-iterating rule would pass vacuously forever without anyone noticing (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ServiceContractPurityTestsBase.cs:8-:11). Instead it scans every assembly the map registers for types carrying the marker, wherever they live, and enforces the invariant from the first marked type onward (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:21-:24). [Rubric §7 - Microservices Readiness] assesses whether a service can be consumed without its internals: a contract that leaks a domain entity or a persistence type forces every consumer to take the producer as a package dependency, which is what makes an extraction irreversible (ArchitectureRules.Contracts.cs:14-:18). [Rubric §9 - API & Contract Design] covers the same boundary from the contract side, and ContractImplementationTests is the twin guarding it from the other direction.
  • Walkthrough - one member, Map (:11), implementing the base's abstract property (ServiceContractPurityTestsBase.cs:22). One inherited fact, ServiceContracts_ShouldNotDependOn_ServiceInternals (:25), delegating to ArchitectureRules.ServiceContractsDoNotDependOnServiceInternals(Map) (ArchitectureRules.Contracts.cs:32-:54). The rule computes the forbidden internal namespaces from the map, returns immediately if that set is empty (:34-:38), and otherwise runs a NetArchTest query per mapped assembly against the types carrying the marker (:40-:53). The marker is matched by full name, MMCA.Common.Shared.Abstractions.ServiceContractAttribute (:10-:11), keeping the rule library free of a framework reference.
  • Why it's built this way - the base adds a second consequence that reads as a design statement (ArchitectureRules.Contracts.cs:26-:29): a marked type living inside a Domain, Application, or Infrastructure assembly fails by construction, and that is the intent, because a published contract belongs in a *.Contracts or Shared assembly rather than inside the service it describes.
  • Where it's used - the rule is live in ADC, not latent. Six [ServiceContract] interfaces are declared in mapped module Shared assemblies today: IAttendeeQueryService (MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.Shared/Users/IAttendeeQueryService.cs:10), IEventLiveValidationService (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Shared/Events/Live/IEventLiveValidationService.cs:12), ISessionBookmarkValidationService (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Shared/Sessions/ISessionBookmarkValidationService.cs:10), IBookmarkCountService (MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/UserSessionBookmarks/IBookmarkCountService.cs:10), IUserEngagementExportService (MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/Exports/IUserEngagementExportService.cs:13), and IUserNotificationExportService (MMCA.ADC/Source/Modules/Notification/MMCA.ADC.Notification.Shared/UserNotifications/IUserNotificationExportService.cs:13). All six sit in Shared assemblies the map registers, so the rule inspects them on every run.
  • Caveats / not-in-source - the *.Contracts projects that hold the generated gRPC clients and adapters are not in AdcArchitectureMap, so any [ServiceContract] type declared there rather than in Shared would be outside this scan.

SharedLayerTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Layering · MMCA.ADC.Architecture.Tests/Layering/SharedLayerTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the guard on the Shared layer, the one layer other modules are allowed to reference: a module's Shared project must not depend on that module's own internal layers, must not reach sibling modules, and must stay free of EF Core.
  • Depends on - SharedLayerTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:5). Three inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/SharedLayerTestsBase.cs:12, :15, :18). [Rubric §7 - Microservices Readiness] and [Rubric §9 - API & Contract Design]: Shared holds the DTOs, requests, integration events, and (in ADC) the six [ServiceContract] interfaces that cross a module boundary, so a dependency from Shared into Domain or Infrastructure would drag the module's internals along with its contract and make extraction impossible.

SliceCohesionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/SliceCohesionTests.cs:8 · Level 13 · class (public, sealed)

  • What it is - the vertical-slice cohesion rule: every module's Application/{Aggregate}/UseCases/{Operation}/ slice keeps its command or query, its handler, and its validator in one namespace, and the build fails if a handler is stranded from its contract (MMCA.ADC.Architecture.Tests/Cqrs/SliceCohesionTests.cs:3-:7).
  • Depends on - SliceCohesionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:10). Two inherited facts, Handlers_ShouldBeCoLocatedWith_TheirContracts and Validators_ShouldBeCoLocatedWith_TheirContracts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/SliceCohesionTestsBase.cs:15, :19). [Rubric §5 - Vertical Slice] assesses whether a feature is one navigable unit; the rule turns the folder convention into something the build can check, so the slice does not erode into a layer-per-type layout one refactoring at a time.

SoftDeleteEnforcementTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:15 · Level 13 · class (public, sealed)

  • What it is - the rule that EF Core's row-erasing members are banned outside four named framework types (MMCA.ADC.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:20-:42). It is the counterpart to CascadeSoftDeleteConventionTests: that one checks the cascade is a soft delete, this one checks nothing erases in the first place.
  • Depends on - SoftDeleteEnforcementTestsBase and AdcArchitectureMap.
  • Concept introduced, an allowlist that names types instead of a namespace. The base bans DbSet.Remove/RemoveRange, DbContext.Remove/RemoveRange, and ExecuteDelete/ExecuteDeleteAsync outside the purge and erasure types the repo lists (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SoftDeleteEnforcementTestsBase.cs:4-:9). ADC's <para> explains the shape of its own list (SoftDeleteEnforcementTests.cs:8-:13): every allowlisted type is in MMCA.Common, because no ADC module, service, or host erases a row of its own, and the list names those framework types individually rather than exempting the MMCA.Common namespace wholesale, so a future framework type that starts erasing rows still fails here and gets reviewed. [Rubric §30 - Compliance/Privacy/Data Governance] and [Rubric §8 - Data Architecture]: soft delete is what keeps a deleted row auditable, restorable, and accounted for under an erasure request (ADR-005).
  • Walkthrough - two members. Map (:17), and AllowedHardDeleteTypes (:20-:42), which overrides the base's empty default (SoftDeleteEnforcementTestsBase.cs:27) with four entries, each carrying its own justification.
    • EFRepository<TEntity, TIdentifierType> (:26), the framework's set-based delete escape hatch. Erasing is the caller's explicit ask there, and the comment names the one ADC caller: ScoreEventSessionsHandler, which replaces a session's AI scores before rescoring, so it is deleting derived rows it is about to rewrite rather than user data (:22-:25).
    • OutboxCleanupService (:31, listed by its full name MMCA.Common.Infrastructure.Persistence.Outbox.Administration.OutboxCleanupService), outbox and inbox retention: delivery plumbing with a bounded lifetime, where the dead-letter sweep is the retention policy and soft-deleting would grow the table the job exists to bound (:28-:30).
    • AuditTrailCleanupJob (:35), audit-trail retention, with the sharpest line in the file: erasing past the retention window is the requirement, because keeping an audit row forever is the privacy defect and not the safeguard (:33-:34).
    • RefreshSessionCleanupService (:41), refresh-session retention. The reasoning is worth reading in full (:37-:40): a session row is framework bookkeeping rather than an aggregate, it carries no IsDeleted flag and no audit stamps, and its content is a credential digest plus the IP and user-agent of a device, so flagging it instead of erasing it would keep a growing record of a data subject's devices past any use for it.
    • The inherited fact is HardDeletes_ShouldOnlyOccurIn_AllowedPurgeTypes (SoftDeleteEnforcementTestsBase.cs:30).
  • Why it's built this way - the four entries are a good worked example of how MMCA reasons about erasure: three of them erase because privacy requires it, not despite it. The base frames the list the same way as the other ratchets, as a reviewed inventory of every place that does not soft-delete rather than a blanket exemption (SoftDeleteEnforcementTestsBase.cs:11-:15).
  • Caveats / not-in-source - entries are full names including the generic-arity suffix (EFRepository`2), so an arity change would drop the exemption and turn the rule red, which is the safe direction.

SpecificationConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC.Architecture.Tests/Domain/SpecificationConventionTests.cs:8 · Level 13 · class (public, sealed)

  • What it is - the cross-source specification guard: no specification may filter by navigating to another entity, because such a filter would not translate if that entity later moved to a different data source. The stated alternative is CrossSourceSpecification (MMCA.ADC.Architecture.Tests/Domain/SpecificationConventionTests.cs:3-:7).
  • Depends on - SpecificationConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Concept introduced - a forward safeguard for a capability that is not currently exercised. Every ADC entity routes to SQL Server today: the Conference Session-to-Cosmos and Room-to-SQLite polyglot trial was reverted, but the framework extension points were kept (ADR-018), so ADC opts into the rule anyway (:3-:5). This is worth noticing as a deliberate choice: the rule costs nothing while the repo is single-engine and prevents a class of query from being written that would have to be unwritten later.
  • Walkthrough - one member, Map (:10). One inherited fact, Specifications_ShouldNotNavigate_ToOtherEntities (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SpecificationConventionTestsBase.cs:16). [Rubric §8 - Data Architecture].

StateManagementConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/StateManagementConventionTests.cs:9 · Level 13 · class (public, sealed)

  • What it is - the Blazor state-ownership guard: the Identity, Conference, and Engagement UI assemblies carry no mutable static state, and stateful UI services stay scoped (MMCA.ADC.Architecture.Tests/Ui/StateManagementConventionTests.cs:3-:8).
  • Depends on - StateManagementConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Concept introduced - why this is a correctness rule and not a style rule. Under Blazor Server every user gets a circuit inside one process, so a mutable static member is shared across every connected attendee, and a singleton-registered stateful service is the same defect wearing a DI hat: one user's selection becomes everyone's. The comment says exactly this (:6-:7). [Rubric §19 - State Management] assesses how client state is scoped and owned; [Rubric §11 - Security] is the sharper edge, since cross-circuit leakage of user state is a data exposure, not just a bug.
  • Walkthrough - one member, Map (:11). Two inherited facts, UiAssemblies_CarryNoMutableStaticState and UiProjects_RegisterStatefulServicesScoped (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:28, :66). The base's AllowedStaticMembers hook (:25) is left at its empty default, so ADC's module UIs have zero static-state allowances. Notification declares no Layer.Ui assembly in the map, so the scan covers the three module UIs only.

UIArchitectureConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:10 · Level 13 · class (public, sealed)

  • What it is - the container/presentational split enforced mechanically: every code-behind under Source/ (module UI and UI hosts alike) stays within the 400-line convention cap, and inline @code blocks stay small (MMCA.ADC.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:3-:9).
  • Depends on - UIArchitectureConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Concept introduced - a size cap as a proxy for a structural property. Nothing can test "this component separates orchestration from presentation", but a code-behind that has grown past 400 lines has almost certainly stopped doing so. The comment records the payoff: the gate subsumes tech-debt item TD-13, because the oversized Conference dashboards were split to conform when it landed (:7-:8). That is the ratchet working in the other direction from RawQueryableConventionTests: instead of pinning the violations, the violations were fixed. [Rubric §18 - UI Architecture] assesses front-end composition and is the rule's own stated target.
  • Walkthrough - one member, Map (:12). Two inherited facts, CodeBehinds_StayWithinTheLineCap and RazorFiles_KeepInlineCodeBlocksSmall (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/UIArchitectureConventionTestsBase.cs:44, :63). All four tuning hooks stay at their base defaults: MaxCodeBehindLines 400 (:22), MaxInlineCodeLines 120 (:29), MinimumCodeBehindFiles 1 (:35), and an empty ExcludedPathFragments (:41), so ADC excludes nothing from the scan.

Per-project test rollup

This guide treats tests as grouped, not sectioned per [Fact] (the logged exception in the charter): the reusable test bases, the shared architecture-fitness library and its per-repo thin subclasses, and the component Gallery harness each get their own ### treatment in the earlier parts of this chapter, but the bulk of the suite, 1,971 individual test types across 45 projects, is rolled up here. Each row below names a test project (assembly), the count of test types it contributes to the 1,971, what it covers, and its style (unit / integration / component / E2E / performance-smoke). Counts reconcile exactly to the unit input.

A few cross-cutting facts hold for every row, so they are stated once here rather than repeated:

  • Stack. Every project is xUnit v3 run under the Microsoft Testing Platform (not VSTest, global.json sets "runner": "Microsoft.Testing.Platform"), with AwesomeAssertions for fluent asserts, Moq for test doubles, and coverlet for coverage (see for example MMCA.Common/Tests/Hosting/MMCA.Common.Aspire.Hosting.Tests/MMCA.Common.Aspire.Hosting.Tests.csproj:8). The lone exception is MMCA.Common.Benchmarks, a BenchmarkDotNet executable (not a test project). See primer §3 for the platform/runner externals. MMCA.Common's CI runs the whole solution behind a discovery floor, --minimum-expected-tests 2000 (MMCA.Common/.github/workflows/ci.yml:144), so a regression that silently stops discovering thousands of tests fails the build instead of passing quietly.
  • Layering mirror. The ADC module suites repeat the same seven-project shape per module ({Module}.{Shared,Domain,Application,Infrastructure,API,UI}.Tests + a per-service {Module}.IntegrationTests), so once you understand the Conference column you understand Engagement and Identity: they differ only in volume, not in kind. Conference carries one project on top of that shape, the standalone AI-scoring evaluation suite. Notification is the deliberate exception, a thinner module with only API, Application, and IntegrationTests projects, because its domain and persistence live in MMCA.Common and are tested there.
  • Feature waves show up as whole clusters. The framework's opt-in enterprise wave (ADR-073 multi-tenancy, ADR-074 scheduler, ADR-075 audit trail, ADR-076 data-subject export, ADR-077 HybridCache, ADR-078 CSV export) sits in Infrastructure, API, Application, Shared and Aspire. A hardening wave added conditional writes (ADR-035), the specification-first read contract with keyset paging and projection pushdown (ADR-055), Authorization/Timeout decorators, distributed rate limiting (ADR-019), poison-message observability (ADR-087), and the gateway edge kit (ADR-088, ADR-089). The newest wave is auth-surface and messaging work: cache-backed password reset (ADR-091), multi-device refresh sessions (ADR-097 on top of ADR-050), the event-upcaster registry (ADR-090), and the extraction of a reusable gateway package. In ADC the matching wave is the sponsor surface, the QR badge check-in and points ledger (ADR-072), and the Activity aggregate. Each one appears below as a named cluster in the row that owns it.
  • Fitness tests and shared bases live elsewhere. MMCA.Common.Architecture.Tests and MMCA.ADC.Architecture.Tests (the NetArchTest layer/purity/extraction suites, thin subclasses of the shared ArchitectureRules rule library, ADR-015) are not in this table: they are covered as first-class sections earlier in this chapter. The same is true of the shared test bases (IntegrationTestBase<TFixture>, HandlerTestBase<THandler>, BunitComponentTestBase, ProductionHostApplicationFactory<TEntryPoint>, SecurityHeadersTestsBase, GracefulShutdownTestsBase<TEntryPoint>, ModuleConformanceTestsBase<TModule>, MmcaGatewayHardeningTestsBase<TEntryPoint>, MiddlewarePipelineOrderTestsBase, ServiceBusEmulatorFixtureBase, CrossServiceFixtureBase, AuthorizationTestsBase, PasswordResetTestsBase, GalleryAxeTestBase, JwtTokenGenerator, TestPolling, DependencyInjectionAssert, RecordingHttpForwarder, the Playwright fixtures) and the MMCA.Common.UI.Gallery harness. The counts below therefore move when a repo adopts one of those bases: ADC's Gateway suite lost its local fake forwarder and its eight hardening gates the week both moved into MMCA.Common.Testing, and its Service Bus emulator fixture collapsed to a subclass the week ServiceBusEmulatorFixtureBase landed.
  • Four projects sit outside MMCA.Common.slnx on purpose. MMCA.Common.UI.Gallery, MMCA.Common.UI.E2E.Tests, MMCA.Common.Benchmarks, and MMCA.Common.Infrastructure.Redis.Tests are absent from the solution file (MMCA.Common/MMCA.Common.slnx:32-37 lists the Tests/Core projects, :39-43 the Tests/Presentation ones and :45-49 the Tests/Hosting ones) so that dotnet test --solution MMCA.Common.slnx never needs Playwright browsers, a Docker daemon, or a multi-iteration timing run. CI builds and runs each one by csproj path in its own job (ui-e2e at MMCA.Common/.github/workflows/ci.yml:228, performance-smoke at :332, redis-integration at :741).
  • Two integration tiers, deliberately split. Each service has a per-service *.IntegrationTests project that boots one host through WebApplicationFactory<Program> with cross-service gRPC edges faked and no broker (these run in the integration-tests CI job and need a real SQL Server named by ADC_TEST_SQL_BASE). Separately, MMCA.ADC.CrossService.IntegrationTests and MMCA.ADC.ServiceBusEmulator.IntegrationTests run against Testcontainers to prove the genuine broker and gRPC round-trips: both live in the weekday-nightly MMCA.ADC/.github/workflows/cross-service-tests.yml, and the recency of a run in which both jobs passed (not the result of any single run) gates deploys through the cross-service-freshness job at MMCA.ADC/.github/workflows/deploy.yml:815, a 5-day window set by FRESHNESS_DAYS at MMCA.ADC/.github/workflows/deploy.yml:825. [Rubric §14, Testability] (assesses how thoroughly and at what cost the system can be verified): the count and spread below, heavy at the inner Application/Domain layers, thinner at the edges, with a dedicated integration + E2E tier, is the classic healthy test pyramid, and the fact that the volume concentrates in fast in-memory unit layers keeps the feedback loop cheap.

MMCA.Common, the framework suite (Tests/ mirrors Source/)

Test project (assembly) Types What it covers · style
MMCA.Common.Shared.Tests 45 The innermost layer: the Result/Error/ErrorType pattern and its extensions, value objects (Money, Email, Address, DateRange, ...) and their factory-method invariants, DTO/paging contracts, and the striped keyed lock behind KeyedSemaphoreStripe (mutual exclusion per key, independent progress across stripes, release on the exception path, and a bounded table size no matter how many caller-supplied keys arrive, MMCA.Common/Tests/Core/MMCA.Common.Shared.Tests/Concurrency/KeyedSemaphoreStripeTests.cs:12). Two files defend contracts that only bite in a head with no ASP.NET pipeline: MoneySerializationTests pins the round trip of Money's private [JsonConstructor], which is also the constructor EF Core uses to materialize the owned type, so a materializer yielding a null currency must fail fast rather than surface a half-built value object (.../ValueObjects/MoneySerializationTests.cs:11); and SupportedCulturesTests pins SupportedCultures.ResolveClosest, the fallback chain a head applies when it resolves its own culture instead of getting request localization's Accept-Language matching for free, since an Android device reports a specific culture (es-MX) while the allowlist holds a neutral one and without the language-level match a Spanish phone would silently start in English (.../Globalization/SupportedCulturesTests.cs:11, ADR-027 / §27). The smart enumeration Enumeration<TEnumeration> has reflection-based member discovery, Result-returning FromValue/FromName lookups (case-insensitive by name, an UnknownValue/UnknownName error rather than an exception) and type-guarded equality pinned, so two enumerations that happen to share an integer value are never equal and never collide in a hash set (.../ValueObjects/EnumerationTests.cs:10). KeysetPaginationTests covers the keyset ("seek") paging value types, the request's clamp semantics, the page result, and the cursor codec's round-trip, version gate and rejection of anything malformed, which matters because a cursor is a client-held token and a corrupted one must be refused rather than quietly paged from the top (.../Abstractions/KeysetPaginationTests.cs:10, ADR-055); RoleValueTests pins that RoleValue.Validate compares roles case-insensitively no matter which comparer the caller's set was built with, after a set built with the default ordinal comparer rejected a correctly spelled role that differed only in casing (.../Auth/RoleValueTests.cs:13). Its newest four all exist because a contract is shared by callers that must never disagree: ErrorTypeSeverityTests pins the severity ranking every transport edge uses to pick the status for a combined result, including that the 400-family ranks equal and that ties keep the earliest error (.../Abstractions/ErrorTypeSeverityTests.cs:10); ProblemDetailsResultReader parses an RFC 9457 payload back into Error instances and lives here rather than in the API package precisely so the UI can reach it (.../Http/ProblemDetailsResultReaderTests.cs:15); NotificationScopeKey ships its formatter and the regex that guards it together, and every key the formatter produces is asserted against that pattern, which is the default the notification hub enforces (.../Notifications/NotificationScopeKeyTests.cs:14); and ModuleNameConventionsTests pins the MMCA.{App}.{Module}.{Layer} parse shared by persistence (SQL schema and data-source names) and the logging decorators' scope enrichment, including the namespaces that carry no module at all (.../Conventions/ModuleNameConventionsTests.cs:15). ConcurrencyETag, the weak entity tag that carries the concurrency token, is pinned here too, one layer below the filter that consumes it (.../Http/ConcurrencyETagTests.cs:10, ADR-035). Pure unit tests, no DI or DB.
MMCA.Common.Domain.Tests 62 The entity hierarchy (BaseEntityAuditableBaseEntityAuditableAggregateRootEntity), domain-event collection, SetItems<T>/GetChildOrNotFound<T>, specifications, and the PiiAttribute/anonymization boundary plus the logging/telemetry redaction half of the [Pii] contract (masks marked members so a data subject's values never reach logs, MMCA.Common/Tests/Core/MMCA.Common.Domain.Tests/Privacy/PiiRedactorTests.cs:12, ADR-005 / §30, PiiRedactor). OwnedByUserSpecification<TEntity, TIdentifierType>, the reusable "rows this caller created" criteria, is covered with a fake that overrides the virtual CreatedBy getter, because that audit field is stamped by the infrastructure layer through EF's change tracker and is otherwise unsettable from a test (.../Specifications/OwnedByUserSpecificationTests.cs:8). SpecificationCompositionTests pins HOW the boolean composers build their criteria rather than only what the composed predicate answers, and neither property is visible from IsSatisfiedBy: the composed tree must contain no InvocationExpression, because a provider that cannot unwrap one (Cosmos) throws at translation time on an ANDed specification, and it must be built once per instance, because the query pipeline reads Criteria on every request while the old implementation rebuilt the tree every time (.../Specifications/SpecificationCompositionTests.cs:18, ADR-018). QuerySpecificationTests covers the builder state a QuerySpecification carries beyond its predicate (includes, ordering, paging, tracking, soft-delete scope) and pins the base chain the SpecificationsDoNotNavigateToOtherEntities fitness rule keys on (.../Specifications/QuerySpecificationTests.cs:14). OutputCacheEvictionRequested is asserted as a shape rather than a behavior (members, schema version, empty-tags default) because it is the framework's own published integration event and every host that consumes it must be able to deserialize it forever (.../IntegrationEvents/OutputCacheEvictionRequestedTests.cs:12, ADR-010). Its newest file is the RefreshSession record (BR-205/206): the token is stored only as an upper-case hex SHA-256 digest and never in the clear, captured client metadata is truncated to its column width rather than overflowing at insert, revocation records when, why and the successor, and re-revoking keeps the first reason (.../Auth/RefreshSessionTests.cs:13, ADR-097). Pure unit tests over the framework domain primitives.
MMCA.Common.Application.Tests 343 The CQRS engine: the decorator pipeline in its registered nesting order, which now carries two validating rings (commands: FeatureGate→Authorization→Logging→Caching→Validating→Timeout→Transactional→handler, MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:131-137; queries: FeatureGate→Authorization→Logging→Caching→Validating→Timeout→handler, .../DependencyInjection.cs:138-143, both registered innermost-first because Scrutor's TryDecorate applies in reverse, .../DependencyInjection.cs:115), the opt-in MiniProfiler decorators added by AddApplicationProfiling (.../DependencyInjection.cs:565) and the CqrsMetrics counters/histograms (MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Decorators/CqrsMetricsTests.cs:20), ModuleLoader topological ordering, DomainEventDispatcher plus the swallow-and-log SafeDomainEventHandler base (.../DomainEvents/SafeDomainEventHandlerTests.cs:14), validation, the IMessageBus abstraction, entity-query projection/paging and the per-type filter strategies, the cross-source CrossSourceSpecification helper, the magic-byte upload sniffer behind ImageContentSniffer (.../ImageContentSnifferTests.cs:12, ADR-045), and the notification read/send handlers driven by an injected TimeProvider test clock. Two older files defend non-obvious properties rather than behavior: PagingMathTests pins the page arithmetic (.../Services/Query/PagingMathTests.cs:12), and QueryFilterServicePropertyCacheTests asserts on the real static property cache inside QueryFilterService to prove that caching a miss never happens, since filter names arrive in the query string and a negatively-cached miss would let any caller grow a process-lifetime dictionary one bogus name at a time while the request still returned a tidy 400 (.../Services/Filtering/QueryFilterServicePropertyCacheTests.cs:14, §11/§12). The hoisted user use cases hold a large block: ChangePasswordHandlerBaseTests pins verify-before-write ordering and the no-save-on-invariant-failure rule (.../Users/ChangePasswordHandlerBaseTests.cs:15, ADR-032); DeleteUserHandlerBaseTests pins the owner-or-privileged-role gate, the delete-then-anonymize-then-save ordering and the post-commit queue (.../Users/DeleteUserHandlerBaseTests.cs:14); UserOwnershipRule gets its own tests for the self-service authorization check that was written out four times across the two apps before the hoist (.../Users/UserOwnershipRuleTests.cs:11); SoftDeletedUserValidatorTests proves the BR-133 check is one query with the soft-delete global filter deliberately bypassed (.../Users/SoftDeletedUserValidatorTests.cs:13); and ExportUserDataHandlerBaseTests drives ExportUserDataHandlerBase<TUser, TQuery> through the ownership gate, the read-only account load and the best-effort section fan-out, where a section that throws or reports itself unavailable must degrade and let the rest of the package travel (a data-subject request is a legal deadline, so a partial export beats a failed one) while a cancelled section propagates instead (.../Users/ExportUserDataHandlerBaseTests.cs:17, ADR-076 / §30). The password-recovery pair is the newest addition to that family and both files are written around anti-enumeration: a malformed address, an unknown address, a throttled request and a failed send are all indistinguishable successes, and the mail that IS sent carries both the prefilled link and the raw token (.../Users/ForgotPasswordHandlerBaseTests.cs:20), while every rejection on the reset leg collapses to one generic Auth.InvalidResetToken, the write happens only when the aggregate accepts it, the token is consumed before the save, and a successful reset clears the lockout so the new credential is immediately usable (.../Users/ResetPasswordHandlerBaseTests.cs:18, ADR-091). RefreshSessionManagementTests covers the per-device surface layered on that workflow: the sid claim stamped on every issued access token, a refresh stamping the successor session's id rather than the predecessor's, an older access token with no sid still accepted, and a device list that carries the client fields and never the token material (.../Auth/RefreshSessionManagementTests.cs:26, ADR-097). Alongside them, SoftDeletedUserCache has its key shape and culture invariance pinned because the API middleware reads that exact key (.../Auth/SoftDeletedUserCacheTests.cs:13); CachingDecoratorConstructorSelectionTests pins that the container picks the logger-bearing constructor on both caching decorators, since if selection ever flipped the decorators would keep working and every test would stay green while production silently stopped reporting cache failures (.../Decorators/CachingDecoratorConstructorSelectionTests.cs:21); and CachingDecoratorTenantScopingTests covers the tenancy half of the same pair, where ICacheService is a singleton that cannot see the scoped tenant, so the key transformation is the isolation (.../Decorators/CachingDecoratorTenantScopingTests.cs:16, ADR-073 / §11). The two newer decorator rings have their own files: AuthorizationCommandDecoratorTests (.../Decorators/AuthorizationCommandDecoratorTests.cs:11) and its query twin cover the permission check that runs inside the pipeline rather than only at the controller, and TimeoutCommandDecoratorTests (.../Decorators/TimeoutCommandDecoratorTests.cs:9) covers the per-handler execution budget. Read-pipeline determinism and projection pushdown form their own block (ADR-055, §12): EntityQueryPipelineOrderingTests pins that a paginated read always ends up with a total order, because Skip/Take over a partial order is undefined and lets one row appear on two consecutive pages while another appears on none, while an unpaginated read is deliberately left alone (.../Services/EntityQueryPipelineOrderingTests.cs:16), with the tie-break parameter covered directly and its omission pinned as behavior-preserving (.../Services/QueryFieldServiceTieBreakTests.cs:11); EntityQueryService gains a projection path that selects DTO columns and never calls the mapper, with a fallback to materialize-then-map when the read cannot be projected (.../Services/EntityQueryServiceProjectionTests.cs:19), and its two-constructor arrangement is proved to actually resolve under Microsoft.Extensions.DependencyInjection, which has no notion of an optional dependency (.../Services/EntityQueryServiceResolutionTests.cs:19); PushNotificationDTOProjectorTests is the safety property underneath all of that, since the two paths are chosen by whether a projector is registered and a divergence would make the response depend on a DI detail (.../Notifications/PushNotificationDTOProjectorTests.cs:16). BestEffortTests covers the helper the fire-and-forget call sites share: the success path is transparent, a failure is swallowed with exactly one Warning and one metric increment, and cancellation is deliberately NOT part of the swallow (.../Services/BestEffortTests.cs:13). The newest block is composition and contract guards. EventUpcasterRegistry covers the schema-evolution ladder: identity for an unregistered contract, a V1→V2→V3 chain applied in order even when registered out of order, envelope (MessageId/DateOccurred) preservation across every hop whether or not the author copies it, and the three constructor-time misconfigurations (duplicate source, cycle, self-map) that must throw naming the offender (.../Services/EventUpcasterRegistryTests.cs:19, ADR-090); its sample contracts live in the test assembly only so the framework's frozen contract snapshot never churns on them. ApplicationPipelineCompositionTests covers AddMmcaApplicationPipeline, the one call that replaces the manual AddApplication → scan → AddApplicationDecorators sequence, and the registration-time seal that turns a handler registered after the decorators (which Scrutor would silently leave undecorated) into a startup failure (.../ApplicationPipelineCompositionTests.cs:20, ADR-014). PackageGraphPurityTests reads the csproj itself, because the IL-based fitness rules can only see what the code uses and are blind to what a PackageReference drags in: one web or persistence package would enter the graph of every consumer of this host-agnostic package and nothing else in the build would notice (.../PackageGraphPurityTests.cs:15, §32). CqrsContractInspectorTests covers the inspector behind the fitness rule that a command marker is never handled as a query and that a request's declared result type matches its handler's (.../UseCases/CqrsContractInspectorTests.cs:7), and ScopedIntegrationEventHandlerBaseTests pins the base that opens one DI scope per delivery (integration-event handlers are singletons and cannot hold a scoped service), disposes it on every path, logs a failure exactly once before letting it propagate so the delivery mechanism can redeliver, and passes cancellation through unlogged because host shutdown is not a delivery failure (.../DomainEvents/ScopedIntegrationEventHandlerBaseTests.cs:15). The framework's largest suite by breadth; fast unit tests with mocked infrastructure.
MMCA.Common.Infrastructure.Tests 403 The widest layer: EF repositories + Unit of Work, the multi-database resolver/registry (DataSourceResolver, EntityDataSourceRegistry, DbContextFactory) with its transaction coverage (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/DbContexts/DbContextFactoryTransactionTests.cs:33) and the cross-data-source degrade convention (.../Persistence/DataSources/CrossDataSourceDegradeConventionTests.cs:24), the outbox processor plus its wake signal (.../Persistence/Outbox/Processing/OutboxSignalTests.cs:13) and the consumer-side EfInboxStore idempotency ledger (.../Persistence/Inbox/EfInboxStoreTests.cs:27, ADR-021), caching, JWT issuance + JWKS + the login-attempt lockout service (.../Auth/LoginProtectionServiceTests.cs:14), column-level encryption (.../Persistence/Encryption/EncryptedStringConverterTests.cs:6), the filtered-unique-index soft-delete convention (.../Persistence/Conventions/SoftDeleteUniqueIndexConventionTests.cs:24), image processing (.../Storage/ImageSharpImageProcessorTests.cs:15), the SignalR push + live-channel plumbing, the message-bus implementations, the polyglot Cosmos-config portability suite (ADR-018), and the in-repo disaster-recovery database-restore drill (.../Resilience/DatabaseRestoreDrillTests.cs:18, a CI-gated RTO baseline, ADR-009 / §29). Three files are pure §12 performance guards and are the only place the emitted SQL or the tracker's work is inspected at all: QueryParameterizationTests asserts that the dynamic-LINQ filter and sort strategies send their values as SQL parameters rather than inlined literals, which is what decides whether SQL Server reuses a plan and whether EF's compiled-query cache hits (.../Persistence/Specifications/QueryParameterizationTests.cs:26); SaveChangeDetectionTests pins that a save runs change detection exactly once and that suppressing the extra passes lost the tracker no actual changes (.../Persistence/Interceptors/SaveChangeDetectionTests.cs:24); and PeriodicBackgroundServiceTests drives PeriodicBackgroundService deterministically through a FakeTimeProvider clock to cover the enablement gate, the startup delay, interval-driven cycles, and the failing-cycle-never-kills-the-loop contract (.../Scheduling/PeriodicBackgroundServiceTests.cs:15). A second cluster hardens the save path: DbContextFactorySaveIntegrityTests pins the post-loop assertion that turns silent data loss into a failure, since the save loop is bounded and in-process domain-event dispatch runs inside it, so a handler can leave tracked changes behind that the loop will never reach (.../Persistence/DbContexts/DbContextFactorySaveIntegrityTests.cs:29); DbContextFactoryCommitAmbiguityTests covers the case where a commit-phase failure has an unknowable outcome, because the database may have applied the transaction and lost only the acknowledgement (.../Persistence/DbContexts/DbContextFactoryCommitAmbiguityTests.cs:34); DomainEventCaptureExclusionTests proves event capture is scoped-exclusion aware (.../Persistence/Interceptors/DomainEventCaptureExclusionTests.cs:26); and EFRepositoryAuditStampTests pins that the repository's own save entry points stamp the acting user like the unit of work does, after they were found calling the plain EF overloads and attributing everything written through them to the system sentinel (.../Persistence/Repositories/EFRepositoryAuditStampTests.cs:32). Two sit outside persistence: the IDistributedLock pair, where RedisDistributedLock must acquire through a single atomic SET NX PX carrying a TTL and release through a compare-and-delete on the owner token (.../Concurrency/RedisDistributedLockTests.cs:15), with InProcessDistributedLock held to the same contract as the no-Redis fallback (.../Concurrency/InProcessDistributedLockTests.cs:12); and AzureNotificationHubDeviceRegistrar, whose tests focus on the ownership check, since installation ids are client-supplied and the user-scoped delete must verify the user:{id} tag the upsert stamps before it removes anything (.../Notifications/Push/AzureNotificationHubDeviceRegistrarTests.cs:16, ADR-044). The enterprise wave contributes four clusters here. Tenancy: ApplicationDbContextTenantFilterTests proves over real SQLite that the named Tenant global filter composes with SoftDelete rather than replacing it, that a null-tenant system context still sees every row, and that one cached model serves two tenants at once with disjoint results (.../Persistence/Tenancy/ApplicationDbContextTenantFilterTests.cs:15); TenantSaveChangesInterceptorTests covers the write side, where an insert is stamped from the scope and any modify/delete that would cross the boundary throws (.../Persistence/Tenancy/TenantSaveChangesInterceptorTests.cs:12); TenantDataSourceTargetTests covers the (source, tenant) expansion the background sweeps run on, because a tenant with its own database has its own outbox, inbox and trail tables and nothing else opens that database (.../Persistence/Tenancy/TenantDataSourceTargetTests.cs:18); AddMultiTenancyTests pins the DI surface, the fail-closed defaults and the startup validation (.../Persistence/Tenancy/AddMultiTenancyTests.cs:19); and TenantContextTests pins the one-write scope (.../Context/TenantContextTests.cs:10). Audit trail: AuditTrailSaveChangesInterceptorTests asserts every case against a real SQLite round-trip, so what is asserted is what commits, one summary row per insert/delete, one row per changed property on a modify, nothing for an entity marked modified but unchanged, and a [Pii] property recording the redaction token and never the clear value (.../Persistence/AuditTrail/AuditTrailSaveChangesInterceptorTests.cs:18, ADR-075 / §30); AuditTrailReader covers newest-first paging with clamped arguments and an empty answer when the source has no trail table (.../Persistence/AuditTrail/AuditTrailReaderTests.cs:20); AuditTrailCleanupJobTests covers the retention purge and its two do-nothing paths (.../Persistence/AuditTrail/AuditTrailCleanupJobTests.cs:22). Scheduler: ScheduledJobRunner is driven over a FakeTimeProvider and an in-memory ScheduledJobs table so the schedule arithmetic is exact rather than timing-dependent, covering registration sync, the configuration override, the claim lease, outcome recording, and the missed-run policy where a clock that jumped past many occurrences runs once and advances rather than storming (.../Scheduling/ScheduledJobRunnerTests.cs:22, ADR-074); CronosNextOccurrenceTests pins the cron grammar, the strictly-after semantics and the UTC-only clock that keeps a schedule stable across both daylight-saving transitions (.../Scheduling/CronosNextOccurrenceTests.cs:11); AddScheduledJobsTests pins the opt-in DI shape (.../Scheduling/AddScheduledJobsTests.cs:16); SchedulerMetrics pins the meter name a host registers for export and the instrument names, units and tags an operator builds dashboards on (.../Scheduling/SchedulerMetricsTests.cs:13); and SchedulerModelGateTests builds the real model under each combination of Scheduler:Enabled and data source to prove a host that never opted in gets exactly the model it had before the scheduler shipped (.../Scheduling/SchedulerModelGateTests.cs:26). HybridCache: HybridCacheService runs against a real in-process AddHybridCache with a recording L2, and the key-shape assertions carry the whole design, since an entry written by the old cache at the same logical key must be a clean miss, never the WRONGTYPE fault that motivated the split (.../Caching/HybridCacheServiceTests.cs:24, ADR-077). The read contract (ADR-055) is covered against a real provider rather than a mock, because all of it is translation: EFReadRepositoryKeysetPagingTests covers keyset paging end to end, cursor round-trips, next-page detection, the non-unique sort key that makes the identifier tie-break load-bearing, and the two rejection paths of an unknown sort column and a malformed cursor (.../Persistence/Repositories/Read/EFReadRepositoryKeysetPagingTests.cs:16); EFReadRepositorySpecificationTests covers the specification-driven members plus tracking and soft-delete scope (.../Persistence/Repositories/Read/EFReadRepositorySpecificationTests.cs:16); EFReadRepositoryProjectedFilterTests pins the ignoreQueryFilters flag on GetProjectedAsync against the production named soft-delete filter, because without it any caller needing deleted rows (an admin restore screen, a GDPR export) had to abandon the projection (.../Persistence/Repositories/Read/EFReadRepositoryProjectedFilterTests.cs:16); SpecificationEvaluator is exercised over SQLite for the ordering chain bound back to its concrete key type by reflection, includes with the collection split-query switch, and Skip/Take (.../Persistence/Specifications/SpecificationEvaluatorTests.cs:15); and PushNotificationProjectionTranslationTests proves a real provider translates the projection into SQL, enum-to-string conversion included, a failure that would otherwise surface only at runtime (.../Persistence/PushNotificationProjectionTranslationTests.cs:15). Its newest work is the auth, outbox and messaging edges. Auth: PasswordResetTokenServiceTests verifies the token lifecycle single use, the wrong-token attempt cap, the per-email request throttle, the address normalization that keeps casing variants on one record, and the hash-at-rest guarantee (.../Auth/PasswordResetTokenServiceTests.cs:18, ADR-091); EFRefreshSessionStoreRotationTests asserts the rotation claim against a real SQLite database because the guarantee is the database's, not the change tracker's, since two requests presenting the same still-live token each load their own tracked copy with RevokedAt null and an in-memory check-then-act would let both mint a successor (.../Persistence/Auth/EFRefreshSessionStoreRotationTests.cs:27, ADR-050); RefreshSessionCleanupServiceTests covers the retention sweep, where a row is a candidate once it stopped being usable rather than once it was created (.../Persistence/Auth/RefreshSessionCleanupServiceTests.cs:33); and PasswordHasherSecurityTests pins the cryptographic parameters rather than the round trip, since a hasher that hashes and verifies with the same weak settings still round-trips: two known-answer tests recompute the digest independently, a negative one proves the iteration count participates in verification, and reflection pins the private constants so lowering one fails the build (.../Auth/PasswordHasherSecurityTests.cs:18, ADR-102). Outbox and migrations: OutboxAdministration is the operator path that gives a dead-lettered event a way back into delivery instead of leaving "wait for the retention sweep" as the only ending, exercised over a real SQLite outbox table so the set-based replay update is the one that would run in production (.../Persistence/Outbox/Administration/OutboxAdministrationTests.cs:27); OutboxProcessorOrderingTests covers ordered delivery, where rows sharing an OrderingKey must reach the bus one at a time in OccurredOn order and that guarantee must survive the two things a batch-local sort cannot cover, a successor arriving in a later batch and a second replica polling the same table (.../Persistence/Outbox/Processing/OutboxProcessorOrderingTests.cs:35); DependencyInjectionOutboxGateTests pins the registration gate, where an in-process host pays for no outbox table or poll loop, a broker host always gets one, and asking for a broker without the outbox fails at registration rather than silently dropping every cross-service event (.../DependencyInjectionOutboxGateTests.cs:15, ADR-003); MigrationApplyProofTests runs a committed migration from a separate fixture assembly against a real SQLite file through DbContextFactory, the same call the "Migrate" strategy makes, so "the framework applies migrations" is proved rather than assumed (.../Persistence/MigrationApplyProofTests.cs:35), with DbContextFactoryMigrationTargetTests proving which sources the factory acts on (.../Persistence/DbContexts/DbContextFactoryMigrationTargetTests.cs:25). Messaging: UpcastingIntegrationEventConsumer is the draining consumer bound to a retired contract, deduping on the original message id, upcasting to the terminal contract, dispatching to that contract's handlers, and recording the inbox row only after every handler succeeded (.../Messaging/Consumers/UpcastingIntegrationEventConsumerTests.cs:26, ADR-090); EventUpcasterStartupValidatorTests covers the startup check over the registered ladders (.../Messaging/Consumers/EventUpcasterStartupValidatorTests.cs:20); IntegrationEventConsumerHarnessTests drives the consumers through a real MassTransit bus on the in-memory transport, because the registration binding, the MessageId surviving serialization, and a handler exception genuinely becoming the Fault<TEvent> that FaultIntegrationEventConsumer subscribes to are broker behaviors no direct Consume call can show (.../Messaging/Consumers/IntegrationEventConsumerHarnessTests.cs:28, and the fault consumer's own file at .../Messaging/Consumers/FaultIntegrationEventConsumerTests.cs:15, ADR-087); InboxDisabledWarningService covers the one startup line that keeps a disabled dedup store from looking exactly like an enabled one (.../Persistence/Inbox/InboxDisabledWarningServiceTests.cs:11); and ServiceBusEmulatorSupportTests pins the emulator branch's detection and derived management connection string, so production cannot be diverted into it by accident (.../Messaging/ServiceBusEmulatorSupportTests.cs:15). Two more round it out: SqlServerUniqueConstraintViolationDetectorTests covers both halves of the detector, the authoritative provider error numbers and the message fallback, building a real SqlException through the provider's own non-public factory and reporting itself skipped rather than failing the build if a provider upgrade moves that factory (.../Persistence/SqlServerUniqueConstraintViolationDetectorTests.cs:17); and ConnectionStringSettingsValidatorTests covers the one startup rule that spans two configuration sections, a host must be able to reach some database declared either top level or as a named DataSources entry, which is why it is a validator rather than a [Required] annotation (.../Settings/ConnectionStringSettingsValidatorTests.cs:15, ADR-070). Mostly unit with EF-InMemory/SQLite boundaries (no real SQL Server here).
MMCA.Common.Infrastructure.Redis.Tests 2 The one tier in the framework that runs the shipped caches against a real Redis. The unit tier mocks IDistributedCache, which means it asserts the calls the cache makes and never the storage format Redis ends up holding, and that is a blind spot with teeth: Redis keys are typed, INCR creates a string, and the IDistributedCache Redis provider stores every entry as a hash of absexp/sldexp/data, so mixing the two at one key round-trips flawlessly against a mock and answers WRONGTYPE in production, on the ADR-029 rate-limit and lockout counters. DistributedCacheServiceRedisTests starts a redis:7-alpine Testcontainer, builds DistributedCacheService exactly as AddCaching does when both a distributed cache and a multiplexer are registered, and proves the increment-then-read round-trip, that increments carry a TTL so a counter can never lock a subject out forever, that concurrent writers may undercount but must never leave the key unreadable (the honest statement of the current read-modify-write contract), prefix invalidation over a real SCAN, and a plain set/get/remove smoke (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.Redis.Tests/DistributedCacheServiceRedisTests.cs:27). HybridCacheServiceRedisTests is its ADR-077 sibling and exists for the same reason at a higher stake: only a real server can distinguish "the two substrates share a keyspace" from "they do not", so it proves an entry written by either cache is a soft miss (never a fault) for the other, that prefix eviction runs both patterns and also drops the evicting process's own local copy, and that increments stay monotonic across two instances sharing one Redis (.../HybridCacheServiceRedisTests.cs:35). Needs Docker, so the project is outside the slnx and runs in the redis-integration CI job (MMCA.Common/.github/workflows/ci.yml:741). Integration style.
MMCA.Common.API.Tests 138 The presentation pipeline: ApiControllerBase.HandleFailure ErrorType-to-HTTP mapping, the exception-handler chain, the [Idempotent] filter + Idempotency-Key replay, permission policies/ownership filters, correlation, the JWKS and OIDC-discovery endpoints, the session-cookie auth handler/refresher/jar, the shared notification + device controllers, the public-endpoint output-cache policy, the database-initialization startup (the SQLite-EnsureCreated-under-Migrate path), and the error-message localization edge (localizes the human-readable message while leaving the machine Code/ProblemDetails title untouched, ADR-027 / §27). UserAccountAuthControllerBaseTests covers the shared account-management controller base that landed with the hoisted user use cases, driving change-password, preferences and delete through mocked handlers plus ICurrentUserService (MMCA.Common/Tests/Presentation/MMCA.Common.API.Tests/Controllers/Auth/UserAccountAuthControllerBaseTests.cs:17), and PasswordResetAuthControllerBaseTests is its recovery sibling, pinning the two endpoints and their attributes, because both actions are anonymous by necessity so losing the per-IP policy or the idempotency marker would break nothing visible and simply leave an unauthenticated endpoint unthrottled (.../Controllers/PasswordResetAuthControllerBaseTests.cs:23, ADR-091). The rate-limiting files are worth reading together (ADR-019, §11): RateLimitPartitionTests drives the global limiter's exemption and partition-key logic directly (infrastructure paths and gRPC content types bypass, anonymous traffic gets the no-limiter partition, an authenticated caller partitions by name then user_id then remote IP) and then the per-IP anti-spray policy, which deliberately fails open, since an unattributable request gets no limiter rather than sharing one bucket with every other unattributable caller (.../Startup/RateLimitPartitionTests.cs:17); AuthControllerBaseRateLimitTests asserts the attachment by reflection, because the policy is applied with an attribute and a dropped attribute breaks nothing loudly (the endpoint simply stops being throttled), pinning LoginAsync/RegisterAsync carrying the auth-IP policy and RefreshAsync deliberately not carrying it, since refresh is automatic and every Blazor Server circuit shares the UI host's IP (.../Controllers/AuthControllerBaseRateLimitTests.cs:21); RateLimitingSettingsTests guards the defaults and their binding, load-bearing because the permit-count overload of AddCommonRateLimiting delegates to them (.../RateLimiting/RateLimitingSettingsTests.cs:13); RedisFixedWindowRateLimiter covers the permit comparison, the one-shot TTL on the key that opens a window, and the fail-open posture on a Redis fault, since a limiter that failed closed would turn a cache outage into a site-wide 429 storm (.../RateLimiting/RedisFixedWindowRateLimiterTests.cs:16); and RateLimitAlgorithmSelectionTests resolves each partition's factory and inspects the limiter it actually builds, because the partition key alone never says whether a request is counted in memory or against the shared Redis counter (.../Startup/RateLimitAlgorithmSelectionTests.cs:21). Enterprise-wave files: TenantResolutionMiddlewareTests covers TenantResolutionMiddleware at the edge, the configured claim-then-header strategy order, the trimmed value, the fail-closed RequireTenant rejection as ProblemDetails, the excluded paths, and the two shapes that must pass every request straight through (.../Middleware/TenantResolutionMiddlewareTests.cs:17); the CSV export pair holds CsvWriter to RFC 4180 field by field (quote only what needs it, double an embedded quote, CRLF line endings, exactly one BOM, .../Export/CsvWriterTests.cs:8) while EntityControllerBaseExportTests drives the page-loop that exists because the query pipeline has no IAsyncEnumerable path, asserting the fan-in across pages, the short-first-page early stop, and the truncation marker appended exactly at the row cap including both page-boundary edges (.../Controllers/EntityControllerBaseExportTests.cs:25, ADR-078); DataExportControllerBaseTests pins the shipped DSAR endpoint, the dated download a subject receives, the ProblemDetails failure path, and the two attributes that are its whole security posture, asserted directly because nothing else fails when one is dropped (.../Controllers/Privacy/DataExportControllerBaseTests.cs:29, ADR-076 / §30). Conditional writes are covered in layers: SupportsIfMatchAttributeTests pins where the filter takes the token from and which status a conflict gets (.../Concurrency/SupportsIfMatchAttributeTests.cs:17) and EntityControllerBaseETagTests pins that a single read emits the token as a weak ETag so a client can turn straight around and send it as If-Match, while a DTO with no token gets no header (.../Controllers/EntityControllerBaseETagTests.cs:22, ADR-035). Next to them IdempotencyFilterPassthroughTests guards a premise rather than a behavior: a request without an Idempotency-Key header passes through untouched, which is what makes attaching [Idempotent] to an existing POST a non-breaking change for every client already calling it (.../Idempotency/IdempotencyFilterPassthroughTests.cs:22). OutputCacheEvictionHandler closes the cross-service cache loop: every tag on the message is evicted, a single tag's failure is swallowed and logged rather than rethrown (rethrowing would redeliver the message, re-evict the tags that already succeeded, and eventually dead-letter a message whose only consequence is a cache entry that expires on its own), and cancellation still propagates (.../Caching/OutputCacheEvictionHandlerTests.cs:20, ADR-026). Its newest block is contract and startup guards. OpenApiBaselineTests fetches the framework-owned OpenAPI document from a real in-memory host, normalizes it and diffs it against a committed openapi-baseline.v1.json, so an SDK or Asp.Versioning.OpenApi bump, a change to the framework's OpenAPI registration, or a change to the generated ProblemDetails schema fails here instead of reaching consumers unnoticed (.../OpenApi/OpenApiBaselineTests.cs:35, §9); ProblemDetailsRoundTripTests closes the loop between the two halves of the error contract by emitting through the real controller path with the serializer options MVC uses on the wire and reading the payload back with ProblemDetailsResultReader, which is what keeps the reader honest (.../Controllers/ProblemDetailsRoundTripTests.cs:22); MiddlewarePipelineBuilder covers the named-step edge pipeline, its default order, the insert/replace/remove operations a host customizes it with, and the invariants Build re-checks so a customized pipeline fails at startup rather than misrouting at runtime (.../Startup/MiddlewarePipelineBuilderTests.cs:12); ForwardedJwtBearerSecurityTests pins two executable security invariants, that the signature algorithm stays RS256 and that RequireHttpsMetadata resolves secure-by-default everywhere except Development, by executing the real registration and reading the resulting options back with nothing fetched from the authority (.../Startup/ForwardedJwtBearerSecurityTests.cs:22, §11), with the authority-resolution half in .../Startup/JwtAuthorityExtensionsTests.cs:13; EntityControllerBaseReadSpecificationTests pins the read-scoping hook, that every read action (both list overloads, lookup, by-id and export) asks GetReadSpecificationAsync for the rows this caller may see while a controller that overrides nothing queries exactly as unscoped as it always did (.../Controllers/EntityControllerBaseReadSpecificationTests.cs:29); and ModuleHostExtensionsTests covers the host-side module wiring (.../Startup/ModuleHostExtensionsTests.cs:22). CurrentUserTargetingContextAccessorTests rounds the row out on the feature-flag side, pinning which claim carries the user id (the Targeting filter hashes it, so a rollout is only sticky per user if that id is stable) and that an anonymous request produces an empty context rather than an error, because a feature filter must never be able to fail a request (.../FeatureManagement/CurrentUserTargetingContextAccessorTests.cs:15). Unit tests of middleware/filters/controllers in isolation.
MMCA.Common.Grpc.Tests 16 The gRPC transport boundary: Result to RpcException round-tripping, the JWT-forwarding client interceptor, and the Polly resilience pipeline on typed clients (retry, circuit-breaker, and fault-injection). GrpcResultExceptionInterceptor is a good example of a test written around a fixed defect: the error-carrying case keeps the shared ToRpcException mapping, while the empty-errors case (what the message-only constructors produce) used to discard the exception message entirely and answer a placeholder "Unspecified failure", leaving the caller with a failure and no cause; it now keeps StatusCode.Internal and carries the real message, because synthesizing an Error.Failure instead would have downgraded a server-side fault to InvalidArgument and blamed the caller (MMCA.Common/Tests/Presentation/MMCA.Common.Grpc.Tests/GrpcResultExceptionInterceptorTests.cs:24). Its newest file covers the decode leg, the client-side turn of an RpcException back into a Result with its errors intact (.../ResultGrpcExtensionsDecoderTests.cs:14). Unit tests asserting ADR-007 / ADR-009 behavior.
MMCA.Common.Aspire.Tests 44 The service-defaults package: OutboxPollFilterProcessor (drops recurring outbox-poll spans from telemetry export), the SecurityHeadersMiddleware, the head-based trace-sampling cost knob (a ratio in (0,1) opts in, anything else samples everything so a typo cannot silently drop all telemetry, MMCA.Common/Tests/Hosting/MMCA.Common.Aspire.Tests/Telemetry/TracesSampleRatioTests.cs:11, §31), the metrics-instrumentation toggle (.../Telemetry/MetricsInstrumentationToggleTests.cs:16) and the Serilog host wiring (.../Logging/SerilogHostExtensionsTests.cs:19). It guards the one deliberate asymmetry in AddInfrastructureHealthChecks: a missing SQL connection string throws at startup when the host requires it, while absent Redis/RabbitMQ skip silently, and the optional checks carry HealthCheckTags.Optional so they never gate /health/ready (.../Health/InfrastructureHealthChecksTests.cs:16, §29). That optionality now has two files of its own after a production incident where readiness went hard-dependent on Redis: RedisPingHealthCheckTests covers the ping probe itself (.../Health/RedisPingHealthCheckTests.cs:16) and RedisReadinessSafetyTests pins that no Redis check can ever appear in the readiness set, because DistributedCacheService already degrades around a cache blip and an untagged check would instead take every replica out of rotation at once (.../Health/RedisReadinessSafetyTests.cs:24). A large half of the suite is §29 warm-up material: WarmupReadinessGate must stay closed until warm-up finishes, then latch open idempotently and safely under concurrency (.../Warmup/WarmupReadinessGateTests.cs:10), and the health-check face of that gate reports Unhealthy while the replica is still warming and Healthy the moment the gate opens, which is what actually keeps a cold replica out of the /health/ready rotation (.../Warmup/WarmupReadinessHealthCheckTests.cs:11); the warm-up hosted service must run every IWarmupTask once and open the gate even when a task fails or hangs (bounded by a per-task timeout), so a transient dependency outage cannot wedge a replica permanently out of rotation (.../Warmup/WarmupHostedServiceTests.cs:13, ADR-025); and SelfHttpWarmupTaskBase covers what the per-service copies each had to get right alone: port resolution under dynamic ports, the Testing short-circuit, the h2c version pin, and the non-fatal wrapper (.../Warmup/SelfHttpWarmupTaskBaseTests.cs:28). Three files pin deployment-shaped configuration and share a technique worth borrowing, assert on the plan a builder produced, never on a running dependency: KestrelEndpointExtensionsTests asserts the listener plan directly rather than through a bound server, because that plan decides whether a deployed revision answers its platform health probes at all (.../Kestrel/KestrelEndpointExtensionsTests.cs:14); DataProtectionExtensionsTests covers the two-stage gate where no DataProtection:BlobStorageUri leaves the in-memory default and takes no Azure dependency at startup while a configured URI swaps in the blob key-ring repository (.../DataProtection/DataProtectionExtensionsTests.cs:19); and KeyVaultConfigurationExtensionsTests runs its gate and malformed-configuration cases against a real HostApplicationBuilder while the cases that do add a source run against a builder double whose configuration merely collects them, because a real ConfigurationManager builds and loads every source the instant it is added and loading a Key Vault source means a live call to the vault (.../Configuration/KeyVaultConfigurationExtensionsTests.cs:27). Its gateway block covers the edge kit this package ships, the extension points ADC's Gateway host consumes rather than hand-writing (ADR-088): GatewayCorrelationMiddleware must leave a correlation ID on the request and the response, preserve a caller-supplied one end to end, and need nothing from DI, which is the whole reason it exists separately from the context-bound CorrelationIdMiddleware in MMCA.Common.API (.../Gateway/GatewayCorrelationMiddlewareTests.cs:15); the downstream health checks are pinned by name, tag and failure status, including that a repeated registration does not produce the duplicate check name the health-check service rejects at startup (.../Gateway/GatewayDownstreamHealthChecksTests.cs:17); the edge limiter covers its settings defaults, the bypass matcher, the fail-open posture on an unattributable client, and that registering twice does not throw (.../Gateway/GatewayRateLimitingTests.cs:21); and the CORS extension covers the allowed-origin policy a browser-facing edge needs (.../Gateway/GatewayCorsExtensionsTests.cs:19). Unit suite over the Aspire service-defaults package.
MMCA.Common.Aspire.Hosting.Tests 4 The AppHost-tier package, tested without ever starting a distributed application: every case builds a DistributedApplicationBuilder, applies the extension, and asserts on the resource model it produced. H2cHealthCheckExtensionsTests covers WithH2cHealthCheck, the probe an AppHost needs because an Http2-only cleartext service cannot answer an ordinary HTTP/1.1 health request: the probe sends HTTP/2 with prior knowledge at the configured path, the check is keyed by endpoint so one resource can gate two, registration is idempotent per builder but its duplicate guard is not shared across builders, an undeclared endpoint name registers lazily rather than throwing, and unreachable/non-success/timeout all report Unhealthy with the reason (MMCA.Common/Tests/Hosting/MMCA.Common.Aspire.Hosting.Tests/H2cHealthCheckExtensionsTests.cs:17). ServiceBusEmulatorBrokerTests covers the local broker swap: the emulator image is pinned to a 2.x tag, both planes (AMQP and management) are published, host ports are left to Aspire, the emulator waits for the SQL Server it needs, the connection string and admin endpoint are derived from the allocated endpoints, and WithBroker selects the Azure Service Bus transport while the RabbitMQ overload stays unchanged (.../ServiceBusEmulatorBrokerTests.cs:16). E2eLiftTests covers the rate-limit lift the E2E stack asks for, both triggers (environment variable and call-site flag), the write-nothing default, and that the emitted keys are derived from the section-name constants owned by the settings types rather than spelled by hand, which is why the two settings packages are referenced from the test project and deliberately not from the AppHost package itself (.../E2eLiftTests.cs:24, and the reasoning is written into .../MMCA.Common.Aspire.Hosting.Tests.csproj). Unit style, no Docker.
MMCA.Common.Gateway.Tests 7 The reusable gateway package extracted so ADC's Gateway host stops hand-writing its edge. AddMmcaGatewayTests pins the registration surface (both config filters and the transform provider), settings binding from the MmcaGateway section, the documented defaults when no section exists, and, as a fitness-style assertion inside the unit suite, that the Gateway assembly does not depend on MMCA.Common.Aspire, keeping the two hosting packages independently consumable (MMCA.Common/Tests/Hosting/MMCA.Common.Gateway.Tests/AddMmcaGatewayTests.cs:16). The two config filters are where the package earns its keep, because they are what let a route table live in configuration (ADR-089): GatewayClusterProfileConfigFilterTests covers defaults applied to a cluster that declares nothing, per-cluster overrides merged property by property, a cluster's own config beating both, and an invalid HTTP version or version policy failing with the cluster named (.../Configuration/GatewayClusterProfileConfigFilterTests.cs:14); GatewayHealthCheckDefaultsConfigFilterTests covers the passive defaults filled into a cluster with no health-check block, active checks staying off unless the host turns them on, and every explicitly written block being left exactly as written (.../Configuration/GatewayHealthCheckDefaultsConfigFilterTests.cs:13). Both filters assert that routes are left untouched, since a filter that quietly rewrote a route would be invisible until traffic misrouted. GatewayRoutePolicyTests covers the named per-route limiters: partitioning on the caller address for a client-IP policy, one shared bucket for a global one, fail-open on an unresolvable IP, a 429 rejection, an out-of-range budget throwing at registration rather than at the first request, and a real window that exhausts and then rejects (.../RateLimiting/GatewayRoutePolicyTests.cs:17). GatewayTraceHeaderTransformProviderTests covers the route/cluster trace headers stamped on every proxied request, including that a client-supplied value is replaced rather than appended to (.../Transforms/GatewayTraceHeaderTransformProviderTests.cs:15), and ForwardedHeadersExtensionsTests covers the three forwarded headers, the cleared known-proxy allow lists a container platform needs, and that the options are fresh per call so one host's customization cannot leak into another (.../ForwardedHeadersExtensionsTests.cs:16). Unit style.
MMCA.Common.Testing.Tests 27 The suite that tests the test framework itself, so a regression in the shared scaffolding fails here rather than silently weakening every consumer suite. HandlerTestBaseTests drives HandlerTestBase<THandler> exactly as a consumer handler test would, registering repositories and relying on the pre-wired unit of work (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/HandlerTestBaseTests.cs:13), and DecoratorPipelineOrderTests runs DecoratorPipelineOrderTestsBase<...> against MMCA.Common's own registration sequence to prove the resolved pipelines nest in the ADR-014 order (.../DecoratorPipelineOrderTests.cs:21). Four files cover the helpers the cross-service tiers lean on, each one because the behavior would otherwise only be observable in a Docker-bound nightly: CrossServiceFixtureBase's container-free logic (connection-string composition and the first-write-wins environment snapshot, .../CrossServiceFixtureBaseTests.cs:13); JwtTokenGenerator.ConfigureInProcessTokenValidation, where what matters is that JWKS/OIDC discovery is switched off and the static committed key takes over, because a test host that still tries to discover fails at the first authenticated request with a network error rather than an auth error (.../JwtTokenGeneratorTests.cs:19); TestPolling.PollUntilAsync, which must stop at the first satisfying probe and on timeout return the last probed value so the caller's own assertion produces the failure message instead of a bare timeout (.../TestPollingTests.cs:11); and DependencyInjectionAssert.ReturnsSameCollection, which has to fail for a registration that hands back a different collection, the failure mode that silently drops everything chained after it (.../DependencyInjectionAssertTests.cs:12). Its newest files are the gateway and broker scaffolding hoisted out of ADC: RecordingHttpForwarder, the fake that never proxies and echoes the destination, timeout, HTTP version and trace headers back so a consumer can assert its route table behaviorally (.../RecordingHttpForwarderTests.cs:16); MmcaGatewayHardeningTestsBase<TEntryPoint>, whose eight edge gates now live here once instead of per repo (.../MmcaGatewayHardeningTestsBaseTests.cs:20); MiddlewarePipelineOrderTestsBase, applied to the framework's own pipeline through a body-less subclass (.../MiddlewarePipelineOrderTests.cs:10); and ServiceBusEmulatorFixtureBase, the bounded, phase-named emulator startup a consumer subclasses (.../ServiceBusEmulatorFixtureBaseTests.cs:14). Two small helper surfaces round it out, the rate-limiter test extensions that let a suite drive a limiter without wall-clock waits (.../RateLimiterTestExtensionsTests.cs:17) and the feature-management extensions that flip a flag inside a test host (.../FeatureManagementTestExtensionsTests.cs:16). Unit style.
MMCA.Common.UI.Tests 127 Shared Blazor components (delete-confirmation, empty-state, the mobile card/infinite-scroll lists, notification bell/inbox/list/send pages, primitives), the MudBlazor theme/provider harness, HTTP-resilience/service-exception helpers, list-page state/query-state services, the primitive markup snapshots, the auth-form view-model validation (§24), and the i18n globalization pair (the [!!...!!] bracket-sentinel pseudo-localizer and the ResxMudLocalizer MudBlazor-chrome boundary) plus the auth-aware nav menu and its mobile top-row. On i18n (ADR-027 / §27): CultureSwitcherTests guards that the switcher delegates to ICultureApplier instead of navigating to /culture/set itself, since that URL is a server endpoint and a hard-coded navigation left MAUI Blazor Hybrid heads (which host no ASP.NET pipeline) routing it through the Blazor router onto the not-found page (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Globalization/CultureSwitcherTests.cs:17); EndpointCultureApplier covers the web default, which must route through /culture/set with a force load because only that round trip writes the cookie SSR prerender and the WASM runtime both read (.../Globalization/EndpointCultureApplierTests.cs:15); and DocumentLanguageTests covers the component that makes a hybrid head's lang attribute follow a culture switch, asserted here precisely because no automated accessibility gate can catch a wrong lang (axe checks presence and syntax only, .../Components/DocumentLanguageTests.cs:13). On theming, ThemeToggleTests covers the app-bar Day/Dark switch including that disposal unsubscribes so the long-lived ThemeService never calls back into a dead component (.../Components/ThemeToggleTests.cs:16, ADR-028), and ApiUserPreferenceWriterTests pins the two guards that keep a signed-out or stale session from spending one 401 per theme/culture toggle, a write that is best effort and whose caller never learns it failed, making a doomed request pure cost (.../Services/ApiUserPreferenceWriterTests.cs:16). On write safety, EntityServiceBaseIdempotencyRetryTests pins the Idempotency-Key EntityServiceBase<TEntityDTO, TIdentifierType> emits on creates and only on creates, the key staying identical across every retry of one logical operation, and the retry predicate's shape (5xx yes, 501 no, 429 yes) plus cancellation aborting the pipeline; the retried cases pay the real Polly backoff, so each is driven through the smallest attempt count that proves the behavior and asserts on captured attempts, never on wall-clock timing (.../Services/EntityServiceBaseIdempotencyRetryTests.cs:21). Its capability and JS boundary: LazyJsModule pins the single-flight import() the UI services delegate to, because an unguarded _module ??= await import(...) lets two concurrent callers each start an import so the browser holds two module instances and the later assignment leaks the earlier reference, while a failed import must not be cached and disposal must tolerate a disconnected circuit (.../Services/LazyJsModuleTests.cs:14); CapabilityFallbackTests sweeps every null/neutral capability default, each of which must degrade (report unsupported, no-op, or return empty) and never throw, because these are what shared components resolve on a head with no native or browser override (.../Services/Capabilities/CapabilityFallbackTests.cs:12, ADR-042); and QrCodeImageTests covers the managed QR render, a PNG data URI with required alt text, nothing at all for a blank payload, and a re-encode when the payload or error-correction level changes (.../Components/QrCodeImageTests.cs:11, ADR-071). Its newest work is the shared read/refresh plumbing and the account surface. LatestLoadGuard is the generation counter behind every "supersede an in-flight load" fix in both apps, pinned once here (.../Common/LatestLoadGuardTests.cs:11), and UiReadCache covers the opt-in client read cache pages share (.../Services/Caching/UiReadCacheTests.cs:15). ErrorSummaryTests covers the form-level error block that turns a failed Result into an accessible summary (.../Components/ErrorSummaryTests.cs:18, §24), ApiFileDownloadButtonTests the authenticated download path (.../Components/ApiFileDownloadButtonTests.cs:24), InfiniteScrollSentinelTests the intersection-observer sentinel behind the mobile lists (.../Components/InfiniteScrollSentinelTests.cs:14), HttpResultExecutorTests the single place a UI service turns an HTTP response into a Result (.../Services/HttpResultExecutorTests.cs:15), and AbsoluteUrlAttributeTests the validation attribute the share/QR surfaces depend on (.../Validation/AbsoluteUrlAttributeTests.cs:14). The multi-device session surface lands here as a page plus a parser: SessionsTests renders the sign-out-other-devices page in its loading, data, empty and error states (.../Pages/Auth/SessionsTests.cs:27) and UserAgentSummaryTests pins the user-agent-to-readable-device summary shown on each row, which is presentation over an attacker-controlled string and therefore must never throw on garbage (.../Services/Auth/UserAgentSummaryTests.cs:13, ADR-097). OfflineFirstPageSnapshotTests adds golden-markup coverage for the offline-first page shell (.../Pages/Common/OfflineFirstPageSnapshotTests.cs:14). Rendered with bUnit (component-render unit tests via BunitComponentTestBase).
MMCA.Common.UI.Web.Tests 4 The Blazor Server web-host pieces: ServerTokenStorageService (during SSR prerender tokens come from the HttpOnly session cookies; on the interactive circuit the access token is held in memory, hydrated single-flight, and refreshed proactively near expiry, while the refresh token is never readable, MMCA.Common/Tests/Presentation/MMCA.Common.UI.Web.Tests/Services/ServerTokenStorageServiceTests.cs), the server form-factor probe (.../Services/WebFormFactorTests.cs), and BlazorCspPolicyProvider, which pins the enforced production Content-Security-Policy verbatim (connect-src locked to the configured API/Gateway origin, no unsafe-eval, permissive Report-Only degradation on an unparseable endpoint, .../Security/BlazorCspPolicyProviderTests.cs, §26). Unit tests.
MMCA.Common.UI.E2E.Tests 18 Playwright axe-core (WCAG 2.1 AA) + render-smoke over the backend-less Gallery host, driven from GalleryAxeTestBase: real Login/Register pages, the primitives/components showcase, and the shared Notification pages against stubbed collaborators, plus the dark-mode toggle, a Web-Vitals budget probe, and two i18n/mobile-parity gates, a qps-Ploc pseudo-locale round-trip asserting the [!! sentinel and no horizontal overflow under roughly 40% text expansion (ADR-027 / §27) and the culture+theme controls pinned into the mobile top-row below 1024px (§22). StickySidebarE2ETests illustrates why a rendered-behavior assertion beats a CSS assertion: it keeps the desktop sidebar pinned while the page scrolls, a behavior that broke because position: sticky resolves against the nearest ancestor scroll container and two innocuous rules (html, body { overflow-y: auto } and .page { overflow-x: hidden }, where a non-visible overflow-x forces the computed overflow-y from visible to auto) turned content-sized ancestors that never scroll into dead scrollports; the test asserts the pinning, so it catches any future ancestor that reintroduces one (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Layout/StickySidebarE2ETests.cs:23). Its newest pages are the password-recovery pair (.../ForgotPasswordPageE2ETests.cs, .../ResetPasswordPageE2ETests.cs:9, ADR-091), the sessions page (.../SessionsPageE2ETests.cs:13), a grid page added to the Gallery so the shared data grid is scanned like every other primitive (.../GridPageE2ETests.cs:16), and AuthOutcomeRulesTests, which pins the outcome vocabulary the auth flows assert on so the browser tests and the components cannot drift apart (.../AuthOutcomeRulesTests.cs:12). Deliberately outside MMCA.Common.slnx; runs in CI's ui-e2e job across chromium, firefox and webkit. E2E/accessibility style. [Rubric §21, Accessibility] (assesses automated a11y gating): this is where the framework proves zero axe violations before downstream apps consume the pages.
MMCA.Common.Benchmarks 6 A BenchmarkDotNet performance-smoke executable covering the two DB-free hot paths. SpecificationBenchmarks measures the per-instance compiled-expression cache behind Specification<TEntity, TIdentifierType>.IsSatisfiedBy (a cached-compile baseline versus the recompile-each-call anti-pattern) and the And/Or composition cost (MMCA.Common/Tests/Performance/MMCA.Common.Benchmarks/SpecificationBenchmarks.cs:14); QueryPipelineBenchmarks adds the read side, which runs on every list request in every consumer and regresses silently because the dynamic-LINQ predicate is re-parsed per call and the shaper reflects over the DTO: a single CONTAINS filter, a three-strategy mixed filter, dynamic sorting, and full-field versus sparse-fields= shaping of a 100-row page (.../QueryPipelineBenchmarks.cs:17). Deliberately outside MMCA.Common.slnx (like the Gallery), but not on-demand-only: CI's performance-smoke job (MMCA.Common/.github/workflows/ci.yml:332) runs the suite and then build/perfgate compares the results against the committed Tests/Performance/perf-baseline.json, failing on any violation of its allocation ceilings or machine-independent ratio floors, and on a rule naming a benchmark that produced no measurement, so the gate cannot pass vacuously (.../ci.yml:371). Moving a number deliberately means updating the baseline in the same PR. [Rubric §12, Performance & Scalability] (assesses measured, not assumed, hot-path cost): this is the evidence harness, and the baseline file turns it from a runs-clean smoke into a regression gate. Performance-smoke style.

MMCA.ADC, Conference module (the largest application module)

Test project (assembly) Types What it covers · style
MMCA.ADC.Conference.Shared.Tests 17 Conference DTOs, requests, enums, and DTO/request mappers (the manual-mapping/Mapperly boundary, ADR-001). The DTO tests are deliberately shallow and cheap, pinning the required-value constructor, record equality, and the empty-collection defaults that keep a consumer from null-checking every navigation (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Shared.Tests/Events/EventDTOTests.cs:6). Pure unit tests.
MMCA.ADC.Conference.Domain.Tests 28 The Conference aggregates (Event, Session, Speaker, Room, Category, Question/Answer, Sponsor and now Activity): factory-method Result<T> outcomes, invariants, state transitions, and emitted domain events. SponsorTests is the model for the newer files: a database-generated id stays default after Create, exactly one SponsorChanged added-state event is raised, each length/blank invariant fails on its own, a booth number without the exhibitor flag is accepted and kept rather than silently dropped, and the tier values are pinned in package display order because the public roster groups by tier and reordering the enum would silently reorder the page (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Domain.Tests/Sponsors/SponsorTests.cs:11). The Activity aggregate (the non-session agenda items on the public schedule) follows the same shape, split across a behavior file and a dedicated invariants file (.../Activities/ActivityTests.cs:10, .../Invariants/ActivityInvariantsTests.cs:6), with a builder in .../Builders/ActivityBuilder.cs so the Application suite constructs one the same way. EventCascadeDeletionDomainServiceTests is the one to read for ordering: the cascade must soft-delete sessions, rooms, sponsors and activities before the event, must stop at the first child failure, and must leave the event undeleted when it does, so a partially cascaded delete is never committed (.../Services/EventCascadeDeletionDomainServiceTests.cs:9). Unit tests.
MMCA.ADC.Conference.Application.Tests 165 The command/query handlers for the Conference controllers, validators, navigation populators, the Sessionize import orchestrator + sync strategies (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Application.Tests/Events/UseCases/RefreshFromSessionize/RefreshFromSessionizeHandlerTests.cs:16), and the event/session live-window validation served to the live layer over gRPC (.../Events/EventLiveValidationServiceTests.cs:17, alongside GetPublicSessionFilterHandler and its cross-source filter query, ADR-018). It carries the AI-scoring background queue (.../Sessions/DecisionSupport/SessionScoringQueueTests.cs:11, SessionScoringQueue) and the sessions-by-speaker filter handler, whose specification resolves the SessionSpeaker join down to an engine-portable ID-list criteria so the speaker pages and speaker dashboard filter server-side instead of pulling the whole catalog (.../Sessions/UseCases/GetSessionsBySpeakerFilter/GetSessionsBySpeakerFilterHandlerTests.cs:16, GetSessionsBySpeakerFilterHandler, §12). A family of public filter handlers (session, session-speaker, session-category-item, event-speaker, speaker, speaker-category-item, room, sponsor, and now activity) all defend the same anonymous-endpoint leak: a junction filter must select only rows whose parent is publicly visible, so a hidden session never leaks its existence through the speakers assigned to it (.../Sessions/UseCases/GetPublicSessionSpeakerFilter/GetPublicSessionSpeakerFilterHandlerTests.cs:18, and its activity twin at .../Activities/UseCases/GetPublicActivityFilter/GetPublicActivityFilterHandlerTests.cs:18, BR-49, §11). ExportEventCalendarHandler covers the whole-schedule .ics endpoint, which is anonymous, so unpublished and unknown events must collapse to the same NotFound (no existence oracle) and a legacy row carrying an unresolvable time zone must degrade rather than fault the request (.../Sessions/UseCases/ExportCalendar/ExportEventCalendarHandlerTests.cs:16). The sponsor and activity use cases are deliberately plain: create/update handlers assert the mapper-failure path, the repository add and the save, and nothing more, because the interesting behavior lives in the aggregate and in the public filter (.../Sponsors/UseCases/CreateSponsorHandlerTests.cs:14, .../Activities/UseCases/CreateActivityHandlerTests.cs:13). Its newest files are convention guards rather than behavior: SessionRoomFilterTests pins the public schedule's Room filter at the Application layer, since Session.RoomId is a real nullable column riding the generic filter pipeline with no dedicated handler, so the feature would break silently if the property were renamed or retyped (an unknown filter key is a validation failure and a mistyped one is ignored at apply time, .../Sessions/SessionRoomFilterTests.cs:15); ConferenceCrudRegistrationTests sweeps the module's CRUD registrations so a new aggregate cannot ship half-wired (.../ConferenceCrudRegistrationTests.cs:32); and BatchAddSessionQuestionAnswersHandlerTests covers the batched answer write behind the organizer feedback screens (.../Sessions/UseCases/BatchAddSessionQuestionAnswers/BatchAddSessionQuestionAnswersHandlerTests.cs:24). The biggest application suite in ADC; fast unit tests with mocked repositories/services.
MMCA.ADC.Conference.Infrastructure.Tests 15 Conference-specific EF configurations, the module DB seeder, the Sessionize HTTP client, and the Anthropic-backed session-scoring service (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Services/AnthropicScoringServiceTests.cs:13). SessionScoringProcessor covers the cross-replica scoring lock and the bounded retry around it: the queue's own dedup is process-local while Conference runs two replicas, so what matters is that exactly one replica invokes the (paid) scoring handler for an event, that the lock handle is always disposed, and that a failed run is retried a bounded number of times rather than dropped or repeated forever (.../Services/SessionScoringProcessorTests.cs:23, §31). SessionScoringSweepJobTests covers the sweep's decision, which is the whole point of that job: scoring is paid, so enqueuing an event nobody triggered, or re-triggering one forever, costs real money, and the cases pin the three answers the sweep must give (finish an interrupted run, never start an untriggered one, never grind on a stale one) plus the queue's own dedup underneath it (.../Services/SessionScoringSweepJobTests.cs:19, §31). The scoring evaluation material is deliberately not here: the golden-replay, live-judge and prompt-contract tests sit in their own MMCA.ADC.Conference.Scoring.Evaluation.Tests project (the next row) so the paid, key-dependent tier can be gated on its own, which leaves this project the fast unit coverage of the client, the processor and the sweep (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Services/SessionizeServiceTests.cs, .../Services/SessionScoringProcessorTests.cs, .../Services/SessionScoringSweepJobTests.cs) plus the module's EF configuration and seeder (.../Persistence/ConferenceEntityConfigurationTests.cs, .../Seeding/ConferenceModuleDbSeederTests.cs). Unit suite over faked HTTP handlers and mocked locks.
MMCA.ADC.Conference.API.Tests 20 Conference REST controllers (events, sessions, speakers, rooms, categories, questions/answers, session selection, sponsors, activities), the module's permission grants, and the Conference error-resource localization completeness check (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.API.Tests/Localization/ConferenceErrorResourcesTests.cs:15, §27). The controller tests follow one shape, visible in SponsorsControllerTests and repeated by the new ActivitiesControllerTests: success and each failure ErrorType map to their status code, and a successful mutation evicts the entity and conference cache tags while a failed one evicts nothing (.../Controllers/SponsorsControllerTests.cs:27, .../Controllers/ActivitiesControllerTests.cs:27). EntityExportAuthorizationTests is the cross-controller guard that came with ADR-078: an action inherited from EntityControllerBase<TEntity, TEntityDTO, TIdentifierType> carries no knowledge of the derived controller's read scoping, and every Conference read filter is applied in a method body rather than as an attribute, so the test pins that every scoped controller declares its own ExportAsync and that export on the bare-[Authorize] controllers carries its capability (.../Controllers/EntityExportAuthorizationTests.cs:18, §11). ConditionalWriteConventionTests pins wiring rather than behavior: the conditional-write behavior lives in the framework filter (covered in MMCA.Common.API.Tests), so what is worth pinning here is that the event lifecycle transitions keep [SupportsIfMatch] and keep a concurrency-aware request body, since an endpoint that quietly lost either would answer 200 to a caller who explicitly asked to write only against the version they read, with nothing failing to say so (.../Controllers/ConditionalWriteConventionTests.cs:14, ADR-035). Unit tests of the API layer.
MMCA.ADC.Conference.UI.Tests 53 Conference Blazor pages and components: the public event/session/speaker/sponsor/activity detail + filtered list pages, the management CRUD forms and management-route authorization, the organizer feedback dashboards, the speaker dashboard, the session-selection dashboard with its AI-score and speaker-overlap views (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Sessions/Selection/SessionSelectionAiScoresTests.cs:15), the home countdown (.../Pages/Home/ADCHomeTests.cs:20), and the share/QR/add-to-calendar buttons. One cluster is all stale-state guards, the failure class bUnit is uniquely good at catching: SessionDetailRoomCacheTests pins that navigating to a session in a different event refetches the per-event room lookup, since otherwise the page renders the previous event's room names and offers its rooms in the edit picker, while navigating within the same event must reuse the cache (.../Pages/Session/SessionDetailRoomCacheTests.cs:20); SessionSelectionStaleResponseTests covers M48, where the organizer dashboard fires a load per event selection and keeps a fire-and-forget score-polling loop running, and both used to write whatever came back into shared state without checking whether the selection had moved on, so a slow response for the previous event could overwrite the board or paint an error banner over it (a generation counter now supersedes in-flight work on every selection, .../Pages/SessionSelection/SessionSelectionStaleResponseTests.cs:23). The sponsor, activity and speaker-QR pages are the newest: PublicSponsorListTests pins the roster scoping to the current-or-next event, grouping by tier in package order with a sort-then-name order inside a tier, the booth badge for exhibitors, and the sponsorship-packet call to action that must appear when the roster is empty and disappear entirely when no packet URL is configured (.../Pages/Public/PublicSponsorListTests.cs:18); PublicActivityListTests does the same for the activity strip on the public schedule (.../Pages/Public/PublicActivityListTests.cs:16); and SpeakerQrTests pins that with the speaker_id claim the code encodes the absolute public profile URL (an app-relative route is useless to another device's camera) and that without the claim the page says so rather than rendering a code that points nowhere (.../Pages/Speaker/SpeakerQrTests.cs:17, ADR-071). Its newest technique is golden-markup: ComponentsSnapshotTests renders each reused Conference component from parameters alone and diffs its normalized markup against a committed baseline under Snapshots/, normalizing the per-render GUIDs MudBlazor injects so the comparison stays deterministic and OS-independent, and refreshed with UPDATE_SNAPSHOTS=1 after an intentional change (.../Components/ComponentsSnapshotTests.cs:27, §28). Alongside it, EventFilteredListPagePrerenderTests pins the prerender pass of the shared filtered-list page base (.../Pages/Common/EventFilteredListPagePrerenderTests.cs:20) and ClientUrlValidationTests pins the client-side URL validation the share and external-link surfaces depend on (.../Pages/ClientUrlValidationTests.cs:23). Rendered with bUnit (BunitTestBase over the shared BunitComponentTestBase). Component tests.
MMCA.ADC.Conference.IntegrationTests 37 Boots the Conference service host via WebApplicationFactory<Program> (gRPC peers faked, JWT re-pointed at an in-process test key) and drives real HTTP per role (Anonymous/Attendee/Speaker/Organizer), plus OpenAPI contract-snapshot, API-versioning, optimistic-concurrency, soft-delete + audit-stamp fidelity, idempotency replay, output-cache eviction, the includeChildren regression, and the in-process CrossServiceUserRegisteredTests (the Identity-to-Conference UserRegistered auto-link handler). SpeakerFeedbackAuthTests is a plain authorization matrix and a good example of what this tier is for: it covers GET /Speakers/{speakerId}/sessions/{sessionId}/feedback, a speaker's own read of free-text comments gated self-or-organizer, written as the regression for a finding where the endpoint was briefly [AllowAnonymous] with a public output cache, so any caller could read (and publicly cache) any speaker's feedback by URL manipulation (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Speaker/SpeakerFeedbackAuthTests.cs:15, §11). Its output-cache pair asserts eviction behaviorally, by the changed response body rather than by reading the cache store, and also pins that an authenticated read carries an Authorization header so the default policy skips the cache entirely (.../Reads/OutputCacheEvictionTests.cs:25). Integration style; needs a real SQL Server (ADC_TEST_SQL_BASE), runs in the integration-tests CI job.
MMCA.ADC.Conference.Scoring.Evaluation.Tests 11 The AI-evaluation suite for session scoring, in its own project so that the paid, key-dependent half can be gated apart from CI. A JSON corpus under Golden/ (seven recorded cases plus prompt-versions.json) feeds three files. GoldenReplayTests replays every case through the real AnthropicScoringService against a handler that returns the response recorded for that case, so no API key and no network are involved and it runs on every CI leg; it pins both directions at once, what goes out (the delimited envelope and the untrusted-input brief, including on the no-speakers case and the injection attempt) and what comes back (a recorded response still parses, still succeeds, and still produces the same weighted overall inside the band the case declares), so a change to prompt assembly or to the weighting math fails with the case that noticed it named (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Scoring.Evaluation.Tests/GoldenReplayTests.cs:25). LiveJudgeTests scores the same proposals through the real Anthropic API, which is what catches the failures a replay cannot (a model deprecation, a structured-output contract change, a prompt edit that reads fine but scores everything a point lower); it costs money and needs a key, so it is trait-gated (Category=AiEval.Live) and skips itself when ANTHROPIC_API_KEY is absent, and its bands are deliberately generous (roughly plus or minus 1.5 around the recorded value, clamped to the 1.0-10.0 range) because a judge model is not deterministic and a tight band would produce a flaky gate that gets ignored (.../LiveJudgeTests.cs:27, §31). PromptContractTests renders the system brief plus the user message for one canonical proposal fixed in the test file (not read from the corpus, so editing a case cannot move what the hash covers), hashes it, and compares against Golden/prompt-versions.json: a prompt edit without a version bump fails, and a version bump with no recorded hash fails too, which is what makes the PromptVersion persisted next to every score mean anything (.../PromptContractTests.cs:29). The corpus record and its nested input/speaker/expectation shapes are in GoldenCase.cs:11,54,70,86, and TestMeterFactory is a real IMeterFactory that exists only because the scoring service records its token counters through one (.../TestMeterFactory.cs:11). Unit (replay) plus a gated live-model evaluation tier.

MMCA.ADC, Engagement module (bookmarks, feedback, the conference-day live layer, and the badge/points wave)

Test project (assembly) Types What it covers · style
MMCA.ADC.Engagement.Shared.Tests 7 Bookmark/feedback/live DTOs, requests, and mappers, plus the contract types the ADR-072 wave added. BadgePayloadTests pins the QR payload both ways: the mmca-adc:badge:{credential} format, a case-insensitive parse that also accepts a bare or dash-less GUID and tolerates surrounding whitespace (a camera hands over whatever it read), and a clean false plus an empty credential for untrusted garbage (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Shared.Tests/CheckIns/BadgePayloadTests.cs:6). PointsSubjectKeysTests pins the ledger's subject keys, whose whole job is to not collide: event, session and sponsor keys built from the same numeric id must stay distinct, and each must be invariant-culture stable, because the key is a persisted uniqueness constraint and a culture-dependent number format would silently create a second awardable subject (.../Points/PointsSubjectKeysTests.cs:7). The settings and scope-name types round out the row. Unit.
MMCA.ADC.Engagement.Domain.Tests 11 The UserSessionBookmark, event/session feedback, and conference-day live-layer aggregates (LivePoll + SessionQuestion), joined by the badge/points aggregates: factory Result<T> outcomes, invariants, and domain events. CheckIn is the richest, because one aggregate carries three scopes and the scope decides which optional id is required and which is forbidden: a session scope needs a session id, an event scope must not carry one, a sponsor scope needs a sponsor id and no session, and exactly one AttendeeCheckedIn event is raised carrying (or deliberately not carrying) the sponsor payload (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Domain.Tests/CheckIns/CheckInTests.cs:8). AttendeeBadge pins the opaque credential: Create generates one, two calls for the same user never produce the same credential, Regenerate replaces it while preserving the owner, and no domain event is raised either way, since issuing a badge is not an engagement act (.../Badges/AttendeeBadgeTests.cs:6). LeaderboardOptIn pins the display-name snapshot taken at opt-in, the soft-delete leave, and the reactivate path that refreshes the name and raises the added state again, while a failed invariant leaves the entity untouched and raises nothing (.../Points/LeaderboardOptInTests.cs:8); PointsEntryInvariantsTests covers the ledger row's guards field by field (.../Points/PointsEntryInvariantsTests.cs:7). Unit.
MMCA.ADC.Engagement.Application.Tests 66 Bookmark, feedback, and live-layer (poll / session-question) add/remove/query handlers and validators, including the cross-module ISessionBookmarkValidationService / IBookmarkCountService / IEventLiveValidationService gRPC collaborators (stubbed), the poll-results builder (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Application.Tests/LivePolls/Services/LivePollResultsBuilderTests.cs:22), the read-side poll handlers added with the presenter surface (.../LivePolls/UseCases/GetPollResultsHandlerTests.cs:20), the question-upvote domain-event handler (.../SessionQuestions/DomainEventHandlers/SessionQuestionUpvoteChangedHandlerTests.cs:25), and the best-effort ILiveChannelPublisher ingress together with the queue that decouples it from the request path (.../Live/LiveChannelPublishQueueTests.cs:10, LiveChannelPublishQueue). The ADR-072 wave contributes three load-bearing files. PointsAwarder is where anti-farming and redelivery idempotency turn out to be the same rule: an award already present writes nothing, a rule configured to 0 (or an undefined activity) succeeds and writes nothing, an entry keeps the value it was awarded when the configuration later changes, and a save that loses the unique-index race reports success, including when the duplicate wording is buried in an inner exception, while any other save failure propagates (.../Points/Services/PointsAwarderTests.cs:13). CheckInAttendeeHandler pins the scan path: a repeat scan reports the original check-in and writes nothing, a session-scoped scan is filed under the session's own owning event (not the client's event context), an unknown and a malformed credential return the same BadgeNotFound (no probing oracle), and an unpublished event or a missing caller is refused (.../CheckIns/UseCases/CheckInAttendeeHandlerTests.cs:17). GetLeaderboardHandler pins the board as opt-in only (points without an opt-in never appear, and leaving removes you), ordering by total with an ordinal name tie-break, truncation to the configured size where 0 or a negative publishes nothing rather than throwing, and the snapshotted name being what is published (.../Points/UseCases/GetLeaderboardHandlerTests.cs:12). UserDeletedPointsHandlerTests closes the privacy loop: on UserDeleted the entry comes off the board and the published name is erased even for an already-left opt-in, the read deliberately looks past the soft-delete filter and tracks, and a redelivery reaches the same state while writing once (.../Points/IntegrationEventHandlers/UserDeletedPointsHandlerTests.cs:14, §30). Its newest file is the cross-service cache edge: bookmark counts are owned here but served by Conference, in another process whose output cache this side cannot touch directly, so UserSessionBookmarkCacheEvictionHandlerTests pins the one thing that closes that distance, that every bookmark change publishes an eviction request carrying the tag Conference actually registered, and that a broker which is down still cannot fail a bookmark the attendee already saved (.../UserSessionBookmarks/DomainEventHandlers/UserSessionBookmarkCacheEvictionHandlerTests.cs:20, ADR-026). Unit.
MMCA.ADC.Engagement.Infrastructure.Tests 4 Engagement EF configuration plus the live-channel publish processor that fans domain changes out to the SignalR hub (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Infrastructure.Tests/Live/LiveChannelPublishProcessorTests.cs:10). Unit.
MMCA.ADC.Engagement.API.Tests 10 The Bookmarks/Feedback/Live/CheckIns/Points REST controllers in isolation. CheckInsControllerTests shows the two rules that matter at this edge: GetMyBadgeAsync takes no parameter other than the cancellation token, so the identity can only come from the token and never from the request body (asserted by reflection, because adding a parameter would break nothing loudly), and a repeat scan is Ok, not Conflict, because the organizer scanning a badge twice is a normal event and an error status would train them to ignore it (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.API.Tests/Controllers/CheckInsControllerTests.cs:23). ConditionalWriteConventionTests does for Engagement what its Conference twin does for events, on the endpoints two people actually race in a live room: poll open/close and the three question moderation decisions, where an endpoint that quietly lost [SupportsIfMatch] would let a moderator's explicit precondition pass unenforced (.../Controllers/ConditionalWriteConventionTests.cs:13, ADR-035). Its newest file pins the owner-or-admin filter vocabulary, the exact policy and claim names the ownership filters are constructed with, because a typo there fails open rather than loud (.../Authorization/OwnerOrAdminFilterVocabularyTests.cs:23, §11). Unit.
MMCA.ADC.Engagement.UI.Tests 36 Engagement Blazor renders and their UI services: the bookmark UI, the session/event feedback pages (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.UI.Tests/Pages/Feedback/SessionFeedbackTests.cs:26), the conference-day live/presenter surfaces (Happening Now, live poll, session Q&A, the moderation panel), the live-channel join/reconnect path (.../Pages/LiveChannelJoinTests.cs:40), and the session-reminder planner/coordinator. LiveEventListenerResilienceTests covers H18, where an invisible layout component rendered on every authenticated page sits under a shell with no ErrorBoundary above it, so anything escaping its render lifecycle tears down the Blazor circuit (a full page reload) on whatever page the user happens to be on, and the shared resilience handler raises TimeoutRejectedException/BrokenCircuitException that the service layer's narrow HttpRequestException catch deliberately does not cover (.../Components/LiveEventListenerResilienceTests.cs:34, §29); NowNextService covers the anonymous now-next endpoint contract plus its not-found degradation. The badge/points surfaces are the newest. CheckInScanTests is the load-bearing one: on a head whose IBarcodeScannerService.IsSupported is false (web and Windows) the scan affordance must not render at all while the manual name/email search stays, because that search is the whole check-in surface there, and the scope toggle must default to Session (.../Pages/CheckIn/CheckInScanTests.cs:20, ADR-042 / ADR-072). MyBadgeTests and MyPointsTests pin the four-state render (loading, data, empty, error) for the attendee's own badge and ledger, including the leaderboard rank block and the participation switch reflecting the opt-in state on load (.../Pages/CheckIn/MyBadgeTests.cs:19, .../Pages/Points/MyPointsTests.cs:19), SponsorVisitTests pins that a repeat visit reports the earlier one rather than an error, with distinct states for an unpublished event, an unrecognized server answer, and a failed post (.../Pages/Sponsors/SponsorVisitTests.cs:19), and the organizer rollups plus the room-code page have their own files (.../Pages/CheckIn/OrganizerAttendanceTests.cs:17, .../Pages/Rooms/RoomCheckInTests.cs:18). CurrentEventNotificationScopeProvider covers the event:{EventId} key shape, the never-throw degradation to unscoped, and the five-minute cache that keeps the notification bell's 30-second poll from costing an events fetch every time (.../Services/CurrentEventNotificationScopeProviderTests.cs:15, §31). Its newest three are the check-in service the scan pages call (.../Services/CheckInServiceTests.cs:15), the app-action route map that turns a notification payload into a destination route (.../Services/AppActionRouteMapTests.cs:20), and a golden-markup snapshot suite over the live-room components, where the committed baselines under Snapshots/ cover the poll card, the moderation panel, the question panel and the attendee search panel (.../Components/ComponentsSnapshotTests.cs:35, §28). Component (bUnit).
MMCA.ADC.Engagement.IntegrationTests 22 Boots the Engagement service host via WebApplicationFactory<Program> and exercises the bookmark/feedback/live workflows + authorization over real HTTP, with the badge/points tier alongside. CheckInScanRoundTripTests proves over a real database what the handler tests prove in memory: a second scan reports the repeat and leaves one row and one enqueued AttendeeCheckedIn, a stale event context is corrected to the session's own event, and MyBadge returns the same credential on every call (MMCA.ADC/Tests/Integration/MMCA.ADC.Engagement.IntegrationTests/CheckIns/CheckInScanRoundTripTests.cs:24). PointsAwardRoundTripTests is the one to read for the outbox idiom: the check-in and its event commit in one transaction, so each test then drains the outbox the way OutboxProcessor does, reading the persisted payload back out of dbo.OutboxMessages, deserializing it, and handing it to the registered handler, which keeps the assertions deterministic instead of racing a background service, and it pins that a redelivery still leaves exactly one entry and that the unique index rejects a second award for the same subject (.../Points/PointsAwardRoundTripTests.cs:41). CheckInAuthorizationTests states the authorization contract plainly: every attendee may fetch their own badge (identity from the token, never the request), while writing a check-in and reading the attendance rollup require engagement:checkin:manage, held only by Organizer and Admin, and anonymous callers get 401 everywhere (.../CheckIns/CheckInAuthorizationTests.cs:14, §11). Integration; real SQL Server, integration-tests CI job.

MMCA.ADC, Identity module (User aggregate + JWT/JWKS + external OAuth)

Test project (assembly) Types What it covers · style
MMCA.ADC.Identity.Shared.Tests 3 Identity DTOs/requests and mappers (User, roles, LinkedSpeakerId). Unit.
MMCA.ADC.Identity.Domain.Tests 4 The User/UserRole aggregate factories, invariants, anonymization, and speaker-linking domain events. Unit.
MMCA.ADC.Identity.Application.Tests 30 Registration/login/profile/role/preferences handlers and validators, the external-OAuth (Google/GitHub) exchange, the SpeakerLinkedToUser/SpeakerUnlinkedFromUser integration-event handlers, and the attendee query service the Notification module reads recipients through. The data-subject export block (ADR-076 / §30, ADC still runs its own handler rather than the framework's endpoint): ExportUserDataHandlerTests pins the owner-or-organizer gate with a case-insensitive role claim, the aggregation of the Engagement and Notification sections when the peers answer, the per-section unavailable degradation when a peer is unreachable or throws so the rest of the package still travels, and UTC-kind timestamps out of DateTimeKind.Unspecified audit columns (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Application.Tests/Users/UseCases/ExportUserData/ExportUserDataHandlerTests.cs:16); EngagementUserDataExportSectionTests pins what the Engagement section actually carries, including bookmarks, submitted questions, points entries with the readable activity name, leaderboard participation, and check-ins that earned no points (a check-in is personal data whether or not it scored, .../Users/UseCases/EngagementUserDataExportSectionTests.cs:11); and ExportUserDataRegistrationTests pins that the handler is discovered through its base class interface by the module scan and that section registration order is preserved (.../Users/UseCases/ExportUserDataRegistrationTests.cs:16). Its newest files are the app-side halves of two framework waves: the password-recovery handlers over the shared bases (.../Users/UseCases/ForgotPasswordHandlerTests.cs:22, .../Users/UseCases/ResetPasswordHandlerTests.cs:19, ADR-091), an in-memory refresh-session store used as the test double for the multi-device surface (.../Support/InMemoryRefreshSessionStore.cs, ADR-097), and the avatar read/remove handlers (.../Users/UseCases/GetUserAvatarHandlerTests.cs:15, .../Users/UseCases/RemoveUserAvatarHandlerTests.cs:16, ADR-045). Unit.
MMCA.ADC.Identity.Infrastructure.Tests 9 Identity EF config/repository, RS256 token issuance, and the JWKS provider. Its newest file is RefreshSessionModelGateTests, the consumer-side proof that the framework's refresh-session table appears in this host's model only when the feature is enabled, so a host that never opted in keeps exactly the migrations it had (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Infrastructure.Tests/Persistence/RefreshSessionModelGateTests.cs:46, ADR-097). Unit.
MMCA.ADC.Identity.API.Tests 8 The Auth REST controller, the Users controller, the JWKS endpoint, and identity middleware in isolation, plus the data-export controller that fronts ADC's own DSAR handler (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.API.Tests/Controllers/UsersDataExportControllerTests.cs:20, §30). Unit.
MMCA.ADC.Identity.UI.Tests 7 Identity Blazor pages (login/register/profile/user-management) rendered with bUnit, now with a golden-markup snapshot suite over the user-claims component (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Components/ComponentsSnapshotTests.cs:15, §28). Component.
MMCA.ADC.Identity.IntegrationTests 34 Boots the Identity service host via WebApplicationFactory<Program> and drives the full auth surface over real HTTP: registration, login and its anonymous edge cases, claims, profile, user preferences, soft-deleted-user handling, the external-OAuth challenge/exchange, GDPR user export against faked Engagement and Notification peers (MMCA.ADC/Tests/Integration/MMCA.ADC.Identity.IntegrationTests/Attendee/UserExportTests.cs:24), and JWKS discovery. It also carries the two contract guards (OpenAPI snapshot and the RFC 9457 Problem Details subclass over ProblemDetailsContractTestsBase<TFixture>, .../Contract/ProblemDetailsContractTests.cs:16, §9), the compliance pair that proves erasure works end to end and that PII never reaches the log pipeline (.../Compliance/ErasureAndPiiLoggingTests.cs:19, ADR-005 / §30), an outbox-fidelity guard asserting registration atomically enqueues UserRegistered into [dbo].[OutboxMessages] (.../Data/OutboxFidelityTests.cs:17), the in-process CrossServiceSpeakerLinkTests, and the newest addition, PasswordResetFlowTests, which walks forgot-then-reset end to end against a real database and a real token store so the anti-enumeration answers and the single-use token are proved where they actually ship (.../Auth/PasswordResetFlowTests.cs:18, ADR-091). Integration; real SQL Server, integration-tests CI job.

MMCA.ADC, Notification module (push + inbox on top of the framework's notification types)

Test project (assembly) Types What it covers · style
MMCA.ADC.Notification.API.Tests 1 NotificationModuleTests pins the module contract itself, and it is a five-member subclass of the shared ModuleConformanceTestsBase<TModule>: it declares only ExpectedName ("Notification"), ExpectedDependencies (["Identity"]), ExpectedRequiresDependencies (true), and an AssertDisabledStubs override that proves RegisterDisabledStubs keeps the cross-module IUserNotificationExportService resolvable as a singleton DisabledUserNotificationExportService when the module is switched off (MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.API.Tests/NotificationModuleTests.cs:8). The conformance assertions themselves live in the base. Unit.
MMCA.ADC.Notification.Application.Tests 5 The module's two application services plus its DI registration: AttendeeNotificationRecipientProvider resolving broadcast recipients through the Identity IAttendeeQueryService gRPC contract (MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.Application.Tests/AttendeeNotificationRecipientProviderTests.cs:7), UserNotificationExportService assembling a data-subject export from the user-notification and push-notification repositories over InMemoryQueryableExecutor on top of HandlerTestBase<THandler> (.../UserNotificationExportServiceTests.cs:12, §30), and AddModuleNotificationApplication proving both are registered against their interfaces (.../DependencyInjectionTests.cs:10). Unit.
MMCA.ADC.Notification.IntegrationTests 9 Boots the Notification service host via WebApplicationFactory<Program> (the Identity recipient-lookup gRPC client faked by FakeAttendeeQueryService) and exercises the push-notification REST endpoints + inbox (NotificationsController/InboxController from MMCA.Common.API, MMCA.ADC/Tests/Integration/MMCA.ADC.Notification.IntegrationTests/Notifications/NotificationControllerTests.cs:16), the two contract guards (an OpenAPI snapshot at .../Contract/OpenApiContractTests.cs:16 and the Problem Details subclass at .../Contract/ProblemDetailsContractTests.cs:15, §9), and the real-time SignalR NotificationHub: a live HubConnection asserts authenticated connect, anonymous rejection (the hub carries [Authorize]), and a POST-triggered broadcast reaching the connected recipient (.../Notifications/NotificationHubTests.cs:15). Integration; real SQL Server (ADC_TEST_SQL_BASE), integration-tests CI job.

MMCA.ADC, host, service-adapter, cross-service, and end-to-end suites

Test project (assembly) Types What it covers · style
MMCA.ADC.Gateway.Tests 8 Boots the real YARP Gateway host in-process and asserts its operational guarantees, most of them with no test body in this repo at all: the host fixture and the gate bodies come from MMCA.Common.Testing, so ADC supplies only the entry point and its own route-table facts. SecurityHeadersTests is a four-line subclass of SecurityHeadersTestsBase that hands it a client from the shared ProductionHostApplicationFactory<Program> class fixture (MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/SecurityHeadersTests.cs:12); the base probes /alive, which always answers regardless of backend reachability, and pins X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, frame-ancestors 'none' and HSTS (§26). GracefulShutdownTests is a body-less subclass of GracefulShutdownTestsBase<TEntryPoint> closed over the Gateway's Program (.../GracefulShutdownTests.cs:9): the base boots the host, stops it under a bounded token, and asserts ApplicationStopping then ApplicationStopped fired, so a hosted service that refuses to drain fails here instead of silently wedging a rolling deploy (§29). RouteMapTests is the one to read. Program.cs no longer hand-maps forwarders: it loads the ReverseProxy configuration section and applies the MMCA.Common.Gateway config filters over it (ADR-089), so adding or repointing a route is an appsettings.json edit. A config-owned table can drift in two directions, so the suite has two gates over one pinned table (.../RouteMapTests.cs:118). Behavioral: every pinned route is driven through the real proxy pipeline with IHttpForwarder replaced by RecordingHttpForwarder, which echoes the destination prefix, the activity timeout, and the forwarded HTTP version and version policy into response headers, so a removed or repointed route, a dropped timeout or a lost h2c setting fails there (.../RouteMapTests.cs:159), and an unmapped path must 404 at the gateway rather than be swallowed by a catch-all (:370). Completeness: the loaded IProxyConfig is compared against the same table, routes (:192) and clusters (:214), so a route ADDED to configuration without a test fails too. The table today is 27 routes across 5 clusters: four identity, sixteen conference, five engagement, two notification. Five clusters rather than four precisely because a YARP cluster owns the request config, so the REST 100-second activity timeout (MMCA.Common's shared 90-second request budget plus a deliberate 10-second margin, so the forwarder outlives the backend's own budget and the client sees the backend's error rather than a gateway abort, .../RouteMapTests.cs:81), the SignalR hub's long timeout, and the h2c prior-knowledge version pin on the three Http2-only backends cannot share one; the REST budget is now declared once as MmcaGateway:ClusterRequestDefaults instead of copied into five HttpRequest blocks, and the test asserts the effective cluster picks it up (:250). Two newer facts live here too: /Auth/{**catch-all} carries a tighter named limiter (auth-tight) than the edge global one, because everything under /Auth is credential handling and a burst from one IP there is far more likely to be an attack than a user, and every other route is asserted to carry no named policy (:307, :69-72); and every proxied request is stamped with route and cluster trace headers (:327). GatewayHardeningTests is now a subclass of MmcaGatewayHardeningTestsBase<TEntryPoint> that supplies only ADC's numbers: the per-client-IP allowance of 120 (.../GatewayHardeningTests.cs:38), the tighter named policy at 30 (:80), a representative limited path, and the downstream service names, while the eight gates themselves (bypass list, correlation ID generated or echoed, partitioning by the forwarded client IP, one readiness check per downstream service tagged ready and never live) live once in the base (ADR-088). AppHostBicepParityTests is the newest and guards a boundary nothing else does: Aspire's WithReference(peer) injects services__{peer}__{endpoint}__0 environment variables and infra/main.bicep hand-writes the same variables onto each container app, with nothing connecting the two, so a new cross-service call boots locally, passes every test and then fails in production with an unresolvable service name (and a Bicep entry for a removed edge is dead configuration nobody notices); both files are read as text rather than executed, because the AppHost hangs when started headless and Bicep is not executable here at all (.../AppHostBicepParityTests.cs:24, §17). The Gateway is a pure reverse proxy (no DbContext or broker) so the boot needs no SQL. Integration style.
MMCA.ADC.Services.Tests 5 The only project that tests the service-host gRPC adapters directly rather than through a booted host. Its subject is a single, easily-missed wire contract in the data-subject export named by ADC's private PRIVACY.md §7 (ADR-005 / §30): SQL Server returns DateTimeKind.Unspecified values, and the "O" format omits the Z marker for them, so a timestamp that is genuinely UTC crosses the wire looking like a local time. UserEngagementExportGrpcServiceTests and UserNotificationExportGrpcServiceTests assert the emit leg (every timestamp ends with Z and parses back to the same instant, MMCA.ADC/Tests/Services/MMCA.ADC.Services.Tests/Exports/UserEngagementExportGrpcServiceTests.cs:17); the two ...GrpcAdapterTests assert the parse leg, which has to survive a rolling deploy where new Identity code talks to a Notification replica still emitting the marker-less form, so both wire forms must land as DateTimeKind.Utc on identical ticks (.../Exports/UserNotificationExportServiceGrpcAdapterTests.cs:19). FakeServerCallContext is a minimal ServerCallContext that lets a server implementation be invoked directly: the export services read only the cancellation token, so nothing else has to behave (.../Support/FakeServerCallContext.cs:10). In MMCA.ADC.CI.slnf (MMCA.ADC/MMCA.ADC.CI.slnf:60), so it runs on every PR with no DB. Unit style.
MMCA.ADC.CrossService.IntegrationTests 13 The real-broker + real-gRPC tier: boots the REST hosts in one process against a Testcontainers SQL Server and a Testcontainers RabbitMQ, so the genuine MassTransit outbox to broker to consumer round-trip (UserRegistered auto-link, SpeakerLinked/SpeakerUnlinked back-link) and the real Conference to Engagement bookmark-count gRPC read run end to end, over a sequential env-boot fixture (a subclass of the shared CrossServiceFixtureBase) and a smoke gate that fails first if the container/host wiring is wrong. Its newest test is the one that needs this tier most: TwoReplicaHubFanOutTests boots the Notification host twice and proves a notification published on one replica reaches a SignalR client connected to the other, which is precisely the guarantee a single-host integration test cannot observe (MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/CrossService/TwoReplicaHubFanOutTests.cs:35, §7), supported by a Notification-specific factory and a faked attendee query service in .../Infrastructure/. Integration style; needs Docker, runs in the weekday-nightly cross-service job (MMCA.ADC/.github/workflows/cross-service-tests.yml:75, scheduled by the 0 6 * * 1-5 cron at .../cross-service-tests.yml:31, behind a should-run guard that skips a night with no new commits, .../cross-service-tests.yml:50), not in Integration.slnf. This is one of the two jobs whose recency the cross-service-freshness deploy gate keys off.
MMCA.ADC.ServiceBusEmulator.IntegrationTests 3 Broker-parity smoke (§33): production runs on Azure Service Bus while local development runs RabbitMQ, so Service-Bus-specific transport behavior is otherwise observable only in the deployed environment. This tier runs MassTransit v8 against the official Service Bus emulator container with ADC's real integration-event contracts and proves the two transport-specific behaviors: admin-plane topology provisioning (topic per message type, subscription, receive-endpoint queue) and the AMQP publish to topic to subscription to consume round-trip (MMCA.ADC/Tests/Integration/MMCA.ADC.ServiceBusEmulator.IntegrationTests/ServiceBusRoundTripSmokeTests.cs:26). The fixture is the whole lesson here. It was dispatch-only from 2026-07-24 after hanging and being killed at its timeout on 7 of 7 runs, and the cause was read at the time as the emulator's floating companion SQL image; it was not. The real cause was provisioning volume: the test class implemented IAsyncLifetime, xUnit re-instantiates a test class per [Fact], and every bus start re-provisions the whole topology through an admin plane throttled at roughly one operation per second. The bus now lives on a collection fixture and starts once, and both startup phases are wall-clock bounded so a future hang fails with a phase-named error instead of being killed at the job timeout (which discards the step log, and is why this went unlocalized for a week): ServiceBusEmulatorFixture is now a thin subclass of the shared ServiceBusEmulatorFixtureBase (.../Infrastructure/ServiceBusEmulatorFixture.cs:22) with its [CollectionDefinition] at :51. Since 2026-08-31 the servicebus-emulator-smoke job is authoritative rather than advisory: it carries no continue-on-error, runs under a 10-minute spend cap (MMCA.ADC/.github/workflows/cross-service-tests.yml:153), and the cross-service-freshness deploy gate now requires a run in which both it and cross-service concluded success. (The third nightly job, apphost-smoke, is the one that stays advisory under ADR-098, .../cross-service-tests.yml:199.) Integration style; needs Docker.
MMCA.ADC.E2E.Tests 84 Playwright end-to-end against the running Aspire stack, using a Page-Object model (PageObjects/, whose page classes count toward the 84 alongside the workflow classes) and E2ETestBase login helpers, organized by actor workflow (Organizer/Speaker/Attendee/Identity/Preferences) plus the Engagement live-poll, session-Q&A and feedback flows, real-time notification push, a Web-Vitals budget check, a pseudo-locale sweep, and an AccessibilityTests axe sweep that runs 31 WCAG 2.1 AA scans across the public browse pages (events, sessions, speakers, sponsors), the organizer management lists and create forms, the speaker dashboard and QR page, Happening Now as both actors, and the badge/points/check-in/attendance surfaces (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/AccessibilityTests.cs:27); it complements the Identity-flow scans shipped in the MMCA.Common.Testing.E2E workflow bases. Those badge/points surfaces are no longer axe-only: CheckInAndPointsTests drives the attendee badge, the organizer manual check-in fallback, the attendee-driven room and booth codes, the two organizer rollups, and the ledger every one of them feeds (.../Workflows/Engagement/CheckInAndPointsTests.cs:29). It is also the file to read for two techniques the rest of the suite reuses. Three of its journeys need an event that is LIVE right now and no seeded event ever is (their dates are fixed), so each mints its own same-day published event, rooms and a session spanning the current moment, and DELETES the event in a finally block, because a leftover same-day published event becomes the app-wide current event (home phase, Happening Now, check-in, attendance, notification scope) for every later test in the suite; and the minted event is created in UTC rather than the seeded America/New_York, because session times are stored as event-zone wall clock, so with a UTC event the wall clock IS DateTime.UtcNow and there is no zone conversion to get wrong on a runner in any region. Points are awarded asynchronously (the check-in writes its row, the outbox publishes AttendeeCheckedIn, and the Engagement points handler awards on the way back), so every ledger assertion re-navigates in a bounded loop instead of expecting the first read to show it. The same wave added the attendee share/export journey (.../Workflows/Engagement/AttendeeShareAndExportTests.cs), live session Q&A (.../Workflows/Engagement/LiveSessionQaTests.cs:22), a data-integrity sweep (.../Workflows/Conference/DataIntegrityTests.cs:11), the public sponsor browse and organizer sponsor management flows (.../Workflows/Conference/PublicSponsorBrowseTests.cs:13), and two suites that are pure subclasses of shared bases: AuthorizationTests over AuthorizationTestsBase (.../Workflows/Identity/AuthorizationTests.cs:11) and a body-less PasswordResetTests over PasswordResetTestsBase (.../Workflows/Identity/PasswordResetTests.cs:5). Runs once per engine via E2E_BROWSER (MMCA.ADC/.github/workflows/e2e.yml:301); the chromium leg gates deploy through e2e-gate (MMCA.ADC/.github/workflows/deploy.yml:677) while firefox and webkit alternate on the Monday and Thursday crons (.../e2e.yml:49-50). The largest single project here and the source of most of the chapter's recorded E2E debugging history. [Rubric §28, Front-End Testing] + [Rubric §22, Responsive/Cross-Browser]: this suite is the cross-browser, real-user-flow safety net. E2E style.

Reconciliation. Common: 45+62+343+403+2+138+16+44+4+7+27+127+4+18+6 = 1,246 (15 projects). ADC Conference: 17+28+165+15+20+53+37+11 = 346 (8). ADC Engagement: 7+11+66+4+10+36+22 = 156 (7). ADC Identity: 3+4+30+9+8+7+34 = 95 (7). ADC Notification: 1+5+9 = 15 (3). ADC host/service-adapter/cross-service/E2E: 8+5+13+3+84 = 113 (5). Total = 1,246+346+156+95+15+113 = 1,971, across 45 projects, matching the unit input exactly.


⬅ Device Capability Abstraction Layer (Native Contracts, MAUI, Browser & Fallback Adapters)IndexCoverage audit ➡