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:
- 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. - Architecture fitness functions (
IArchitectureMap,ArchitectureMapBase,Layer,LayerRef,ArchitectureAssert,RuleHelpers,CrossEntityNavigationFinder, the twenty-nineArchitectureRulespartial files, and the forty-six abstract*TestsBaseclasses includingRouteAuthorizationTestsBase,ModuleConformanceTestsBase<TModule>andBrandColorTokenTestsBase) turn architectural rules into build-gating assertions that run identically across every repo. - 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. - End-to-end (Playwright) testing (
PlaywrightFixture,E2ETestBase,E2ETestConfiguration,AuthOutcomeRules,PageExtensions,AxeOptions,AccessibilityViolationException,WebVitalsCollector, the reusable page objectsLoginPage/RegisterPage/ProfilePage/ForgotPasswordPage/ResetPasswordPage, and the shipped workflow suites such asAuthorizationTestsBase) drive a real browser against a running app, asserting accessibility and performance alongside behavior. - 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. - 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).
The Gallery harness
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.ControllerBaseandMicrosoft.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'sFullName, 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,RouteAttributeandAllowAnonymousAttributeare 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 (IsControllerwalksBaseType,: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
LoadableTypesof each target assembly, keeps controllers and routable components, and projects each survivor throughAnonymousEndpointsOf, distinct and ordinal-ordered (AnonymousEndpointTestsBase.cs:125-:131). Methods are read withBindingFlags.DeclaredOnlyandinherit: 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 fromMMCA.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 withinherit: 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
ControllerBaserather 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,
initincluded (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 theTCommandof some concreteICommandHandlerin 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
nameofin 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.Combineinvariant 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 aResultto carry; exactly three exception types are permitted (:16-:21). Everything else in a Domain assembly is a violation. The mechanism is deliberately narrow: for eachthrowopcode the rule looks at the immediately preceding instruction, skipping thenops a Debug build interleaves, and reads the exception type off anewobj(: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 acatchcompiles to the distinctrethrowopcode, notthrow, 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
TicketDomainExceptionisinternal 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]fromSystem.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
CancellationTokennamed exactlycancellationToken" (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-Disposemethods whose return type isTask,Task<T>,ValueTask, orValueTask<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
exemptMethodsparameter (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 isExternallyFixedMethods(: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
CancellationFixturesnamespace 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 IBadgeGranterwithTask 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.
IFakeExportServiceis 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 realServiceDescriptor. - Walkthrough - no members. Its only role is to be the
ServiceTypethat 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
Leftfixture namespace, which a type in theRightnamespace derives from. That inheritance edge is what closes the loop back intoLeft. - Depends on - nothing. It is
public abstract class LeftModelBasewith a singleint 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'sRootNamespace, records a directed edgeA -> Bwhenever a type inAstructurally references a type inBin 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. Beingabstractis irrelevant to the cycle rule (which walks the type surface, notConcreteClasses); 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 implementsICommandWithRequest<TRequest>and a concreteIValidator<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'sAddValidatorsFromAssemblyscan 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 aninternalconstructor with a single expression-bodied rule, and both useNotEmpty()on the one string property. Nothing about the rule content matters to the fitness function; the existence of a closedIValidator<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,ConflictandValidation(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.NotFoundand a support ticket quotes it, so two modules both shippingItem.Invalidmake 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 nineErrorfactory 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 soerror 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.
TicketErrorsandDuplicateTicketErrorsare the collision;TwoBranchTicketErrorsis the shape a naive "count the construction sites" implementation would wrongly flag;SharedCodeErrorsproves the exemption is honored on the prefix rule as well as the uniqueness rule. Keeping every fixtureinternal staticwith 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 inCommand. 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), whichCommandInventory_CountsTheDataCarryingCommandspins 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 singlestring Sku. - Concept introduced -
SchemaVersionas the ordering a fixture set has to make visible. BaseIntegrationEvent declarespublic 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, throughRuntimeHelpers.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*.IntegrationEventsnamespace 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
Skuon 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
nameofin 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
EventUpcastersHaveUniqueSourceTypesgroups 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 higherSchemaVersionthan 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;EventUpcastersIncreaseSchemaVersioncompares the two declared versions and flagstargetVersion <= sourceVersion(:44-:48). Note what the second rule deliberately does not do: a missing or non-intSchemaVersionis skipped rather than reported (:37-:42), because that is the business of the separateIntegrationEventsDeclareSchemaVersionrule. 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);SourceTypeandTargetTypecome 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
internalkeeps them off the assembly's public surface while still visible toConcreteClasses, which is what the rule enumerates (ArchitectureRules.Upcasters.cs:67). - Where they're used - reflected over by EventUpcasterFitnessTests through UpcasterTestMap, and named by
nameofin 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 toDeleteChildren(the helper on the aggregate base,MMCA.Common/Source/Core/MMCA.Common.Domain/Entities/AuditableAggregateRootEntity.cs:273), or a zero-argumentDeletereached through thecallvirtopcode, which is what a hand-rolledforeach (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 isbase.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-virtualcall, 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
internaland 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
nameofin 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.Testsproject during CI'sbuild-and-testjob (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
*TestsBaseand 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.Testsproject during CI'sbuild-and-testjob. 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
CascadeSoftDeleteConventionTestshas 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
DomainExceptionhierarchy 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). Thenewobjimmediately precedes thethrow, soConstructedExceptionTypenames 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 viadescriptor.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
DomainEventSaveFixturesis generic over. It carries no meaning; it exists so the four fixture handlers can implement a real, closedIDomainEventHandler<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.IDomainEventHandler1matched 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 DateOccurredandGuid MessageId, which are the two members IDomainEvent requires. It is the onlypublictype in the file; the handlers around it are allinternal. - 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
AbstractFitnessControllerBaseand, 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
CreateInheritedAsyncas one of its public methods, and the rule reads attributes withinherit: 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 fromAuthControllerBaseorAggregateRootEntityControllerBasesatisfies 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'sConcreteClassesscan 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 withBindingFlags.DeclaredOnlyand attributes withinherit: 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 (IsControllerwalks the base chain,AnonymousEndpointTestsBase.cs:101-:112), and theDeclaredOnlyfilter is what keeps it silent: it declares no methods of its own, soAnonymousEndpointsOfyields 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).
NavigationContractTests
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.mdstays in lockstep with the routable pages theMMCA.Common.UIassembly 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 CoreRouteAttributeandAuthorizeAttribute(the route and guard metadata reflected over,:107,:113), two source-generated regexes via[GeneratedRegex]partial properties (:139,:142), the embeddedNavigationFlow.mdmanifest resource (:125; wired by the csproj atMMCA.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.mdlives next to the framework code (it is an embedded resource of this test project, perMMCA.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) andDocResource = "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:undocumentedroutes (real but missing from the doc,:36) andphantomroutes (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 asAuthenticated...,Anonymous, orAny(: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 withRouteParameterRegexand 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 eachRouteAttribute.Templateand whether the type carries[Authorize](inherited attributes included,:113);DiscoverDocumentedRoutes(:123-:137) reads the embedded doc, throwing a clearInvalidOperationExceptionwhen the resource is missing (:125-:126), and extracts route rows via the source-generatedRouteRowRegex(:139-:140, a 2000ms-timeout[GeneratedRegex]partial property).
- Why it's built this way -
NavigationFlow.mdis 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.Testssuite; it runs in CI'sbuild-and-testjob (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 realInvalidOperationExceptionthrow 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 namesfixtures.observability-main.bicepandfixtures.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.Architecturepackage (ArchitectureRules plus its*TestsBasefamily), 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) andRunbookResource => "fixtures.observability-OPERATIONS.md"(:18), replacing the base defaultsinfra.main.bicepandinfra.OPERATIONS.md(ObservabilityConventionTestsBase.cs:42,:45). One own[Fact],ResourceAssembly_DefaultsToTheDerivedTypesAssembly(:24-:29), asserts the resolvedResourceAssemblyis 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 sloAlertSpecsandresource sloAlerts(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Fixtures/observability-main.bicep:6-:25), which meets the base's defaultMinimumAlertSpecs => 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 realinfra/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
ValidatedTypescontains 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
ICommandWithRequest1matched 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
nameofinCommandCoveredThroughTheRequestBridge_IsCoveredandBridgeWithNoRequestValidator_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
Rightfixture namespace that derives from a base inLeft, supplying theRight -> Leftedge. - Depends on - LeftModelBase (
public sealed class RightModel : LeftModelBase,RightFixtures.cs:6, with theusingof theLeftnamespace 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 Nameinitialized tostring.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 (theRegisterparameter,:39), IFakeExportService and DisabledFakeExportService (the stub pair,:44), plusIServiceCollectionandIConfigurationBuilderfromMicrosoft.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,RegisterDisabledStubsas 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-bodiedservices.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
TModuleof 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:
Nameplus an emptyRegister, and nothing else. Its whole purpose is to not overrideDependenciesorRequiresDependencies, 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 (theRegisterparameter,: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, somodule.Dependencieswill not compile againstFakeLeafModuleand a reflection lookup on the concrete type finds nothing. The shared base solves this by locating theIModuleinterface 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 emptyRegister(...)(: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}ModuleTestsfiles 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
TModuleof 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> _seeninitialized with a collection expression (:97), and aHandleAsyncthat guards its argument, records the event's type name, and returnsTask.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 seeIBadgeGranter.GrantAsyncand stop, because an interface method has no body to read. - Walkthrough - one field, one constructor, and a
HandleAsyncthat returns_granter.GrantAsync(cancellationToken)directly (:87-:88). It is notasync, 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), whosebecausestates 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 -> Righthalf of the fixture cycle: aLefttype whose property type lives in theRightnamespace. - Depends on - RightModel (the nullable
Modelproperty,LeftFixtures.cs:9, with theusingat: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 onCryptographicOperations(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'sTypes/PredicateListquery 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
PasswordHasherSecurityTestsdoes; 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 theRfc2898DeriveBytesreference disappears from the IL and this test goes red. Same for replacingCryptographicOperations.FixedTimeEqualswithSequenceEqual: identical behavior on every test input, and a timing side channel in production. The twobecausestrings 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 stringfully-qualified type names,CryptographicOperationsType(:17) andRfc2898DeriveBytesType(:19), keep the matched names in one place.Infrastructure(:21) anchors the scanned assembly throughtypeof(PasswordHasher).Assembly, and the privatePasswordHasherTypes()helper (:56-:59) narrows it withTypes.InAssembly(Infrastructure).That().HaveName(nameof(PasswordHasher)).ScannedPasswordHasherSet_IsNotEmpty(:23-:27): the non-vacuity guard, and the first fact for a reason.HaveNamematches 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.ContainSingleon the full name (:25-:26) closes that.PasswordHasher_DependsOnASlowKeyDerivationFunction(:29-:40) andPasswordHasher_DependsOnConstantTimeComparison(:42-:54): each runs.Should().HaveDependencyOnAll(<type name>).GetResult()and routes the result throughArchitectureAssert.NoViolations.HaveDependencyOnAllis 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) andCryptographicOperations.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
PasswordHasherSecurityTestsin 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 -
HaveDependencyOnAllreports 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
Leftand that nothing points back at, so it must never appear in a cycle report. - Depends on - LeftService (the one-way
Serviceproperty,AcyclicFixtures.cs:9,usingat: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 inApi/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 sealedsubclass of a shipped*TestsBasewhose 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 subclassprivateand 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 theMMCA.Common.Sharedassembly reached throughtypeof(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 leastMinimumScannedTypes, 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 => 21on AnonymousEndpointTests,MinimumBaseResources => 3on LocalizationResourceTests,MinimumRoutes = 8in NavigationContractTests, and theDistinctErrorCodeCountandHandledCommandCountinventories 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).TargetAssembliesreturns that one assembly (:120-:121), andAllowedAnonymousEndpointsis empty (:123), which is irrelevant here because nothing is discovered to compare against. Beingprivateand 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 inheritedScannedEndpointSet_IsNotEmptyinto 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, andMicrosoft.Extensions.DependencyInjection'sServiceCollection/ServiceLifetime(:70,:77). - Concept introduced - asserting on a
ServiceCollectioninstead 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 bareServiceCollection(:70), callsRegisterDisabledStubson it (:72), and then inspects the resultingServiceDescriptordirectly (: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), andExpectedRequiresDependencies => true(:66). The dependency list is written in the opposite order to the module's own declaration (:35), which is deliberate: the base compares withBeEquivalentTo(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 forIFakeExportService(:74-:75), implementation typeDisabledFakeExportService(:76), lifetimeSingleton(: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
ExpectedNameand leaving every other expectation at the base's default. It is the exact subclass shape the three byte-identical consumer{X}ModuleTestsfiles 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
DependenciesnorRequiresDependencies, and this subclass declares neither expectation, so the base's defaults ([]andfalse,ModuleConformanceTestsBase.cs:29,:35) must line up withIModule'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 vacuousAssertDisabledStubsdefault, 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 theassemblyto 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 inOtherModuleNamespaces, 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) andModuleNames => ["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 singleLayerReffor module"Beta"at the caller'sfromLayer, pointing at the caller's assembly with a derived root namespace.OfLayerand 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 whenlayer == forbiddenLayerand 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
CrossLayerViolationshared 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 namesAnonymousFixtureController(:22), with thebecausestating 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 assertsNotThrow(: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'sAnonymousEndpointsForTest()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 theMMCA.Common.Testing.Architecturepackage, 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 = trueon 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 noON DELETE CASCADEto 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 fromAuditableAggregateRootEntity<T>that declares a collection ofAuditableBaseEntity<T>children and does not delete them in its ownDelete()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_domainEventsescapes 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'sList<string>demonstrates from the other side. - Why it's built this way - the file doc explains why every fixture here is
internaland in its own namespace (:10-:15): it keeps them invisible to the framework rules that run over CommonArchitectureMap, which registers theSource/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(...).FullNameandnameof(...)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 baretypeof(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 whatnameofwould have caught. [Rubric §15 - Best Practices & Code Quality] and [Rubric §14 - Testability] apply. - Walkthrough -
TargetAssembliesis 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.MinimumScannedTypesis 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'sprotectedenumeration so the enclosing test class can assert on the emitted set directly. The base exposesAnonymousEndpoints()asprotectedfor 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, andBase_DoesNotReport_AnInheritedAttributeOnTheDerivedController(:61-:71) reads itsAnonymousEndpointsForTest()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 subclassprivateand 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
LayerRefsequences andDefineLayers()simply flattens them (:131). EveryLayerRefpoints at this same test assembly with a fabricated root namespace of the formMMCA.Fake.{module}.{layer}(:123-:124), andRepoTokenis"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 aLayerReffor all five layers, andThin(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.
FitnessPrincipalis 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 byFitnessDependent.Principal. Deriving fromAuditableBaseEntity<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 readsmap.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 useFramework(...)rather thanModule(...), 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 byAssembly.Locationalso 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-entryDefineLayers()naming one layer and one anchor type from the fixture namespace it serves. Anchoring bytypeof(SomeFixture).Assemblyrather than byAssembly.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
Tof theDbSet<T>,IQueryable<T>andList<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 = trueand 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,ExecuteDeleteandExecuteDeleteAsync(:10-:11), from a type outside the repo's allowlist. The interesting half is the disambiguation.Removeis 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>orLocalView<T>(:18-:24,:138-:139);ExecuteDeleteandExecuteDeleteAsyncare unambiguous wherever EF declares them, so any declaring type under theMicrosoft.EntityFrameworkCoreprefix 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 ispublic(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.ContractsandMMCA.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()plusModuleApplication()(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.ErrorCatalog.cs:38-:44), and the validator-coverage rule takesModuleApplication()alone, mirroring where the framework'sAddValidatorsFromAssemblyscan looks (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:88-:89). A map built fromFramework(...)entries hasModuleset to the empty string (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:94-:95) and the module-layer lookup keeps only entries withl.Module.Length > 0(:101-:102), soModuleApplication()returns nothing and both rules would pass having read nothing at all. UsingModule("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 usesFramework(...)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-entryDefineLayers()returningModule("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 sealedinside 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_HasNoStaleEntriesfact 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, computingstaleas 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 abecausethat 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 -
TargetAssembliesis this test assembly (:110-:111), so the fixture controllers are discovered normally.AllowedAnonymousEndpointsholds 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 onlyAllowList_HasNoStaleEntriesas 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.Architecturepackage 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), andBase_Fails_WhenRequiresDependenciesDrifts(:104-:110). Base_ReadsDefaultInterfaceImplementations_ForALeafModule(:112-:121): instantiates FakeLeafModuleConformanceTests and calls the two inherited assertions directly (:119-:120). Passing proves the base reachedIModule'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 overrideAssertDisabledStubs(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).
- Three adversarial facts, one per assertion the base makes, each pointing at DriftedTests and asserting the delegate throws:
- 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 byName, and chooses between a hard start failure and stub registration fromRequiresDependencies(the base's ownbecauseclauses 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, andMMCA.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.ForgotPasswordAsyncand 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'sFullNamethat 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 againstmap.Layers.Select(l => l.Assembly)(:42), so a map holding only this assembly is what makesIAsyncEnumerator<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-entryDefineLayers()returningFramework(Layer.Application, typeof(CancellationTokenFitnessTests).Assembly)(:67-:68). - Where it's used - constructed on every call of the test class's private
RunRulehelper (: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-
CancellationTokenrule: 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 inMMCA.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
RunRulehelper (:53-:60) captures the exception's message once and each fact makes positive and negativeContainassertions against that single string. The result is one rule execution that proves six independent behaviors, and every assertion carries abecauseclause 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 bothMissingTokenFixtureService.RunAsyncand.PingAsyncappear (:19-:22), and that neitherCompliantFixtureServicenor the internalHiddenAsyncdoes (: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.LegacyAsyncis reported (:41-:43); with that one entry passed in (:45), it disappears (:47). The same run also assertsMoveNextAsyncnever 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 (
CommandsHaveValidatorsandHandledCommandCount,CommandValidatorCoverageFitnessTests.cs:25,:103), its nested FixtureModuleMap, the thirteen types inMMCA.Common.Architecture.Tests.CommandValidatorFixtures(:1), andXunit.Sdk.XunitException(:3), which every fact asserts on by exact type rather than as a bareException. - 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_CountsTheDataCarryingCommandscallsHandledCommandCountand 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 readonlyFixtureModuleMap(:20) are shared by every fact.CommandWithNoValidation_IsFlagged(:22-:31) andCommandWithItsOwnValidator_IsCovered(:33-:42): the offender and the direct-coverage case, asserted byContainandNotContainon the one message.CommandCoveredThroughTheRequestBridge_IsCovered(:44-:53) andBridgeWithNoRequestValidator_IsNotCoverage(:55-:64): the two halves of the bridge, with the secondbecausestating exactly why a registeredCommandRequestValidatoris 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 assertsNotThrow, the only fact in the class where the rule is quiet.CommandInventory_CountsTheDataCarryingCommands(:100-:108): the count, with thebecauseenumerating 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
XunitExceptionspecifically rather thanExceptionmeans 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
CycleFixturesnamespace 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
LayerRefdirectly to override the derived root namespace. Every other map in this chapter uses theFramework(layer, assembly)helper, which computes the layer's root namespace asMMCA.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 beneathLayerRef.RootNamespace(ArchitectureRules.Cycles.cs:12-:16) and skips types outside it entirely: with a derived root the fixtures would not be grouped intoLeft/Right/Acyclicnodes at all. So this map builds theLayerRefby hand withFixtureRootas 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 aDefineLayers()returning a singlenew LayerRef(string.Empty, Layer.Application, typeof(NamespaceCycleFitnessTests).Assembly, FixtureRoot)(:56-:63), whereFixtureRootis 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 (marksEmailandFullName,:91,:94), and Result (returned fromAnonymize,:99). - Concept introduced - closing a vacuous fitness function with a stand-in data subject. The framework's
[Pii] => IAnonymizablescan (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'sUser) 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) andCity(:97) are non-PIIinitpass-through fields;EmailandFullNameare[Pii]with private setters (:91-:95), which is what letsAnonymizerewrite them in place while no caller outside the type can.Anonymize()(:99-:105) overwrites both PII fields with the fixed placeholders and returnsResult.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
PiiRedactorandAnonymizetogether. - 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.EntityFrameworkCoreforDbSet<T>and theExecuteDeleteAsyncqueryable 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.ForgetcallsList<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):Removeon aList<T>is what makes the naive "ban the name Remove" version of this rule useless. Having a genuineList<T>.Removecompiled next to a genuineDbSet<T>.Removeis 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
DbSetRemovingFixturemust leaveExecuteDeletingFixturereported. 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,ErrorCodesUseAnAllowedPrefixandDistinctErrorCodeCount,ErrorCatalogFitnessTests.cs:24,:58,:92), its nested FixtureModuleMap, the six fixture catalogs (:1), andXunit.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, andIsTicketsCode(: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) andPrefixedCodes_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 thebecause(: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 (thePrincipal?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 nullablePrincipalnavigation (:52), and aFlagscalar (: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 onPrincipalIdandFlag; NavigatingSpec reaches throughPrincipal. - 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 readsmap.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 useFramework(...)rather thanModule(...), 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 byAssembly.Locationalso 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-entryDefineLayers()naming one layer and one anchor type from the fixture namespace it serves. Anchoring bytypeof(SomeFixture).Assemblyrather than byAssembly.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), plusMicrosoft.AspNetCore.Mvc(:1), xUnit, and AwesomeAssertions. - Concept introduced -
nameofinstead of string literals in message assertions. Every assertion here is built fromnameof(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 anactdelegate, captures the thrown exception, and asserts the message containsUndeclaredFitnessController.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): threeNotContainassertions covering the direct declaration (:31-:33), the inherited one (:34-:36), and the justified opt-out (:37-:39), each with abecauseclause stating the ruling.Rule_SkipsAbstractControllers(:42-:50): assertsAbstractFitnessControllerBaseis absent, which is theConcreteClassesfilter'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:
PostActionsDeclareIdempotencyIntentstarts frommap.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-entryDefineLayers()returningFramework(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 (
ModulesDeclareLayersin 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.
ModulesDeclareLayersis 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
FiveLayerslist (: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, assertedNotThrow.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 plusLayer.Uiand 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) andModulesDeclareLayers_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 forwardsoverrides: 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 withWithMessagerather 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'sModuleRequiredLayerOverrides; 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 underCycleFixtures(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
allowedCycleNamespaceslist 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.Leftand...CycleFixtures.Right(:21-:22) and not...CycleFixtures.Acyclic(:23-:25), each with abecausenaming the edge that put it there.Rule_Passes_WhenTheWholeCycleIsAllowed(:28-:36): passes both namespaces as the allowance and assertsNotThrow()(:31-:35). This is the only fact in the class where the rule does not throw.Rule_StillFails_WhenOnlyPartOfTheCycleIsAllowed(:38-:46): passes onlyLeftand 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-
.protocontract 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, andBuildProtoContract,:93), ArchitectureMapBase (FindRepoRoot,:92), two fixture.protofiles underTestData/, and externals xUnit plus AwesomeAssertions. - Concept introduced - a wire contract as a committed, sorted string snapshot. A
.protofile 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 printBuildProtoContractand 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), theTestDatadirectory (:18), and the pinned and drifted file arrays (:20,:22). The 12-lineFrozenContract(: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 newproduct_id = 7line as unexpected (:58-:60) and the frozenproduct_id = 1line as missing (:61-:63), which is what makes a renumbering legible in the failure output.Rule_FlagsAnAddedRpc(:66-:74): asserts the addedDeleteProductrpc 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.protorather 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 equalsFrozenContractexactly and contains neither thecsharp_namespaceoption nor thetimestamp.protoimport (: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.protoversusfitness-catalog-drifted.proto), and the pinned one deliberately includes a nested message, a nested enum, arepeatedfield, anoptionalfield, 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
TemplateColumnwith 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.razorfixture files underRazorFixtures/, andXunit.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; aTemplateColumnhas no bound property, so marking oneSortable="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.razortext 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 genericT="Foo<Bar>"argument all work (:22-:29); theSortablevalue 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 -
FixtureRootis built fromArchitectureMapBase.FindRepoRoot("MMCA.Common.slnx")plus the four path segments down toRazorFixtures(:17-:22), withOffendingandCleanas expression-bodied properties over it (:24,:26).SortableTemplateColumn_IsFlaggedWithFileAndLine(:28-:37): asserts the message containsSortableTemplateColumnGrid.razor:7. That exact number is the point: in the fixture the element opens on line 7 and theSortable="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): pinsExpressionSortableTemplateColumnGrid.razor:5, a one-line element withSortable="@(true)"written first (RazorFixtures/Offending/ExpressionSortableTemplateColumnGrid.razor:5).ConformingMarkup_Passes(:50-:57): the discrimination fact, and the one carrying the most fixture.ConformingGrid.razorpacks seven near-misses into one file: a sortablePropertyColumn(:6), aTemplateColumnwith noSortableat all (:9), one explicitlySortable="false"(:16), one bound to a field (:19), a longer attribute nameIsSortable="true"(:22), a generic argument carrying an angle bracket inside a quoted value with a real sortablePropertyColumnon the line below it (:26-:27), and a longer element nameTemplateColumnGroup(:30).CommentedOutGrid.razorholds 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
.razorfiles rather than inline strings because the rule takes directory roots and enumerates files itself, excludingbinandobj(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 sortablePropertyColumnbelow 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
Sortablevalue bound to a field or property is deliberately not judged, so aTemplateColumnwhose binding evaluates totrueat run time passes this gate.ConformingGrid.razor:19is 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 inMMCA.Common.Architecture.Tests.CascadeFixtures(:1), andXunit.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.AggregatesCascadeSoftDeleteToChildrendecides its verdict from the instruction stream of aDelete()override, so no amount of reviewing the rule's source proves it distinguishesbase.Delete()fromchild.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 abecausenaming the form.AggregateWithNoChildEntities_IsIgnored(:62-:71): the scope fact, with the ruling stated inline, aList<string>is not a child-entity collection.AllowlistedType_SilencesOnlyThatType(:73-:85) andAllowlistedNamespace_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 callsArchitectureRules.PinnedPackageMajorBelowagainst this repo'sDirectory.Packages.props. - Concept introduced - the thin-subclass fitness pattern. Almost every rule in this project lives once in the reusable
MMCA.Common.Testing.Architecturepackage 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 idsMassTransit,MassTransit.RabbitMQ, andMassTransit.Azure.ServiceBus.Core(:17-:22) with anexclusiveMajorCeilingof 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 forSixLabors.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 areprotected 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.Infrastructureand deliberately do not subclass the base, because the default list would assert aDirectory.Packages.propsentry 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.Testssuite in CI'sbuild-and-testjob. - 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, soIntegrationEventsexcludes 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 unlikeMMCA.Common, so nothing derived from the token can collide with the real framework namespaces. - Walkthrough -
DefineLayers()is ayield-based iterator with one entry (:128-:131). The events it brings into scope are the eight upcaster fixture contracts, which are the onlyIIntegrationEventimplementations 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 theMapof 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 forFindRepoRoot("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
RepoRootand, 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 underSource/orTests/may hold more thanMaxDirectFilesdirect 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 forMMCA.Common.slnxruns once per test instance.ExemptFolderSuffixes(FolderWidthTests.cs:28-:33) returns three repo-relative suffixes:MMCA.Common.Application/UseCases/Decorators,MMCA.Common.Application.Tests/Decorators, andMMCA.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 toArchitectureRules.FoldersStayNarrow(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.FolderWidth.cs:34), which walksSourceandTestsrecursively (:42-:50), skips the tool-owned and build-output segmentsbin,obj,Migrations,Platforms,Resources,node_modules,wwwrootand.git(:82), and counts a.razorcomponent and its co-locatedX.razor.cscode-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/andPlatforms/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.Testssuite in CI. The same base is activated once per repo; the ADC binding is the identical shape withFindRepoRoot("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 atUPGRADING.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 eventMISSING member,changed typeandEXTRA 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 throughReverseMembers(:90-:94) and assertsNotThrow, with thebecausestating why order is not observable.Base_Fails_WhenTheCodeDeclaresAMemberTheContractDoesNot(:36-:47) andBase_Fails_WhenTheContractDeclaresAMemberTheCodeDropped(:49-:60): drop the first member and append aGhostProperty:String, asserting"EXTRA member"and"MISSING member GhostProperty:String"respectively.Base_Fails_WhenAMembersTypeChanges(:62-:74): rewrites the first member's type toGuidand 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
privateso 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
ExpectedContractliterals are the real subject of the gate. - Caveats / not-in-source - the base's
Parsekeeps 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
.resxfiles 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
RequiredCulturesasSupportedCultures.AllminusSupportedCultures.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) filtersSupportedCultures.Allto the non-default entries; the allowlist is[Default, "es"]withDefault = "en-US"(MMCA.Common/Source/Core/MMCA.Common.Shared/Globalization/SupportedCultures.cs:12,:18), so the required set is today exactlyes.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]isTranslations_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-Plocis deliberately kept out ofSupportedCultures.All(SupportedCultures.cs:20-:28) precisely so this gate does not demand a.qps-Ploc.resxsibling. - Where it's used - run by the
MMCA.Common.Architecture.Testssuite.
NavigatingQuerySpec, ScalarOnlyQuerySpec
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), andSystem.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 climbsBaseTypeall the way up (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/RuleHelpers.cs:80-:91). BecauseQuerySpecificationitself derives fromSpecification, the prefix still matches two levels down, and the string prefix match means bothSpecificationandQuerySpecificationare 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
Criteriaand 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 -
ScalarOnlyQuerySpecis the sharper of the two. Without it, a future rule implementation that (reasonably but wrongly) also inspectedIncludeswould still pass every other fixture in the file. Both are declaredpublicwhile their plain-Specificationsiblings areprivate, which makes no difference to the rule (it scansConcreteClassesacross the assembly, not by visibility). - Where they're used - the two inputs to
Rule_AlsoAnalyzesQuerySpecificationsin SpecificationFitnessTests (:26-:38).
NavigatingSpec
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
Criterianavigates 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!.IsActivepredicate (: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 functionSpecificationsDoNotNavigateToOtherEntitiestreats 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
Criteriaexpression that dereferences thePrincipalnavigation (: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] => IAnonymizablescan 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) assertsPiiRedactor.HasPiirecognizes the sample, so the later guards assert against something real.PiiRedactor_MasksEveryPiiMember_AndPassesThroughNonPii(:26-:35) redacts an instance and checksEmailandFullNamebecomePiiRedactor.RedactedTokenwhileIdandCitypass through unchanged (:31-:34).PiiRedactor_LeaksNoClearTextPii_ToLogsOrTelemetry(:37-:50) verifies neither the redacted dictionary values (:42-:44) norRedactToStringoutput (: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 isIAnonymizable(:56), thatAnonymize()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] => IAnonymizablescan 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'sUser) 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 sealedsubclass of the frozen-contract base whoseExpectedContractis 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) andExpectedContract { get; } = expected(:138). Beingprivatekeeps xUnit from collecting the inheritedIntegrationEventContracts_ShouldMatch_TheFrozenSnapshotas 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
Criteriafilters 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
Criteriafiltering onPrincipalIdandFlag(: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
Removeis never mistaken for one, and that both allowlist granularities work. - Depends on - ArchitectureRules (
HardDeletesOnlyInAllowedTypes,SoftDeleteEnforcementFitnessTests.cs:24), its nested FixtureAssemblyMap, the four types inMMCA.Common.Architecture.Tests.HardDeleteFixtures(:1), andXunit.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 namesPurge,PurgeManyandPurgeAsyncappear 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 nestedyield returnover 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 withWithMessage("*DbSetRemovingFixture*").HardDelete_ReportsEveryErasingMember(:30-:40): the completeness fact described above, with abecausenaming which EF member each one maps to.OrdinaryCollectionRemove_IsNotAHardDelete(:42-:51): the discrimination fact, asserting the wholeSoftDeletingFixturetype name is absent even though it contains a literalRemovecall.AllowlistedNamespace_SilencesTheRule(:53-:60) andAllowlistedType_SilencesOnlyThatType(:62-:74): the two granularities, the second carrying the control assertion thatExecuteDeletingFixtureis 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
SpecificationsDoNotNavigateToOtherEntitiesfitness 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 theQuerySpecificationsubclass 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 anactdelegate,ArchitectureRules.SpecificationsDoNotNavigateToOtherEntities(new SpecTestMap())(:18), captures the thrown exception throughShould().Throw<Exception>().Which(:20), and asserts its message containsNavigatingSpecand the word "navigates" while not containingScalarOnlySpec(: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, assertingNavigatingQuerySpecis reported andScalarOnlyQuerySpecis 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-entryDefineLayers()(: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.
IsSaveCallaccepts a callee namedSaveChangesorSaveChangesAsync(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:12) whose declaring type is either under theMicrosoft.EntityFrameworkCorenamespace prefix, or whose simple name ends with one of four suffixes:DbContext,DbContextFactory,UnitOfWorkorRepository(: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'sDbContextFactorysave surface, a consumer'sSQLServerDbContext, and anyIRepositorythat forwards to one, without the rule package taking a compile dependency on any of them. These three fixtures all reach it throughIUnitOfWork, the suffix case. - Walkthrough - all three share the same shape: a
private readonly IUnitOfWork _unitOfWork, aninternalconstructor 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 -
DirectSavingHandleris named by three facts of DomainEventHandlerSaveFitnessTests (:30,:92,:103);PointsWriteris asserted to appear in the reported chain (:43);BadgeGranteris reached transitively and is the reasonInterfaceDispatchSavingHandleris 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), becauseEnqueueTargetsfilters 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
internalconstructor, andinternal async Task AwardAsync(CancellationToken cancellationToken) => await _writer.WriteAsync(cancellationToken).ConfigureAwait(false);(:48-:49). Beinginternalrather 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 inAllowlistedCollaborator_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
asyncmethods. - 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'sAsyncStateMachineAttributeto 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
internalconstructor, andpublic 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, andSaveChangesAsync. - Where it's used - the subject of
TransitiveSave_TwoHopsAway_IsFlagged(DomainEventHandlerSaveFitnessTests.cs:34-:45), which asserts all four names appear, and the control in bothAllowlistedCollaborator_PrunesTheWalkWithoutHidingDirectSaves(:88-:90) andDepthBound_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 inMMCA.Common.Architecture.Tests.DomainEventSaveFixtures(:1), andXunit.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, becausemaxCallDepthdefaults 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) andTransitiveSave_TwoHopsAway_IsFlagged(:34-:45): the two reach cases, the second asserting the handler, both hops andSaveChangesAsyncall appear, each with abecausenaming 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, assertedNotThrow, with thebecausenaming 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 leavesDirectSavingHandlerreported 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 withmaxCallDepth: 1and asserts the direct save is still found while the two-hop save is not, with thebecausecalling 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.Architectureand 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 theFramework(...)helper rather than a module layer, and that distinction is load-bearing well beyond bookkeeping: several rules branch onmap.ModuleNames.Countto 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 oneFramework(Layer.X, anchorType.Assembly)entry per package, using a single anchor type to resolve each assembly (mirrors the oldPackageAssemblieshelper): 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 byEnforceUIMauiLayerBoundaryinSource/Build/MMCA.Common.LayerEnforcement.targetsand the windowsbuild-mauiCI 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
Mapby every thin*ConventionTestssubclass 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.Grpctransport boundary and the placement of theIMessageBus,IJwksProvider, andILiveChannelPublisherabstractions. - Depends on - IMessageBus, IJwksProvider, ILiveChannelPublisher, and the NetArchTest
Typesquery 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
Assemblyaccessors anchor the Grpc, Application, and Infrastructure assemblies by an anchor type each (:15-:19). Three[Fact]s assertMMCA.Common.Grpchas no dependency on Domain, Application, or Infrastructure (:21-:34) via theAssertNoDependencyhelper (:51-:59), which runs aTypes.InAssembly(...).ShouldNot().HaveDependencyOnAny(...)NetArchTest query and routes the result throughArchitectureAssert.NoViolations(:58). Three more[Fact]s assert placement by comparing the abstraction's declaring assembly against the anchored layer assembly:IMessageBuslives in Application (:36-:39),IJwksProviderin Infrastructure because it handles crypto and PEM material (:41-:44), andILiveChannelPublisherin Application besideIPushNotificationSender(: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: aSales.Domain -> Catalog.Applicationproject 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 knowsMMCA.Common.*references (rule doc,:34-:41; class doc,:8-:14).ModuleInternalLayersAreIsolated(:47) closes it by iterating the full four-by-four product, withLayer.Uideliberately 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 namesMMCA.Common.Applicationas 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, assertedNotThrow. 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
Actionand 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 inMMCA.Common.Architecture.Tests.DomainThrowFixtures(:1), andXunit.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 underUi/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 assertsNotThrow, with thebecauserecording the fact that makes it interesting, that MMCA.Common's own Domain is the reference implementation of ADR-013 and its single throw is theArgumentNullExceptionguard inSpecification.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), theNonFixtureThrowsarray (:29-:33) and a readonly FixtureAssemblyMap (:35) are shared; every fixture fact passesNonFixtureThrowsso the fixtures are the only subject.BusinessException_IsFlagged(:37-:46): asserts both the owning type and the exception's full nameSystem.InvalidOperationExceptionappear (: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 abecauselisting all three exception types and stating why they are exempt, a caller bug rather than a business outcome.BareRethrow_IsNotFlagged(:70-:79) andNonThrowingCode_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) andAllowlistedType_SilencesOnlyThatType(:114-:126): the two granularities, each built by spreadingNonFixtureThrowsand 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
throwin 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.Generatedallowlist 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 atypeof, so renaming or relocatingNavigationContractTestswithout editing:31would 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 (
BuildIntegrationEventContractandIntegrationEventsResideInSharedIntegrationEventsNamespace,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.
IntegrationEventsincludes framework layers only whenmap.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 mentionsOutputCacheEvictionRequested.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
EventVersioningConventionTestshere 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 (
EventUpcastersHaveUniqueSourceTypesandEventUpcastersIncreaseSchemaVersion,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
UpcasterFixturesnamespace 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) andRunSchemaVersionRule(:66-:71). Because the fixtures always contain an offender for each rule, both helpers can assertShould().Throw<Exception>().Which.Messageunconditionally and let the facts make positive and negativeContainassertions 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 assertsNotThrow, with thebecausestating 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 onlyFramework(...), whoseLayerRef.Moduleis 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), andModuleNamesis 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 notMMCA.Common, so the derived module namespace looks like a consumer's.DefineLayers()(:54-:58) is ayield-based iterator returning two entries:Framework(Layer.Domain, typeof(BaseIntegrationEvent).Assembly)(:56), which brings the framework's real integration event into the map, andModule("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*.IntegrationEventsnamespace 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.
AllowedCycleNamespaceslists 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) returnsMMCA.Common.Infrastructure,MMCA.Common.Infrastructure.PersistenceandMMCA.Common.Infrastructure.Messaging(:44-:46), the acceptedroot -> Messaging -> Persistence -> roottangle 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):DependencyInjectionlives 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 enqueueOutboxMessagerows and wake the OutboxProcessor through IOutboxSignal, the consumers resolve the physical source through IDataSourceResolver, and the outbox reports throughBrokerMetricsand readsMessageBusSettings. Splitting the two would put half of one delivery guarantee on each side of a package boundary.Persistence -> root(:30-:34): theEntityTypeConfiguration*shims carry[UseDataSource]and[UseDatabase], marker attributes that stay in the root namespace because consumers annotate their own configurations with them. Pushing them down intoPersistencewould 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 wasMMCA.Common.Infrastructure.Settings, that folder dissolved into the features it configured, and TenancySettingsValidator, which carried the oldSettings -> Persistenceedge, now lives underPersistence/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.Testssuite in CI'sbuild-and-testjob (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*.Contractsor 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 callsArchitectureRules.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 ruleCarriesServiceContractAttribute(:44,:69-:74), and assertsShouldNot().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.Testssuite in CI'sbuild-and-testjob. 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 theMMCA.Common.Infrastructure.Persistencenamespace, 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 underMMCA.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 namedExecuteDeleteAsync), 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 leavesIRepositoryfree to be used everywhere else. - OutboxCleanupService (
:36, now under...Persistence.Outbox.Administration), forPurgeAsync,PurgeInboxAsyncandSweepDeadLettersAsync. 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 noIsDeletedflag 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).
- EFRepository<TEntity, TIdentifierType> (
- 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/Administrationhad 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.Testssuite in CI'sbuild-and-testjob. 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-@codecaps. - 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.Architectureand 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 themvirtualand 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 takesMap.RepoToken, finds the repo root by walking up to{RepoToken}.slnx, and scans that repo'sSource/tree (UIArchitectureConventionTestsBase.cs:89,:91-:92), skippingobj/andbin/plus anyExcludedPathFragments(:97-:99, empty here since the subclass does not override:41). The two inherited facts then run:CodeBehinds_StayWithinTheLineCapglobs*.razor.csand fails any file overMaxCodeBehindLines= 400 (:44,:46,:22), first asserting at leastMinimumCodeBehindFiles= 1 file was discovered so a glob that matched nothing cannot pass vacuously (:35);RazorFiles_KeepInlineCodeBlocksSmallglobs*.razor, measures each file's inline block from the first line starting with@codeto end of file, and fails anything overMaxInlineCodeLines= 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@codeblock 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) andMMCA.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:
EventUpcastersincludes 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 fromFramework(...)entries,ModuleNamesis 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-entryDefineLayers()returningFramework(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 enumerateConcreteClassesacross 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
DataSourcesconfiguration 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 recordof 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:
LogicalNameisConference,DatabaseNameisADC_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,
LogicalNameandDatabaseName(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 keysSetNamedDataSourcepushes,DataSources__{LogicalName}__SQLServerConnectionStringandDataSources__{LogicalName}__SQLServerMigrationsAssembly(CrossServiceFixtureBase.cs:272-275). - Where it's used: as the
DataSourceslist 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
IServiceCollectionit was given, so a fluent chain stays intact. - Depends on:
AwesomeAssertionsandMicrosoft.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 theServiceCollectionitself so the call site stays one line (:25, the doc shows the shape at:16-18), invokes the registration under test (:27), and assertsresult.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, andMMCA.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 signatureEntityBuilderBase<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 asTBuilder, so theWithX(...)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 defaultWithXhelpers, 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, andMMCA.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.jsonwould 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,
IServiceCollectionandIConfigurationfromMicrosoft.Extensions.*plusAddFeatureManagementfromMicrosoft.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 classicthis-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 lastIConfigurationdescriptor and reads itsImplementationInstance(:40-42), seeds aConfigurationBuilderfrom it when one was found (:44-48), appends the flags as in-memoryFeatureManagement:{Name}keys so they win over anyFeatureManagementsection the host configured (:50-56), registers the composed root as theIConfigurationsingleton (:58), callsAddFeatureManagementagainst itsFeatureManagementsection (: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 butFeatureManagement, 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.
- The whole class body is a single C# preview
- Why it's built this way: pushing overrides through the real
IConfigurationplusAddFeatureManagementpath (rather than mocking anIFeatureManager) 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'sConfigureServices, 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 resolvedIFeatureManager(: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
ConfigureTestFeatureFlagsonly 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 concreteWebApplicationFactory, the reusable IntegrationTestBase<TFixture> stays host-agnostic, and each downstream app supplies its own concrete fixture with its ownProgram, JWT keys, and data sources. This is the boundary that keeps the shared test scaffolding inMMCA.Common.Testingand 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 anHttpClientconfigured 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 theTFixtureconstraint 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), andMicrosoft.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). TheuserIdparameter is typedUserIdentifierType(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, thekidthe host advertises on its JWKS document), and the pairedDefaultPublicKeyPem(line 49) andDefaultPrivateKeyPem(line 68). The class doc records the wiring contract: test host appsettings setJwt:SigningAlgorithm=RS256,Jwt:RsaPublicKeyPem, andJwks:KeyId(JwtTokenGenerator.cs:18-20) so RsaJwksProvider publishes a JWKS entry with the matchingkid. GenerateToken(...)(JwtTokenGenerator.cs:112-156): imports the PEM private key intoRSAParametersinside ausingso theRSAinstance can be disposed without invalidating the key held bySigningCredentials(: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.NameIdentifiercarrying the culture-invariant user id andClaimTypes.Role(:137-141).NameIdentifieris what the JWT bearer handler produces for a real token'ssubunder 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, matchingITokenService, because two claims for one identity can disagree. ConfigureInProcessTokenValidation(JwtBearerOptions options, string audience)(JwtTokenGenerator.cs:170-192): the validation half. It nullsAuthorityandConfigurationManagerand clearsRequireHttpsMetadata(: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):AddForwardedJwtBearerotherwise fetches the issuer and signing keys from the Identity service's JWKS document through the gateway, which no in-process test topology serves.
- Public constants (
- 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.ConfigureInProcessTokenValidationis called from aPostConfigure<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 byMMCA.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 perJwtSettings.RsaPrivateKeyPemand 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), plusMicrosoft.AspNetCore.TestHost'sTestServer,WebApplicationFactory,Microsoft.Extensions.Diagnostics.HealthChecks'HealthCheckServiceOptions, YARP'sIProxyConfigProvider/IProxyConfigFilter,AwesomeAssertionsandXunit(: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 throughTestServer.SendAsync(Action<HttpContext>, CancellationToken)rather than anHttpClient(:22-28), and the reason is a chain of two facts: the kit partitions onConnection.RemoteIpAddress, which aTestServerrequest 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) andDownstreamServices(:76). - Virtual knobs with kit defaults:
BypassedPaths(:84-88,/.well-known/jwks.jsonand/health, exempt because probes and JWKS discovery run at high frequency by design),NamedPolicyPath(:94, null skips that gate) andNamedPolicyPermitLimit(:100),ActiveProbeInterval(:103, 30 seconds) andActiveProbeTimeout(:106, 5 seconds),ActiveProbePath(:113,/aliveand 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) andForwardedForHeader(:122). Route_IsThrottledOnceTheClientExhaustsItsWindow(:124-144): sends exactlyPermitLimitrequests, 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 needxunit.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-225and: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 registeredHealthCheckServiceOptions(:251-252) and, per downstream, asserts a check named{DownstreamCheckPrefix}{service}exists, is taggedready, is not taggedlive, and hasFailureStatus = 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 registeredIProxyConfigFilterchain (: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 probingActiveProbePathat the declared interval and timeout (:296-304).RateLimiter_PartitionsByForwardedClientIp_NotByProxyIp(:308-331): every request arrives from the same connection IP whileX-Forwarded-Fornames 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) andSendForwardedAsync(path, proxyIp, forwardedFor)(:363-378), each shaping anHttpContexton the test server (method, HTTPS scheme, path, connection IP, and for the second one the forwarded header) and returning the response status code.
- Client addresses (
- 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/1as the limited path (:41), four downstreams (:47-53),/hubsadded to the bypass list because a SignalR connection is long-lived (:62-67), and/Auth/loginat 30 requests as its named policy (:70,:80). Store (MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/GatewayHardeningTests.cs:32-33) reads itsPermitLimitstraight 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/webhookto 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 abstractSampleGatewayHardeningTests(: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
WebApplicationFactorythat pins the hosting environment toProductionand hangs on to the startedIHost, so a test can both exercise production-only middleware branches and drive the host's own lifetime. - Depends on:
Microsoft.AspNetCore.Mvc.Testing'sWebApplicationFactory<TEntryPoint>(extended,MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:23) andMicrosoft.Extensions.Hosting'sIHost/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 reasonProductionis pinned: the restrictive CORS policy, HSTS emission, and other production-only middleware are branches a defaultDevelopmentboot 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 becauseWebApplicationFactorybuilds 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), callsbuilder.UseEnvironment("Production")(:36), then assigns and returnsbase.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.StopAsyncis not reachable through theWebApplicationFactorysurface 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 xUnitIClassFixture<...>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'sRateLimiterOptions,Microsoft.AspNetCore.Httpand 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 aPostConfigure, so it wins over whatever the host registered no matter when the host's ownAddRateLimiterran, which matters because a test factory'sConfigureTestServicescan 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 theRateLimitingstep, 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 singlePostConfigure<RateLimiterOptions>that setsGlobalLimiterto aPartitionedRateLimiter.Create<HttpContext, string>returningRateLimitPartition.GetNoLimiter(partitionName)for every request (:34-36), then returns the collection for chaining (:38).partitionNameonly 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.
- The class body is one C# preview
- 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
NeutralizeGlobalRateLimiterreference under either repo'sTests/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:
AwesomeAssertionsandXunit(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:1-2). It deliberately does not extend IntegrationTestBase<TFixture>: it needs only anHttpClient, 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 sharedAddCommonSecurityHeaders/UseCommonSecurityHeadersmiddleware (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/alivebecause 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 GETsProbePath(:26-27, threadingTestContext.Current.CancellationToken) and asserts six headers (:29-35):X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: strict-origin-when-cross-origin, aPermissions-Policycontaininggeolocation=(), aContent-Security-Policycontainingframe-ancestors 'none', and (because the host under test boots in the Production environment) an HSTSStrict-Transport-Securityheader with amax-age=.CreateClient()(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:42): abstract, the subclass supplies it from itsWebApplicationFactoryclass 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
/alivekeeps 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
CreateClientoverride,MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/SecurityHeadersTests.cs:11-12andMMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/SecurityHeadersTests.cs:12-13, each also taking aProductionHostApplicationFactory<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.ServiceBusprovisions 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.ServiceBusand its.Administrationadmin-plane client,MassTransit(v8 by policy),Testcontainers.ServiceBusand xUnit'sIAsyncLifetime(: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.1because 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.
- 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 (
- Walkthrough
- Constants and state:
DefaultEmulatorImage(:61),AdminPlanePort5300 (:64), the container, AMQP client and bus fields (:66-68), and the static constructor loweringDefaultMessageTimeToLive,BasicMessageTimeToLiveandAutoDeleteOnIdleto one hour (:70-77). - Public surface for the tests:
Client(:80-81, the AMQP data-plane client, throwing a clearInvalidOperationExceptionbefore startup),AdminClient(:87),BusControl(:95-97, namedBusControlrather thanBusso it does not shadow MassTransit's staticBus.Factory, and throwing a message that names the fix when no queue was declared), the sharedConsumedbag (:103, safe because each test matches on a value unique to itself), andHostAddress(:106,sb://localhost/). - Virtual knobs:
EmulatorImage(:112), the three wall-clock budgetsContainerStartTimeout(4 minutes,:118, sized for a cold image pull on a CI runner),BusStartTimeout(3 minutes,:121) andBusStopTimeout(1 minute,:124), andReceiveQueueName(:131, null by default for a fixture that wants the two clients and no bus). ComposeAdminConnectionString(hostname, mappedAdminPort)(:145-148):public staticand 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 andEmulatorImageis read as a virtual member (:153-158); runs PHASE 1 bounded byWaitAsync(ContainerStartTimeout)(:162) and rethrows a namedTimeoutExceptionthat 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-clientsHost(...)overload, the only v8 path onto the emulator (:184-194), with exactly ONE receive endpoint (:193); then runs PHASE 2 bounded byWaitAsync(BusStartTimeout)(:198) with its own namedTimeoutExceptionnaming 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.WaitAsyncrather than aCancellationTokenbecause 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 oneendpoint.Handler<TContract>per contract. Defaulting to no handler provisions the queue and nothing else.
- Constants and state:
- Why it's built this way: the class doc records the incident behind the shape (
:29-39). A test class implementingIAsyncLifetimeis 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 theadc-sb-emulator-smokequeue (:25,28) and binds two real integration-event contracts,UserRegisteredandSpeakerLinkedToUser(:37-46); Store (MMCA.Store/Tests/Integration/MMCA.Store.ServiceBusEmulator.IntegrationTests/Infrastructure/ServiceBusEmulatorFixture.cs:14) declaresstore-sb-emulator-smoke(:17) and bindsProductVariantChanged(: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-52on 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 privateProbeFixturesubclass (: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 fixedTask.Delaybefore 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
lastunconditionally (: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:15and the ADC equivalent atMMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/CrossServiceTestBase.cs), with call sites such asMMCA.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'sIAsyncLifetime(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,MessageBussettings, and JWT settings frombuilder.Configurationat configure-time, beforebuilder.Build(), which is beforeWebApplicationFactory.ConfigureAppConfigurationdeltas 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 duringStartAsync).[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
DummyBearerAuthorityconstant (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 publicRabbitMqConnectionString(: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) andDisposeHostsAsync(:146), plus the optional hooksOnContainersStartedAsync(:152) andConfigureSharedEnvironment(:161). SqlServerBaseConnectionString(:70-72) throws a clearInvalidOperationExceptionwhen the container has not started, and thepublic 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 withTask.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 DATABASEruns 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) pushesASPNETCORE_ENVIRONMENT=Testing(:229), one named data source per module (:243-246), the real broker settingsMessageBus__Provider=RabbitMqandConnectionStrings__rabbitmq(:249-250), and the dummy Bearer authority that only has to exist soAddForwardedJwtBearer'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 ontoDefault(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 NameMMCA-{LogicalName}(:270) so it differs ordinally from the top-levelConnectionStringsvalue 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, andSetEnvironmentVariable(: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 callsRestoreEnvironment(: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 documentedCS0618suppression for the parameterless Testcontainers module builders (:197-202).CreateDatabasesAsync(:207-225) issues one guardedIF DB_ID(...) IS NULL CREATE DATABASEper source with a scopedCA2100suppression justified because the database names are the subclass's own compile-time constants (:218).
- State: the private
- 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 prefixMMCA.ADC.Migrations.SqlServerat:69) andMMCA.Store/Tests/Integration/MMCA.Store.CrossService.IntegrationTests/Infrastructure/CrossServiceFixture.cs:29(two databases at:56-60, prefixMMCA.Store.Migrations.SqlServerat:63). MMCA.Common covers the container-free half of the base through its own privateFakeCrossServiceFixture(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
ServiceCollectionthrough 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), plusSystem.Reflection,Microsoft.Extensions.DependencyInjection,AwesomeAssertions, andXunit(: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
TryDecorateapplies decorators in reverse registration order, so the outermost decorator is the last one registered, and an innocent-looking reorder of theAddApplicationDecorators()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
- Four type parameters
(
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:34-37): a representative command with itsTResultand a representative query with itsTResult, each of which must have a concrete registered handler. ConfigureServices(IServiceCollection services)(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:46): the one abstract member. The subclass registers test doubles for the decorator dependencies (IFeatureManager, ICurrentUserService, IPermissionRegistry, ICorrelationContext, ICacheService, IUnitOfWork,ILogger<>) and then runs the repo's real registration sequence, module scans first andAddApplicationDecorators()last (doc:20-28).ExpectedCommandDecorators(:49-58) pins seven links, outermost first: FeatureGateCommandDecorator<TCommand, TResult>, AuthorizationCommandDecorator<TCommand, TResult>, LoggingCommandDecorator<TCommand, TResult>, CachingCommandDecorator<TCommand, TResult>, ValidatingCommandDecorator<TCommand, TResult>, TimeoutCommandDecorator<TCommand, TResult>, TransactionalCommandDecorator<TCommand, TResult>.ExpectedQueryDecorators(:61-69) pins six: FeatureGateQueryDecorator<TQuery, TResult>, AuthorizationQueryDecorator<TQuery, TResult>, LoggingQueryDecorator<TQuery, TResult>, CachingQueryDecorator<TQuery, TResult>, ValidatingQueryDecorator<TQuery, TResult>, TimeoutQueryDecorator<TQuery, TResult>, the query pipeline differing from the command one by the absence of the transaction only. Both lists arevirtual, so a host with a deliberately different chain can narrow them.- The two
[Fact]s,CommandPipeline_NestsDecorators_InAdr014Order(:71-73) andQueryPipeline_NestsDecorators_InAdr014Order(:75-77), each hand the closed handler interface and the expected list toAssertPipeline. AssertPipeline(:79-98): builds the collection, builds a provider, opens a scope (handlers are scoped,:84-85), resolves the outermost handler and asserts it is non-null with a message that tells the subclass author what is missing (:87-89). It then unwraps the chain, maps each link to a simple type name, and asserts every element except the last equals the expected decorator list in order (:91-94), finally asserting the innermost element does not end inDecorator, that is, it is the concrete handler (:96-97).UnwrapChain(:105-125): walks outermost to innermost by reflecting over each object's instance fields (public and non-public) and picking the first value that implements the same closed handler interface and is not the object itself (:112-115), which is how it finds the compiler-generated backing field holding the inner handler.SimpleTypeName(:127-132) strips the generic-arity backtick suffix so a two-arityLoggingCommandDecoratorcompares as the plain name.
- Four type parameters
(
- 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/PingQuerypair (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:30andMMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/DecoratorPipelineOrderTests.cs:28over the real IdentityChangePreferencesCommand/GetUserPreferencesQuerypair, andMMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/DecoratorPipelineOrderTests.cs:35over 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.StopAsyncunder a bounded cancellation token, and asserts the host drained cleanly, firingApplicationStoppingand thenApplicationStoppedinside the timeout. - Depends on:
ProductionHostApplicationFactory<TEntryPoint>
(
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:32), plusMicrosoft.Extensions.Hosting'sIHost/IHostApplicationLifetime,Microsoft.Extensions.DependencyInjection,AwesomeAssertions, andXunit(: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 plainProductionHostApplicationFactory<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 separateConfiguredAsyncDisposable(:40), with an inline comment explaining why (:38-39): the shorterawait using var factory = ....ConfigureAwait(false)form would retypefactoryand lose access toCreateClientandStartedHost.- It then creates and immediately disposes a client (
:42-45), which is what forces the lazy host to build and start, assertsStartedHostis non-null (:47-48), resolvesIHostApplicationLifetime(:49), and confirmsApplicationStartedalready fired (:50). - The stop itself (
:55-56): aCancellationTokenSourceforShutdownTimeoutSecondsandawait 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 andStopAsyncthrow (:52-54). - The two closing assertions (
:58-61):ApplicationStoppingmust have fired during the shutdown, andApplicationStoppedmust 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:9andMMCA.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'sCI.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
TFixtureconstraint,MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:14), plusXunit'sIAsyncLifetime,System.Net.Http.Headers, andSystem.Net.Http.Json(:1-3). - Concept introduced, the xUnit async test lifecycle and per-test isolation.
[Rubric §14, Testability]: the base implementsIAsyncLifetimesoInitializeAsyncruns before each test andDisposeAsyncafter, 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 = 1000seed (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:16), and theFixture/Clientprotected properties (:19-22). - Constructor (
:24-28): stores the injected fixture and eagerly creates theHttpClientfrom it. InitializeAsync(:31): aValueTaskthat awaitsFixture.ResetDatabaseAsync()before each test.DisposeAsync(:34-39): suppresses finalization and disposes the client.- Auth helpers:
SetBearerToken(string)/ClearAuthentication()(:42-48) set or clear theAuthorizationheader, the hook through which a JwtTokenGenerator token is applied. - Typed HTTP helpers:
GetAsync<T>(:51-56, which callsEnsureSuccessStatusCodethen deserializes), andPostAsync<T>/PutAsync<T>/PutAsync/DeleteAsync(:59-72) returning the rawHttpResponseMessageso a test can assert status codes. NextId()(:75):Interlocked.Incrementover the shared seed, so parallel tests never collide on generated ids.
- Fields and properties: a
- 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
Interlockedcounter 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
IHttpForwarderthat never proxies. It echoes the destination prefix, the matched cluster, and theForwarderRequestConfigit 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) andYarp.ReverseProxy.Model(IReverseProxyFeature), plusMicrosoft.AspNetCore.HttpandSystem.Globalization(:1-4). No first-party dependency. The trace headers it reads back are the ones the gateway kit's GatewayTraceHeaderTransformProvider stamps. TheYarp.ReverseProxypackage reference lives inMMCA.Common.Testingitself 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 resolvesIHttpForwarderfrom DI, soMapReverseProxy()goes through the fake exactly as a hand-writtenMapForwardercall 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, plusRouteTraceEchoHeaderandClusterTraceEchoHeaderfor 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) andClusterTraceHeaderName(:69):initproperties defaulting to the shared kit's own header names,X-MMCA-RouteandX-MMCA-Cluster, so a host with different header names can still use the fake.- The five-argument
SendAsyncoverload (:72-78) simply delegates to the six-argument one withCancellationToken.None. - The real
SendAsync(:81-115): null-guards context, config and transformer (:89-91); writes the destination prefix (:93); reads the matched cluster id offIReverseProxyFeature, falling back toUnsetValue(:94-95); writes the activity timeout, forwarded HTTP version and version policy (:96-101); runstransformer.TransformRequestAsyncagainst a throwawayHttpRequestMessage(:106-108); echoes the two stamped trace headers (:110-111); then sets a 200 and returnsForwarderError.None(:113-114). HeaderOrUnset(:121-124): reads one outbound header, joining multiple values with a comma, or returns the unset marker.
- Header constants (
- 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), plusMicrosoft.AspNetCore.Mvc.Testing(WebApplicationFactory),Microsoft.Data.SqlClient,Respawn, andXunit'sIAsyncLifetime(: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 theDataSourcescollapse 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, theWebApplicationFactory, theRespawner, a_databaseCreatedflag, and the publicClient/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), andCreateFactory()(:134, where the subclass builds the host).CreateClient()(:64) satisfies the interface by delegating to the factory. InitializeAsync(:67-96): resolves the server base fromSqlBaseEnvironmentVariableor falls back to LocalDB (:69-70), composes a GUID-suffixed database name and connection string (:71-72), forcesASPNETCORE_ENVIRONMENT=Testingand pushes the top-level SQL connection string as environment variables (:75-76), lets the subclass push its own viaConfigureTestEnvironment(:77), and builds the factory (:79); creating the client is what triggers the host'sMigrateinit to create the database and apply the module's migrations (:81-84). It then builds theRespawner, ignoring__EFMigrationsHistory(:86-95).ResetDatabaseAsync(:99-112): returns immediately when no respawner exists, otherwise opens a connection and callsRespawner.ResetAsync.DisposeAsync(:115-131): disposes client and factory, drops the database when one was created, and restores the environment.ConfigureTestEnvironment(:142-144) is an emptyvirtualhook 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 tomaster, and runs a guardedSET SINGLE_USER WITH ROLLBACK IMMEDIATEplusDROP DATABASE(:180-183), with a scopedCA2100suppression justified because the database name is a server-generated GUID, never user input (:179).
- State (
- Why it's built this way: overrides go through process environment variables because the host reads
its connection string at configure-time; forcing the
Testingenvironment skipsappsettings.Development.json(which would pointDataSourcesatlocalhost) 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 theTFixturefor 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.slnffilter.
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.jsondocument 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, andXunit(: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), andCorePublicResources(:50, the resource paths that must keep being described). OpenApiDocument_IsServed_AsWellFormedOpenApiDescribingTheApiSurface(:52-65): parses the JSON and assertsopenapistarts with3.,info.titleis non-empty, apathsobject exists, and it holds at leastMinimumPathCountentries.OpenApiDocument_DescribesEveryCorePublicResource(:67-85): first guards against a vacuous pass (the subclass must pin at least one resource,:70-71), then checks everyCorePublicResourcesentry 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.
- Overridable and abstract knobs:
- 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, andXunit(: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 400application/problem+jsonbody) and the framework'sHandleFailureResult-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 theproblem+jsoncontent type and the model-validation-only extensionstype,traceId, anderrors(:35-38).NotFound_404_HasProblemDetailsShape(:41-47): sends the 404 probe and asserts the shared shape.- Abstract probes:
SendValidationErrorProbeAsync(:54) andSendNotFoundProbeAsync(:60), the only app-specific pieces, authenticating first when the endpoint requires it (the docs suggestpageNumber=0against a[Range(1, int.MaxValue)]paged read, and reading an id that does not exist,:49-60). AssertProblemDetailsShapeAsync(:67-83): the sharedprotected staticassertion, JSON content type, echoedstatus, non-emptytitle, and at least one diagnostic extension (errors,traceId, orrequestId,: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
HandleFailurepath 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-RowVersionconflict test on top of the inherited facts (.../MMCA.ADC.Conference.IntegrationTests/Contract/ProblemDetailsContractTests.cs:40-67, reusingAssertProblemDetailsShapeAsyncat: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
/ServiceInfois served by both v1.0 (deprecated) and v2.0, selected by theapi-versionheader, 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, andXunit(: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 theServiceInfocontroller ships inMMCA.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, checksapiVersion == "1.0"and that the evolvedsupportedVersionslist is absent in the v1 shape (:35-36), then asserts anapi-deprecated-versionsresponse header contains1.0(:38-40).ServiceInfo_V2_ReturnsEvolvedShape_AndIsReportedSupported(:43-57): requests v2.0, asserts 200, checksapiVersion == "2.0"and thatsupportedVersionscontains2.0(:50-52), then asserts anapi-supported-versionsheader advertises2.0(:54-56).GetServiceInfoAsync(string apiVersion)(:59-65): clears auth and sends the GET with theapi-versionheader 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
ReportApiVersionsheaders, 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:15andMMCA.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), plusAwesomeAssertionsandXunit(:1,3). This is the reference that makesMMCA.Common.Testingdepend onMMCA.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 unreachablejwks_uriwhen the pre-forwarded capture drifts away fromUseForwardedHeaders, 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 noWebApplicationhas 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 tonull, meaning the host under test calls the zero-argumentUseCommonMiddlewarePipeline()overload. A host that customizes the pipeline overrides this with the sameAction<MiddlewarePipelineBuilder>itsProgram.cspasses (:20-23).ExpectedStepNames(:38-58): the pinned order, outermost first, all eighteen steps named throughMiddlewarePipelineStepNamesconstants rather than string literals: ExceptionHandler, CorrelationId, RequestLocalization, PreForwardedCapture, ForwardedHeaders, HttpsRedirection, ResponseCompression, Routing, Cors, Authentication, TenantResolution, RateLimiting, SoftDeletedUserFilter, Authorization, OutputCache, JwksEndpoint, OidcDiscoveryEndpoint, Controllers. It isvirtual, 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 assertsbuilder.StepNamesequalsExpectedStepNames, 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 assertsbuilder.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 byConfigure?.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
MiddlewarePipelineStepNamesconstants 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, andMMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/MiddlewarePipelineOrderTests.cs:15. The framework's ownUseCommonMiddlewarePipelinedoc 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, andNullLogger<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 ofMock<IUnitOfWork>plusGetRepositorywiring plusSaveChangesAsyncsetup.[Rubric §14, Testability]assesses whether the design permits fast isolated tests; the fact that handlers depend onIUnitOfWork(an Application-layer abstraction) rather than aDbContextis 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-configuresUnitOfWork.SaveChangesAsync(...)to return1, the success path, so a happy-path test writes no persistence setup at all. Failure-path tests override it with their ownSetup(doc:32-35). UnitOfWork(:45): theMock<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'sILogger<THandler>constructor parameter.RegisterRepository<TEntity, TIdentifierType>()(:56-64): creates aMock<IRepository<TEntity, TIdentifierType>>, wires that same object into bothGetRepository<...>()andGetReadRepository<...>()(:61-62), and returns the mock for furtherSetup/Verify. Constrained toAuditableAggregateRootEntity<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 looserAuditableBaseEntity<TIdentifierType>(:73) and wiring onlyGetReadRepository<...>()(:77).
- Constructor (
- Why it's built this way: wiring one repository mock into both accessors matters because a handler
may read through
GetReadRepositoryand write throughGetRepositoryon the same aggregate; a test forced to register two mocks would have to keep their state in sync. Pre-succeedingSaveChangesAsyncencodes 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 exampleMMCA.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 workedCreateEventHandlerTestsexample (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
HandlerTestBasereference under either repo'sTests/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'sRouteAttributeare 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'sFullNamefor a type-level attribute andFullName.MethodNamefor a method-level one, lines 40-42), and aMinimumScannedTypesfloor (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 forControllerBase, andIsRoutableComponent(line 117) looks forRouteAttributeon the type, combined inIsScannedEndpointType(line 95). AnonymousEndpoints()(line 125) isprotectedrather 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 withinherit: false(line 135) and methods are enumeratedDeclaredOnly(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).
- The subclass supplies
- 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
DeclaredOnlydecision 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 wholeTicketsController, 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 privateDriftedTests,StaleAllowListTestsandEmptyScanTestssubclasses (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
NoViolationsoverloads that turn a rule breach into a readable, offender-listing assertion failure. - Depends on:
NetArchTest.Rules.TestResultand 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;ArchitectureAssertis 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 whenresult.IsSuccessful(line 13), otherwise null-coalescesFailingTypesto an empty list, joins the full names into a bullet list (lines 18-19), and assertsIsSuccessful.Should().BeTrue(...)with the reason plus the violation list as thebecauseargument (lines 21-22).NoViolations(IEnumerable<string> violations, string reason)(line 26) materializes the sequence once and assertslist.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.AssertNoViolationscopies: 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, andAssembly.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
#1565C0and the required tokenvar(--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 viaReadEmbeddedCss(line 56, which throws a clearInvalidOperationExceptionwhen 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 withOrdinalIgnoreCaseso 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.
- Two private constants pin the forbidden literal
- 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
BrandColorTokenTestsin 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 andHashSet<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.AwardAsyncemits acallvirtnaming 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 asIDomainEventHandler<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:
_typesby full name,_ancestorsper type, and_implementorsfrom 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 namedSome.Ns.IFoo.Methodin 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.
- Three ordinal-keyed dictionaries back the index:
- 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) andDomainEventHandlersDoNotSave(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
ExpressionVisitornested in ArchitectureRules that walks a specification'sCriterialambda and collects the names of other entity types it navigates into. - Depends on:
System.Linq.Expressions.ExpressionVisitor,MemberExpression,PropertyInfo(BCL) and the RuleHelpers extension propertyInheritsAuditableEntity. - 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
Criteriaexpression tree, and visits it. This backs the polyglot and database-per-service invariant (ADR-006): aCriteriathat 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_navigatedis the accumulatingHashSet<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 throughEntityTypeOf(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 asICollection<TChild>to their element type (lines 129-136).
- The primary constructor captures
- 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 orCriteriathrows 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, andServiceHostare 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.Segmenttranslates each member to its namespace segment, and two of those translations are not the identity mapping:Apibecomes"API"andServiceHostbecomes"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.Contractsis 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, andRequiresDependenciesstill say what the repo expects, and gives the subclass a hook to assert whatRegisterDisabledStubsputs in the container. - Depends on:
[Fact](xUnit), AwesomeAssertions, andSystem.Reflection(GetInterfaces,GetProperty,BindingFlags). It has noMap: 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, matchesModulesSettingsentries byName, and decides between a hard start failure and stub registration fromRequiresDependencies(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 defaultCreateModule()is justnew()(line 68); a module without a parameterless constructor overrides that hook. - Three expectation members the subclass declares: the abstract
ExpectedName(line 27), and the virtualExpectedDependencies(line 30, defaulting to the empty list for a leaf module) andExpectedRequiresDependencies(line 36, defaulting tofalse). - Four
[Fact]s.Module_ShouldDeclare_ExpectedName(line 39) compares the readName, with abecausespelling 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 toIEnumerable<string>, asserts it is not null (the shapeModuleLoadersorts on, lines 49-50), thenBeEquivalentTothe 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 theAssertDisabledStubshook. 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 theIModuleinterface 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).
- The type parameter is constrained
- 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 againstIModule'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"],RequiresDependenciestrue, and overridesAssertDisabledStubsto assert theIUserNotificationExportServicestub descriptor is a singleton) andIdentityModuleTests(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 privateDriftedTestssubclass (: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.bicepprovisions and asserts each one keeps a matching, severity-correct triage section in that repo'sinfra/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-generatedSystem.Text.RegularExpressionsregexes,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 namesBicepResource(line 42,infra.main.bicep) andRunbookResource(line 45,infra.OPERATIONS.md), andResourceAssembly(line 51), which defaults toGetType().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 betweenvar sloAlertSpecsandresource sloAlerts(lines 109-112, each index assertion carrying its ownbecause), runsAlertKeyRegexandAlertSeverityRegexover 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.
- Overridable knobs:
- Why it's built this way: the class doc (lines 23-28) records why the
ResourceAssemblydefault is load-bearing. The base ships inside the framework package, so resolving resources against its own assembly would look for the consumer's bicep insideMMCA.Common.Testing.Architecture.dlland always throw. Defaulting to the derived type's assembly means a subclass needs no wiring beyond twoEmbeddedResourceentries 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 theResourceAssemblydefault 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
.protoparser 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
.protofiles 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 whatDescribeMemberbranches on (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:194): aServicescope renders rpcs (lines 198-209), anEnumscope 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), withOneofas the discard fallback arm.Oneofexists specifically to be treated as transparent: aoneofblock 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 theArchitectureRules.Protos.cspartial.
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 exactRequiredRole(line 31), anIsGovernedPagestrategy (line 40), and aMinimumGovernedPagesnon-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 soIsGovernedPagematched 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 theRolesproperty off the attribute instance and requires an exact ordinal match, so a bare[Authorize]or a different role fails), the subclass helperHasAuthorizeAttribute(line 94),Routes(line 97), and the base-type walkIsOrDerivesFrom(line 105) that lets a derived authorize attribute still count.
- The subclass supplies
- 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 matchIsGovernedPage(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 classicthis-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 catchingReflectionTypeLoadExceptionand returning the types that did load throughOfType<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 andImplementsGeneric(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'sAbstractValidator).DeclaredPublicProperties(line 94) narrows to declared-only public instance properties.InheritsAggregateRoot(line 101) andInheritsAuditableEntity(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 reportsfalsewhen there is no public setter, andfalseforinit-only setters by looking for theSystem.Runtime.CompilerServices.IsExternalInitrequired 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 multipleextension(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 compiledAssembly, and itsRootNamespace(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.
Moduleis 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'sDefineLayers. - Where it's used: ArchitectureMapBase stores a lazy
IReadOnlyList<LayerRef>and derives every projection from it; itsFrameworkandModulefactory helpers are what build these. The namespace-cycle rule takes aLayerRefdirectly, using itsRootNamespaceto 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 iteratesmap.Layersdirectly 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-diskAssembly.LocationthroughScannableAssemblyLocations(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
.protoparser's scope stack: a ProtoScopeKind plus the block's name, empty for a transparentoneof(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:294-297). - Depends on: ProtoScopeKind. Private to the
ArchitectureRules.Protos.cspartial. - Concept: the two-field record is what makes nested messages and enums render under their qualified name without a real grammar.
DescribeProtoFilekeeps aStack<ProtoScope>(line 112), pushes on a scope header and pops on a line starting with a closing brace (lines 124-132), andQualifiedName(line 234) reverses the stack and joins the non-empty names under the file'spackage. - Walkthrough: a two-parameter positional
sealed record(line 297), constructed only inTryPushScope(line 184). Because theOneofarm passesstring.Emptyas the name, the length filter inQualifiedName(line 238) drops it, which is exactly the wire semantics: aoneofmember is a field of the enclosing message. - Where it's used:
DescribeProtoFile(line 109),TryPushScope(line 166),DescribeMember(line 194), andQualifiedName(line 234), all feeding ProtoContractTestsBase throughBuildProtoContract(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 (
RepoTokenline 42,ModuleNamesline 45), the rawLayersinventory (line 48), and the projections the rules lean on:OfLayer(all assemblies of a kind, line 51), the per-moduleModuleDomain/ModuleApplication/ModuleShared(lines 54-60),Infrastructure()/Api()across framework plus modules (lines 63-66), the lookupsFor(module, layer)(line 69) andModuleOf(assembly)(line 72), namespace derivationRootNamespace(module, layer)(line 75), andOtherModuleNamespaces(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 Mapon nearly every*TestsBasein 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
RepoTokenand aDefineLayers()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, andSystem.IO(forFindRepoRoot). - 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 aLazy<IReadOnlyList<LayerRef>>(lines 15-16) so the assembly list materializes once, andLayersreads 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 privateModuleLayer, line 101),Infrastructure(line 48), andApi(line 51) are one-line LINQ projections overLayers.For(line 54) andModuleOf(line 59) are the lookups, both ordinal-comparison based.RootNamespace(line 63) branches on module: framework layers become theMMCA.Commonprefix 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 fromAppContext.BaseDirectoryto 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 clearInvalidOperationExceptionwhen not found (lines 89-90). - The
protected static Framework(...)(line 94) andprotected Module(...)(line 98) factory helpers build LayerRefs with the right namespace, and the internalSegment(line 105) maps each Layer to its namespace token, throwingArgumentOutOfRangeExceptionon an unmapped member (line 116).
- The constructor wraps
- 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), andHelpdeskArchitectureMap(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).FindRepoRootis 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 ruleProtoContractsMatchFrozenList(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
*Serviceclass 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 theMaxConstructorDependencieshigh-water mark (line 22; it is abstract, so every repo states its own number).ApplicationServices_DoNotExceedConstructorDependencyCeiling(line 25) scansMap.ModuleApplication()for concrete*Serviceclasses (lines 27-31), asserts at least one was found (non-vacuity, lines 33-34), computes each service's maximum constructor parameter count withDefaultIfEmpty(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
AuthenticationServicefacade (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*Serviceat 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 classsplit across thirtyArchitectureRules.*.csfiles, 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*TestsBasesupplying its own map. - Depends on: IArchitectureMap, Layer, ArchitectureAssert, RuleHelpers, CallGraphIndex, NetArchTest (
Types.InAssembly(...)),Mono.CecilplusMono.Cecil.Cilfor the IL-reading rules,System.Xml.Linqfor the props-file and.resxrules, source-generatedSystem.Text.RegularExpressionsfor the proto and localized-text parsers, and, for the specification rule,System.Linq.Expressionsplus 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*TestsBaseclasses are thin[Fact]shells that delegate. The partial is organized by concern across the filesArchitectureRules.{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 viamap.RootNamespace(...), runs aTypes.InAssembly(...)chain filtered to theControllersuffix, and reports throughArchitectureAssert.NoViolations(result, ...).ControllersDoNotDependOnEntityFrameworkCore(line 22) is the same loop against the literalMicrosoft.EntityFrameworkCorenamespace, 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) throughUiDoesNotDependOnInfrastructure(line 60), all delegating to the privateLayerNotDependOnLayer(line 139), which loops every assembly of thefromlayer and asserts no dependency on thetolayer's namespace. Two non-vacuity rules sit alongside them,LayerMapDeclaresLayers(line 72) and the twoModulesDeclareLayersoverloads (lines 89 and 113). - Reflection shape,
ControllersAreSealed(ArchitectureRules.Controllers.cs:37): flat-mapsmap.Api()to each assembly'sConcreteClasses(the RuleHelpers extension property), filters non-sealed controllers via the privateIsController(line 70, which matches on theControllersuffix 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 aMeetCustomRulepredicate (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 transitiveDomainEventHandlersDoNotSave(ArchitectureRules.DomainEventHandlerSaves.cs:76). All six open each mapped assembly withMono.Cecil.ModuleDefinition.ReadModuleoverScannableAssemblyLocations(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.
- NetArchTest shape,
- 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.targetsguards the same layer flow at build time as a second, faster gate. - Where it's used: every
*TestsBasein 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 exampleBuildProtoContract/AssertProtoContractfrom 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-50and 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.mdmust 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 optionalForbiddenResidencyClaimslist (line 23), and implementsExtractDeployedRegion(repoRoot)(line 53) against its own source of truth (the doc cites ADC parsing the SQL region default out ofdeploy.ymland Store parsinginfra/DISASTER-RECOVERY.md). The single[Fact]PrivacyPolicy_DataStorageRegion_MatchesDeployedRegion(line 26) locates the repo root viaFindRepoRooton the map's repo token (line 28), asserts the extracted region is non-blank (lines 31-32), readsPRIVACY.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.razorform underSource/Modulesmust keep its unsaved-changes guard, dirty tracking, and validatedMudForm, so those protections cannot silently regress. - Depends on: IArchitectureMap, ArchitectureMapBase
.FindRepoRoot,System.IOfile enumeration, AwesomeAssertions. - Concept introduced, the markup-scanning fitness function (it reads
.razortext, 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): theUnsavedChangesGuardcomponent, anIsDirtyAccessorbinding (through the live accessor, which pre-empts the one-render stale-IsDirtylag, a §19 concern), an_isDirtyfield, aMudFormelement, a required-field marker, and aRequiredErrormessage. The marker list isvirtual, so a repo can narrow or extend it. - Walkthrough: the subclass supplies
Map(line 17) and optionally a higherMinimumCreateFormscount (line 24, default 1).AdminCreateForms_KeepUnsavedChangesGuardAndValidation(line 38) resolves the repo root, enumerates*Create.razorunderSource/Modulesexcludingobjandbin(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.razorglob (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'sDirectory.Packages.propsmust 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 anyMMCA.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 optionallyMinimumCommonPackageCount(line 22, default 13).AllMmcaCommonPackages_ArePinnedToOneVersion(line 25) loadsDirectory.Packages.propsfrom the repo root (lines 27-30), selects everyPackageVersionelement whoseIncludestarts with theMMCA.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 atMMCA.Common/FACTS.mdfor the authoritative released-package count (lines 17-21; that count is 17 today, generated atMMCA.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
IQueryablesurfaces (Table,TableNoTracking,TableNoTrackingSingleQuery,TableNoTrackingSplitQueryon 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.IOenumeration, and a source-generatedRegex(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.cstext 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'sSource/tree by project name (lines 47-57), and isvirtualfor 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.csfile, skippingobj,bin, and allowlisted file names (lines 71-79), and reports throughArchitectureAssert.NoViolations(line 84). ScanFile(line 88) yields a file-name, line-number and trimmed-line triple for every non-comment line matchingRawQueryableAccessRegex(line 103), a[GeneratedRegex]partial property matching theTableandTableNoTrackingmember-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'sAllowedFilesoverride 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
staticmembers (which leak one user's state to another) or in singleton-registered stateful services. - Depends on: IArchitectureMap, ArchitectureMapBase
.FindRepoRoot, reflection over the UI assemblies, aSource/file scan, andSystem.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 underLayer.Ui) and optionallyAllowedStaticMembers(line 25).UiAssemblies_CarryNoMutableStaticState(line 28) reflects overMap.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 notreadonly, notconst, and not compiler-generated, plus any settable static property, minus the exempted members (lines 45-56).UiProjects_RegisterStatefulServicesScoped(line 66) scansSource/.csfiles (skippingobj,bin, non-UI paths, andTestingpaths, lines 74-80) for a line containing bothAddSingletonand aStateServiceorStateContainername, 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, andIsCompilerGenerated(line 111) treats any member name containing an angle bracket as generated, or one carryingCompilerGeneratedAttribute.
- 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.cscode-behind stays withinMaxCodeBehindLines, and a.razorfile's inline@codeblock stays withinMaxInlineCodeLines. - Depends on: IArchitectureMap, ArchitectureMapBase
.FindRepoRoot,System.IOfile 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 capsMaxCodeBehindLines(line 22, default 400),MaxInlineCodeLines(line 29, default 120),MinimumCodeBehindFiles(line 35, a non-vacuity floor, default 1), andExcludedPathFragments(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.razorfile's@codeline 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 excludingobj,bin, and the excluded fragments.
- Caveats / not-in-source: the inline-
@codemeasurement assumes the block is the file's tail (stated in the doc at lines 25-28), so a.razorfile with markup after its@codeblock 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 returningResult<T>with no public constructor, and that every domain or value-object factory returns aResult. - 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 Mapplus 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-specificDomainAggregateRootsHaveNoPublicConstructorsrule atMMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Entities.cs:90rather than the module-scoped one at line 167), andDomainFactories_ShouldReturn_Result(line 24, delegating toArchitectureRules.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 cancellationTokenparameter. - 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 optionallyCancellationTokenExemptMethods(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, orValueTask<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 throughInterfaceTargets, line 178, with unmappable generic interfaces swallowed).Dispose/DisposeAsyncand special-name members are excluded outright (line 97).
- The subclass supplies
- Why it's built this way: exempting only what is externally fixed keeps the gate honest. A framework contract such as
IHostedService.StartAsynccannot 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.JoinChannelAsyncandNotificationHub.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 passContext.ConnectionAbortedinto 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 = trueon 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 noON DELETE CASCADEto 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 optionallyAllowedCascadeExemptTypes(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 toDeleteChildren, or to a zero-argumentDeleteon something other thanthis, 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-virtualcallto a virtual method, a shape nothing else produces, so the rule can tell "delete myself" from "delete my children" without guessing.
- The subclass supplies
- 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 exemptsCategory, 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-lessSource/, 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) andHandledCommandCount(: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 optionallyAllowedUnvalidatedCommands(line 31, empty by default) andMinimumCommands(line 38, default 1). - Two
[Fact]s:Commands_ShouldHave_ValidationCoverage(line 41) forwards to the rule, andCommandInventory_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
ICommandHandlerimplemented in the map's per-module Application assemblies, restricted to closed types with at least one public settable property; aninitsetter 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 concreteIValidator<TCommand>, or the bridge, where the command implementsICommandWithRequest<TRequest>AND a concreteIValidator<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.
- The subclass supplies
- Why it's built this way: interfaces are matched by open-generic full NAME through a shared
ClosedGenericArgumentsprojection (: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 keepsAllowedUnvalidatedCommandsempty 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
*UpdateRequestimplements IConcurrencyAware, because the optimistic-concurrency token travels in theIf-Matchrequest 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 toArchitectureRules.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.SimpleNameends withUpdateRequestand 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 optionallyAllowedPublicImplementations(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 withType.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 throughCarriesServiceContractAttribute(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 ControllersExemptFromApiControllerBaselist (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), andControllers_ShouldInherit_ApiControllerBase(line 24, passing the exempt list). The underlying rules live inArchitectureRules.Controllers.csat 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 noMapon 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) loopsMassTransitPackageIds(lines 17-22:MassTransit,MassTransit.RabbitMQ,MassTransit.Azure.ServiceBus.Core) and callsPinnedPackageMajorBelowwith an exclusive ceiling of 9.ImageSharp_MustNotExceed_MajorVersion3(line 48) does the same forImageSharpPackageIds(line 45) with ceiling 4. Both id lists arevirtualso 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 anITransactionalcommand), 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 optionallyAllowedSavingTypes(line 33) andMaxCallDepth(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 atArchitectureRules.DomainEventHandlerSaves.cs:50-60). - The single
[Fact]DomainEventHandlers_ShouldNotReach_SaveChanges(line 43) forwards all three to the rule. - The rule identifies handlers by the
IDomainEventHandlerinterface full name throughCallGraphIndex.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 toSaveChangesorSaveChangesAsync(line 12) on a declaring type whose name ends withDbContext,DbContextFactory,UnitOfWorkorRepository(line 20), which is what keeps an unrelatedSaveChangesout 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
asyncor iterator method, whoseMoveNextholds the real body (doc, lines 44-49).
- The subclass supplies
- 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 sharedForbiddenDomainDependencieslist is at line 9). - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); it adds an
ExtraForbiddenDomainDependencieshook (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) andShared_ShouldBe_FrameworkFree(line 18), both passing the extra-forbidden list, thenApplication_ShouldNotDependOn_EntityFrameworkCore(line 21) andApplication_ShouldNotDependOn_AspNetCore(line 24), whose rules sit atArchitectureRules.Purity.cs:24,:39,:54and: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 atMMCA.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
throwin 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 withMono.Cecil. - Concept introduced, why a thrown business failure is a defect and not a style choice. It skips
Result.Combineinvariant 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 atArchitectureRules.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 aResultto 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 optionallyAllowedThrowingTypes(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.ReadModuleoverDomainAssemblyLocations(ArchitectureRules.DomainThrows.cs:107) and, for everythrowinstruction, 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.ThrowIfNullfamily emits a plain call with nothrowin 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;
WithUnverifiablecomposes the message atArchitectureRules.ErrorCatalog.cs:151).
- The subclass supplies
- 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.Failureor 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 returningResult<T>with no public constructor, every domain and value-object factory returns aResult, 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 atArchitectureRules.Entities.cs:167),DomainFactories_ShouldReturn_Result(line 24),DomainEntities_ShouldBe_Sealed(line 27),DomainEntities_ShouldReside_InDomainLayer(line 30),DomainEntityProperties_ShouldNotHave_PublicSetters(line 33, delegating toArchitectureRules.EntityPropertySettersAreNonPublic,ArchitectureRules.Entities.cs:149), andDtosAndRequests_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) andDistinctErrorCodeCount(: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 toMap.ModuleNames, which is the convention),AllowedSharedCodes(line 35, defaulting to the three generic statics on the framework'sErrorclass that exist precisely to be reused),MinimumErrorCodes(line 43, default 1), andIsCodePrefixAllowed(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), andErrorCodeCatalog_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
Errorfactory call sites across the per-module Domain and Application assemblies. The recognised members are every static factory onErrorplus the primary constructor (ErrorFactoryNames,ArchitectureRules.ErrorCatalog.cs:17-29); the record's copy constructor is excluded by a first-parameter-is-string check, so awithexpression 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.
- Four overridable members:
- 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.ModuleNameswith 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 inAllowedSharedCodesinstead.
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*.IntegrationEventsnamespace in the Shared layer. It also polices the upcasters that carry a retired contract forward. - Depends on: IArchitectureMap, ArchitectureRules (
ArchitectureRules.Events.csandArchitectureRules.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 byArchitectureRules.Events.cs:6,:17and:28. Two upcaster rules follow (ADR-090, doc lines 6-7):EventUpcasters_ShouldHave_UniqueSourceTypes(line 23, delegating toArchitectureRules.Upcasters.cs:12, because with two IEventUpcaster implementations reading one source contract the message a handler receives would depend on DI registration order) andEventUpcasters_ShouldIncrease_SchemaVersion(line 26, delegating toArchitectureRules.Upcasters.cs:28, which skips a source or target whoseSchemaVersionis 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 nameEventVersioningConventionTests(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/orTests/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 flatServices/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 inArchitectureRules. - Walkthrough
- Three members to override or accept:
RepoRoot(line 20, abstract, the only required one),MaxDirectFiles(line 23, virtual, default 12), andExemptFolderSuffixes(line 30, virtual, default empty). - One
[Fact],Folders_stay_narrow(line 33), which passes all three straight toArchitectureRules.FoldersStayNarrow. - The rule walks
Source/andTests/(skipping a tree that does not exist), collects every offending folder asrelative: count direct code files (max N), sorts the list ordinally and hands it toArchitectureAssert.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
.razorfile counts once and its co-locatedX.razor.cscode-behind counts with it,.resxsatellites 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).
- Three members to override or accept:
- 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
*Serviceexceeds the god-class constructor-arity ceiling. - Depends on: IArchitectureMap, ArchitectureRules (
ArchitectureRules.Handlers.cs). - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); it adds a
MaxServiceConstructorParametersoverride (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerConventionTestsBase.cs:12, default 8, matching the rule's own default atArchitectureRules.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 atArchitectureRules.Handlers.cs:6,:18,:31,:44,:70and: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
TResultis Result orResult<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, atMMCA.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 throwsInvalidOperationExceptionat runtime for any non-ResultTResult(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) andQueryHandlers_Return_ResultTypes(line 27) delegate to the matching rules, which share the privateHandlersReturnResultimplementation (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
Mapand 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'sIdempotency-Keyfilter (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 carryingHttpPostAttribute, and reports the ones carrying neitherIdempotentAttributenorNonIdempotentAttribute(lines 71-74). Attributes are matched by simple type name (HasAttributeNamed, line 80) withinherit: 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 thatTicketsController'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.HasPublicMutableSetterunderneath through the sharedMutablePropertyViolationsprojection (ArchitectureRules.Immutability.cs:76). - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); the
init-only versus mutable distinction is exactly whatHasPublicMutableSetterdetects via theIsExternalInitmodifier.[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 byArchitectureRules.Immutability.cs:6,:18,:34,:45and: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 committedExpectedContractsnapshot (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 updateExpectedContractin 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).
- The subclass supplies
- 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), andModuleRequiredLayerOverrides(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) andLayerMap_ModulesDeclareEveryExpectedLayer(line 55, passing the overrides through to the three-argumentModulesDeclareLayersatArchitectureRules.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 anArchitectureRules.Layers.csmethod:Domain_ShouldNotDependOn_Application(line 59) throughUi_ShouldNotDependOn_Infrastructure(line 95). The UI trio (lines 89-95) encodes the documented exception that UI depends only on Shared for Blazor WASM compatibility.
- Three overridable declarations come first:
- 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 atMMCA.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
.resxresources subclasses this and lists its required cultures; the build fails if any base.resxunderSource/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 noMapon this base; it scansSource/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 optionallyMinimumBaseResources(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.resxstring entry withXDocument(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 nameTranslationCompletenessTests(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
.razoror.razor.csunderSource/(snackbar messages, pageTitleproperties, page-title markup, breadcrumb labels) but resolve throughIStringLocalizerresources. - 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 optionallyMinimumScannedFiles(line 21, default 1) andAllowedFiles(line 28, whole-file exemptions; the preferred exemption is a per-linei18n: allowcomment, per the class doc at lines 8-11).UserVisibleText_IsLocalized(line 31) resolves the repo root and delegates to the rule with theSourcedirectory, 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 atMMCA.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 sharedTransportDependencieslist 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 toArchitectureRules.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 atMMCA.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 usesOtherModuleNamespacesto compute the forbidden targets through the privateModuleLayerIsolated(: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 rulesModuleDomains_ShouldNotReach_OtherModuleInfrastructures(line 25) andModuleApplications_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.Uiis 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).
- The seventh,
- 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 optionallyAllowedCycleNamespaces(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).
- The subclass supplies
- 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). ItsAllowedCycleNamespacesrecords the single accepted tangle in the framework,MMCA.Common.InfrastructuretoSettingstoPersistenceand 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.SimpleNameto match suffixes on generic types and funnels the two message rules through the sharedAssertMessageSuffix(: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 atArchitectureRules.Naming.cs:21,:33,:37,:41,:54,:67,:79,:91,:103and: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]plusIAnonymizablesoft-delete-versus-erasure model is taught in Group 02 (ADR-005).[Rubric §30, Compliance / Privacy / Data Governance]and[Rubric §11, Security]assess erasure discipline. TheIAnonymizablecontract 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 atMMCA.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
.protofiles, 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 noMapon this base: it reads committed.prototext, 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'spackage, 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,importlines, andoptiondeclarations includingcsharp_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), andFrozenProtoContracts(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.protopath 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).
- Three abstract members:
- 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.protoof 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
*.Contractsprojects: 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.Layersand runs NetArchTest per assembly withMeetCustomRule(CarriesServiceContractAttribute)as the selector (lines 40-47). That predicate (line 132) readsMono.Cecil.TypeDefinitioncustom attributes and matches the public constantServiceContractAttributeFullName(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
*.Contractsor Shared assembly (ArchitectureRules.Contracts.cs:25-29).
- The subclass supplies
- 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, itsSessions/ISessionBookmarkValidationService.cssibling,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, plusMMCA.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/andValidators/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) andValidators_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 atMMCA.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 withMono.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) andAllowedHardDeleteTypes(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 unrelatedRemoveon 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.
- The subclass supplies
- 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 aTemplateColumn. - Depends on: ArchitectureRules
.SortableGridColumnsUsePropertyColumn(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.Markup.cs:42),[Fact]. There is noMap: 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
TemplateColumnhas 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.FindRepoRootso 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
.razorTEXT rather than IL, because the defect lives in markup that compiles perfectly (rule doc,ArchitectureRules.Markup.cs:16-19). It reads eachTemplateColumntag 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 trueSortablevalue 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.
- The subclass supplies
- Where it's used: subclassed in ADC and Store, both pointing at the whole
Sourcetree 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 callsAssertNoAccessibilityViolationsAsyncgoes 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 themessagestring the thrower builds. - Why it's built this way: a purpose-named exception (not a bare
ExceptionorInvalidOperationException) 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 theScanAsync/ScanGridAsynchelpers on E2ETestBase and by the*_ShouldHaveNoAccessibilityViolationsfacts 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/DefaultPasswordhave public setters so a downstream app seeds its own admin identity from a[ModuleInitializer], whileE2E_ADMIN_EMAIL/E2E_ADMIN_PASSWORDwin 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:
DefaultEmailis"admin@localhost"andDefaultPasswordis"Admin123!"(MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:68-69);EmailandPasswordreadE2E_ADMIN_EMAIL/E2E_ADMIN_PASSWORDand 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.comin Store (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Infrastructure/TestSetup.cs:10) andadmin@adc.comin 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, andSilent(MMCA.Common.Testing.E2E/Infrastructure/AuthOutcomeRules.cs:7,:10,:13). - Depends on: nothing; a plain
internalenum, reachable from the framework's own gallery suite through theInternalsVisibleToin 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
.Classifyand 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
RunOnlyto 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) setsRunOnlytoType = "tag"with the four WCAG A/AA tag valueswcag2a,wcag2aa,wcag21a,wcag21aa(:19-23). This is the target for every strict scan.Wcag21AaExceptMudPagerCombobox(:35) repeats that tag set and adds aRulesdictionary disablingaria-input-field-name(:42-45), for grid list pages whose only violation is MudBlazor's internalMudTablePager"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 (noLabeloraria-labelparameter onMudTablePager), 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
.AssertNoAccessibilityViolationsAsyncthrough E2ETestBase.ScanAsync(strictWcag21Aa,MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:368) and.ScanGridAsync(the pager exception,:329), and directly by the*_ShouldHaveNoAccessibilityViolationsfacts 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 fewDefault*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 becauseBrowserselects the engine CI iterates over. - Walkthrough: teaching order.
DefaultBaseUrl(settable,https://localhost:7108,E2ETestConfiguration.cs:10) andBaseUrl, which prefersE2E_BASE_URL(:12-13). ADC's module initializer overrides that default tohttps://localhost:6002, the port its AppHost pins the Blazor UI to (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Infrastructure/TestSetup.cs:13).Headlesstreats any value other than"false"as headless, compared case-insensitively (:15-16);SlowMoslows each Playwright action by an environment-set millisecond count for visual debugging, default 0 (:45-46).DefaultTimeout(30_000 ms, fromE2E_TIMEOUT,:18-19) is the general action timeout.AuthTimeout(:27-28) is a separately tunable ceiling for the slowest step, the post-auth wait, inheritingDefaultTimeoutunlessE2E_AUTH_TIMEOUTis 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-modeforceLoad.Browserselectschromium(default),firefox, orwebkitfromE2E_BROWSER(:53-54);TracePathreturns a non-emptyE2E_TRACEpath or null, enabling full-speed Playwright trace capture (:63-64).
- Why it's built this way: separating
AuthTimeoutandAuthGraceTimeoutfrom the generalDefaultTimeoutis deliberate. The auth round-trip (full sign-in plusforceLoadreload 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-passwordscreen, 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-callRequestResetAsyncaction. - Depends on:
Microsoft.Playwright(IPage,ILocator,AriaRole) and the PageExtensions helpersGotoAndWaitForBlazorAsyncandFillAndVerifyAsync(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:
ConfirmationAlerttargets 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
IPagefield set in the constructor (ForgotPasswordPage.cs:8-10);EmailFieldlocated by label andSubmitButtonby its accessible name "Send a password reset link" (:12-13);ConfirmationAlertas MudBlazor's.mud-alert-text-successclass (:16);BackToLoginLinklocated by link role, with the comment recording that "Back to Sign In" is a MudButton withHrefand therefore renders as an<a>(:18-19).GotoAsyncfull-loads/forgot-passwordand waits for interactivity (:21-22).RequestResetAsyncfills the address through PageExtensions.FillAndVerifyAsyncand 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
/loginscreen. It exposes the login form's controls as namedILocatorproperties and offersGotoAsync/LoginAsyncactions, so a test saysloginPage.LoginAsync(email, password)instead of hand-querying the DOM. - Depends on:
Microsoft.Playwright(IPage,ILocator,AriaRole) and the PageExtensions helpersGotoAndWaitForBlazorAsyncandFillAndVerifyAsync(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
IPagefield set in the constructor (LoginPage.cs:8-10); locator properties forEmailField,PasswordField,LoginButton, theErrorAlert(MudBlazor's.mud-alert-text-errorclass), and theCreateAccountLink, which the inline comment explains is a MudButton withHrefand therefore renders as an<a>located by link role (:12-18).GotoAsyncnavigates throughGotoAndWaitForBlazorAsync("/login")(:20-21);LoginAsyncfills both fields through the sharedFillFieldAsyncand then clicks (:23-28). The privateFillFieldAsyncdelegates 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
/profilescreen, exposing the name, address, and password sections' fields and buttons as named locators. - Depends on:
Microsoft.Playwrightand PageExtensions.BlazorNavigateAsync(MMCA.Common.Testing.E2E/PageObjects/ProfilePage.cs:1-2). - Concept: the Page Object Model taught in LoginPage. One difference is load-bearing:
GotoAsyncusesBlazorNavigateAsync("/profile"), client-side routing (ProfilePage.cs:34-35), not a full page load, because/profileis[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 plusSaveAddressButton,:17-24); password (CurrentPasswordField,NewPasswordFieldlocated withExact = trueso it does not also match "Confirm New Password",ConfirmNewPasswordField,ChangePasswordButton,:26-30).ErrorAlertis located by ARIA alert role rather than a MudBlazor class (:32).GotoAsyncis the client-side navigation described above (:34-35). - Where it's used: by ProfileManagementTestsBase for all six of its facts. ADC's own
ProfileManagementTestscovers 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
Pathto load, anEnglishProbestring owned by a resource rendered on that page, and an optionalSettleSelectorproving 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-Plocpseudo culture every letter gains a combining accent, so the plain en-US form ofEnglishProbeappearing 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:
Pathis loaded through the culture endpoint by the base;EnglishProbemust be a string known to come from a resource rendered on the page (doc,:14-18);SettleSelectoris 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-sectionsettle 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
/registerscreen, exposing the registration form (name, email, password, plus the optional address panel) and aRegisterAsyncaction. - Depends on:
Microsoft.Playwrightand 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.
PasswordFieldusesGetByLabel("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
RegisterButtonandErrorAlert(:12-18), theAlreadyHaveAccountLinksign-in link (:21), and the optional address panel and fields (:24-29).GotoAsyncfull-loads/register(:31-32);RegisterAsyncfills the five required fields through the shared helper, reusing the same password for the confirm field, then clicks (:34-42); the privateFillFieldAsyncdelegates 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-passwordscreen, 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.Playwrightand the PageExtensions helpersGotoAndWaitForBlazorAsyncandFillAndVerifyAsync(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
IPagefield set in the constructor (:8-10);EmailFieldandTokenFieldlocated by label (:12-13);NewPasswordFieldlocated withExact = 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), andConfirmPasswordFieldbeside it (:18).SubmitButtonis located by the accessible name "Reset your password" (:20);ErrorAlertandSuccessAlertare the two MudBlazor alert classes (:21-22);GoToLoginLinkis again a link-role locator over a MudButton withHref(:24-25).GotoAsyncloads the bare page (:27-28);GotoWithLinkAsyncbuilds the/reset-passwordURL with the address and token query parameters, escaping both withUri.EscapeDataString(:34-36);ResetAsyncfills all four fields throughFillAndVerifyAsyncand 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), soResetAsyncandSuccessAlertare 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:
DefaultEmailis"user@localhost"andDefaultPasswordis"User123!"(MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:80-81);Email/PasswordpreferE2E_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 tocustomer@mmca.comin Store andcustomer@adc.comin 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, andInp(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 thewindow.__vitalsJSON 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
initdoubles 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.MeasureWebVitalsAsyncfor 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 aTask.WhenAnycontinuation. - Walkthrough: one method.
Classify(bool navigatedAway, bool errorAlertVisible, bool logoutVisible)(:38) returnsSucceededwhen the page navigated away from the auth page OR the signed-in state's logout control is showing (:40-43), and otherwiseErrorShownif an error alert is visible,Silentif not (:45). The precedence is deliberate and documented (:30-34): a completedforceLoadis 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
internaland is opened to exactly one assembly throughInternalsVisibleToonMMCA.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'sIPageandILocatorthat 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), andSystem.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._internalis set before components have attached handlers, so the wait has a second phase keyed on thedata-mmca-interactiveattribute thatMmcaThemeProvidersin 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 theextension(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.
MudDataGridRowSelectoris the public default data-row selector for a MudDataGrid in table layout (:29).BlazorRuntimeLoadedPredicateprobes for any truthywindow.Blazor._internaland is shared by the wait and byGotoProtectedAsync's readiness probe so the two cannot drift (:31-39).InteractiveMarkerPredicateprobes for thedata-mmca-interactiveattribute (:47-48).TimedSettleScriptis 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);SingleFrameSettleScriptis the one-frame flush used once the marker has already confirmed interactivity (:60). extension(IPage page)(:62).WaitForBlazorAsyncruns the two-phase wait then flushes one frame (:85-97).GotoAndWaitForBlazorAsyncnavigates, waits forLoadState.Loadrather thanNetworkIdle(which never settles under a persistent SignalR socket), then waits for interactivity (:103-110).BlazorNavigateAsyncdrives Blazor's client-side router throughBlazor.navigateTo, tolerating the context-destroyed race aforceLoadcan cause, then pollswindow.location.pathnameinstead ofWaitForURLAsync(whose default Load wait hangs on a same-document navigation) and re-asserts interactivity, retrying once if that itself races a reload (:118-151).GotoProtectedAsyncreaches 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).WaitForPageAndBlazorAsynccovers a full-page navigation's load-plus-timed-settle (:197-206).AssertNoAccessibilityViolationsAsyncrunsRunAxe(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.SearchAndWaitForRowAsyncsettles the grid, fills the search field throughFillAndVerifyAsync, 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).ConfirmDeleteAsyncclicks a delete affordance, confirms the dialog bydata-testidrather 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).WaitForGridToSettleAsyncwaits 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).FillAndVerifyAsyncfills, then auto-waitsToHaveValueAsync, 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.ClickAndVerifyAsyncwaits 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).ClickAndWaitForUrlAsyncclicks a navigating link and re-clicks until the URL matches the supplied regular expression, for grid rows whose cells wrap content inMudLinkso a row-center click lands on padding (:423-446).- The private
CompactHtmlcollapses 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).
- The constants.
- 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 theextension(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 forCompactHtml, 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'sProductListPageandCategoryListPageand ADC's session, speaker, room, event, question, category and user list pages all delegate their search and delete to it (for exampleMMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/PageObjects/ProductListPage.cs:21,:42, andMMCA.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'sIAsyncLifetime(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:
PlaywrightandBrowserare public properties with private setters (:8-9).InitializeAsynccreates the driver (:13), switches onE2ETestConfiguration.Browser.ToUpperInvariant()to pick Firefox, WebKit, or (for any unrecognized value) Chromium (:17-22), and launches it withHeadlessandSlowMofrom configuration (:24-28).DisposeAsyncsuppresses finalization, then disposes the browser and the driver defensively (:31-44): the browser is disposed only through anis { } browserpattern match and the driver through a null-conditional call, because a failedInitializeAsync(missing browser binaries, a launch timeout) leaves both null despite thenull!declarations, and the resultingNullReferenceExceptionfrom 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: aLabel, the pagePath, 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.WriteArtifactAsyncand serialized with the sharedWriteIndented = trueoptions (: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(theShould().BeLessThanOrEqualTo(...)calls,MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:4), andSystem.Globalizationfor 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 (normallyITestOutputHelper.WriteLine,:141), then asserts LCP, FCP, TTFB, and CLS against their ceilings (:143-146). INP is asserted only whensample.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 privateMessagehelper, which names the metric, the measured value, the ceiling, and the page path (:154-157).
- The static
- 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
Nameconstant (: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
PerformanceObserverscripts 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, andSystem.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 intowindow.__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:
InstallAsyncregisters the observers throughAddInitScriptAsyncso they are active on the next navigation (:40-44).CollectAsyncevaluates a script that stamps TTFB from Navigation Timing and returnswindow.__vitalsas JSON, deserialized into a WebVitalsSample (:47-57).WriteArtifactAsyncresolves the output directory fromWEB_VITALS_OUTPUT_DIRor falls back toartifacts/under the current directory, creates it, wraps the sample in a WebVitalsArtifact, and writesweb-vitals-{label}.jsonindented (: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
IAsyncLifetimeandTestContext, andMicrosoft.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 inInitializeAsyncand disposes it inDisposeAsync, 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 currentPageis a protected property (:11-18).InitializeAsynccreates a context withIgnoreHTTPSErrorsand the base URL, sets the default timeout from configuration, optionally starts trace capture with screenshots, snapshots, and sources whenTracePathis set, and opens thePage(:20-38).DisposeAsyncstops tracing, closes the page, and disposes the context, each step guarded by a null check because a failedInitializeAsyncleaves the fields null and the resultingNullReferenceExceptionwould otherwise mask the real setup error (:40-62). The privateStopTracingAsync(: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.
LoginAsAdminAsyncandLoginAsUserAsync(:92-96) delegate toLoginAsyncwith the AdminCredentials and UserCredentials pair.LoginAsync(:98-145) first clears any existing session when a sign-out button is visible, removing theauth_access_tokenandauth_refresh_tokenlocalStorage entries and issuing aDELETE /auth/session-cookiefetch, guarded against the context-destroyed race from an in-flight logout (:106-122); it then navigates to/login, fills throughFillFieldAsync, clicks, and awaitsWaitForAuthResultAsyncfollowed byWaitForInteractiveOrReloadAsync.SignOutAsync(:162-173) clicks the sign-out button and then waits for the browser to actually land on/login, matching against the shared cachedLoginUrlPatternregex on a 15 second budget (:175-176); aPlaywrightExceptionwhose message carriesNS_BINDING_ABORTEDrepeats the same wait once.RegisterNewUserAsync(:178-211) generates a uniquee2e-{id}@test.comemail with the fixed passwordTestPass123!, 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 aPlaywrightExceptionor aTimeoutException, reloads once and re-waits rather than watching the same stuck boot; the comment records why both exception types are caught (Playwright'sTimeoutExceptionderives fromSystem.TimeoutException, notPlaywrightException, 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 throughTask.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).Succeededreturns; the other two outcomes get one grace window (:279), after whichErrorShownthrows anInvalidOperationExceptioncarrying the alert text (:284-288) andSilentthrows 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 staticFillFieldAsyncdelegating 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 withWcag21Aa.
- Lifecycle. The class carries
- Why it's built this way: the auth helpers encode hard-won timing knowledge once (the
forceLoadreload, 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 forLoadState.Loadwas waiting on the CURRENT document, whose load event had already fired, so the nextgotodied with "execution context was destroyed" or "interrupted by another navigation"; the URL wait fixes that once for every caller, and theNS_BINDING_ABORTEDretry covers Firefox, which surfaces the abandoned first request when the logoutforceLoadis 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, andMicrosoft.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 throughGotoAndWaitForBlazorAsync(: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), callsbudget.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 isweb-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, andMicrosoft.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
ProtectedPathsandPublicPaths(abstract,:26,:29) and optionallyAuthenticatedUserPathandAdminPaths(virtual, defaulting to null and an empty list,:35,:44). Four facts follow.AnonymousUser_ProtectedPages_ShouldRedirectToLoginasserts each protected path bounces to/login(:46-58).AnonymousUser_PublicPages_ShouldBeAccessibleasserts each public path stays put (:60-72).RegisteredUser_AuthenticatedPage_ShouldBeAccessibleregisters a non-admin, then client-navigates throughGotoProtectedAsyncbecause SSR cannot read the JWT, passing vacuously when no path is declared (:74-93).RegisteredUser_AdminPages_ShouldBeForbiddenregisters a non-admin, then asserts each admin path renders the shared Forbidden page, matchingh1[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.assertfor a declared skip (:77-78,:98-99). The non-empty assertions onProtectedPathsandPublicPaths(: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-entryAdminPathscarries 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), andMicrosoft.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_ShouldRedirectToLoginPageregisters, confirms the sign-out button is visible, clicks it, waits for the load state, and asserts the sign-in button appears (:16-29).Logout_ShouldPreventAccessToProtectedPagesregisters, waits for interactivity (becauseRegisterNewUserAsynccan return with the button visible before JS interop is ready,:37-40), then clicks sign-out insideRunAndWaitForResponseAsyncso it blocks until the best-effortDELETE /auth/session-cookieresponse arrives (:47-51), confirms the sign-in button is visible (:54-55), and then re-requests/profileup 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
/profilebefore 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-passwordand/reset-passwordpages: 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 anunknown-{UniqueId()}@test.comaddress 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 blockOnValidSubmitso 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) andResetPasswordPage_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 strictWcag21Aapreset explicitly rather than running an unscopedRunAxe, 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_ShouldUpdateProfileNameclears and fills both name fields, saves, re-navigates, and asserts the values persisted (:26-53);ChangeAddress_ShouldUpdateProfileAddressdoes the same for the five address fields and asserts on line 1 (:55-78). Both use Playwright's plainFillAsyncrather than the re-hydration-safe helper, since the profile page is reached by client-side navigation on an already interactive runtime.ChangePassword_WithValidCurrentPassword_ShouldSucceedfills the three password fields through the sharedFillFieldAsync, waits for the "Password changed successfully." snackbar, then signs out through the base'sSignOutAsyncand logs back in with the new password (:80-109, sign-out at:105).ChangeEmail_ShouldUpdateEmailis opt-in and returns immediately unlessProfileSupportsEmailChangeis overridden true (:111-145).ProfilePage_ShouldLoadWithUserDataasserts the form is pre-filled from registration (:147-165).ProfilePage_ShouldHaveNoAccessibilityViolationsscans 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.SignOutAsyncinstead of clicking and waiting inline, so this workflow inherits both halves of the fix (the/loginURL wait rather thanLoadState.Load, and the FirefoxNS_BINDING_ABORTEDretry) 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, soMMCA.ADC.E2E.Testswrites its ownProfileManagementTestsdirectly 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-Plocpseudo 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, andSystem.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
IStringLocalizerand the framework'sPseudoStringLocalizerto 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.
ScannedPagesis the one abstract member (:60).PseudoLocaledefaults to"qps-Ploc", restated here rather than referenced because the shipped fixture library deliberately does not reference MMCA.Common.Shared (:62-66).Sentineldefaults to"[!!"(:69),OverflowTolerancePxto 1 CSS pixel so sub-pixel rounding is absorbed without admitting a genuine sideways scrollbar (:71-75),ProbeVisibleTextOnlyto false so the probe is searched in the whole document including attributes such as an input placeholder (:77-83),AssertProbePresentUnderDefaultCultureto true (:85-90), andTimeoutto 15_000 ms (:93). PseudoLocale_RendersSentinel_WithoutEnUsLeak_AndDoesNotOverflowHorizontally(:95-132) first guards non-vacuity onScannedPages(: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 evaluatesscrollWidth - clientWidthon 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, whenAssertProbePresentUnderDefaultCultureholds, asserts the probe IS present so the leak check cannot pass vacuously (:149-155).- Two private helpers:
ProbedTextAsyncselects document content or body innerText per the flag (:164-167), andSettleAsyncwaits for the optional settle selector and then for zero[role='progressbar']elements (:174-184).
- The knobs.
- 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-Plocis 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 overridesProbeVisibleTextOnlyto 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_ShouldNavigateToHomePageregisters (which auto-logs in), signs out through the base'sSignOutAsync(: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_ShouldShowErrordrivesLoginPage.LoginAsyncwith a nonexistent account and asserts the error alert appears and the URL stays on/login(:43-57).Login_NavigateToCreateAccount_ShouldGoToRegisterPageclicks the create-account link and asserts/register(:59-71).LoginPage_ShouldHaveNoAccessibilityViolationsscans with AxeOptions.Wcag21Aa(:73-84). - Why it's built this way: the sign-out step is a single call into E2ETestBase
.SignOutAsyncrather than an inline click plus wait, and the comment above it (:23-26) keeps the reason on the call site: waiting for the logoutforceLoad's/loginURL rather thanLoadState.Loadis 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, andMicrosoft.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.Cultureplusmmca_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
#1a2027or itsrgba(26,32,39,1)form, whitespace-stripped (:25-28), plus desktop and mobile action-cluster locators scoped by container (.appbar-icon-actionsand.toprow-actions,:37-39) to disambiguate the duplicated NavMenu controls. Three facts follow.CultureSwitch_ToSpanish_ShouldLocalizeAndPersistopens 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-itemand carry no menuitem role), then asserts the Spanish probe survives a fresh full page load (:41-64).ThemeToggle_ToDark_ShouldApplyAndPersistclicks the title-stable toggle (its aria-label flips with state, its title does not), asserts the palette variable flipped throughAssertDarkPaletteAsync, assertslocalStorageholdsmmca_themeset todark, then reloads and re-asserts (:66-85).MobileViewport_CultureAndTheme_ShouldBeReachablesets 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 privateAssertDarkPaletteAsyncpolls 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-labelattribute, 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, andMicrosoft.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_ShouldNavigateToHomePageregisters a unique user through the Page Object and asserts the URL left/registerand the sign-out button is visible (:17-34).Register_WithMismatchedPasswords_ShouldShowErrorfills every field throughFillAndVerifyAsync, submits exactly once, and asserts the inline "Passwords do not match" validation text plus staying on/register(:36-63).Register_WithDuplicateEmail_ShouldShowErrorregisters, then re-registers the same email and asserts the error alert (:65-79).RegisterPage_ShouldHaveNoAccessibilityViolationsscans 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 firesOnInvalidSubmitand 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'sIRenderedComponent<TComponent>(:2), andMicrosoft.AspNetCore.Components.IComponentas 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 isextension<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>viacut.FindAll("button")(:20), then takes the first whoseTextContentcontainstextunderStringComparison.OrdinalIgnoreCase(:21). On no match it throws anInvalidOperationExceptionwhose 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 toFindButtonByTextand calls bUnit'sClick(), which raises the component'sonclickthrough the renderer.HasText(string text)(:32-33) is the read form, a case-insensitiveContainsovercut.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) andMMCA.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:
HasTextmatches 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, andIReadOnlyDictionary<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'sCaptureAsync(:82-88).Methodis the verb;Uriis the full request URI and is nullable becauseHttpRequestMessage.RequestUriis;Pathisuri?.AbsolutePathwith an empty-string fallback (:85);PathAndQuerykeeps the query string thatPathdrops (:86), which is what a paging or filter assertion needs;Authorizationis the header rendered back to a string, or null when absent (:87);Bodyis the content read as text, left null when the request had no content (:75-79). Headers(:179-180) is anIReadOnlyDictionary<string, string>over anOrdinalIgnoreCasecomparer, defaulting to an empty dictionary and set through an object initializer byCaptureAsync(: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
Headersis a non-positional init member rather than a seventh positional parameter (:172-177): adding one would rewrite the record's primary constructor andDeconstruct, breaking every consumer that constructs or deconstructs a captured request.Authorizationtherefore stays where it is even though the same value is also reachable throughHeaders.
- The positional six (
- 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 moveAuthorizationintoHeadersis 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 itsRequestsFor(method, absolutePath)(:61-65); consumed by every UI HTTP-service test in the three repos, for example the optimistic-concurrency assertioncaptured.Headers["If-Match"].Should().Be("\"v7\"")and the content-header pair inMMCA.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
ErrorSummarycomponent'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'sErrorSummary.razor, which renders the marker class on its title (MMCA.Common/Source/Presentation/MMCA.Common.UI/Components/ErrorSummary.razor:13). Externals: bUnit'sIRenderedComponent<TComponent>and AngleSharp's element query API, plusMicrosoft.AspNetCore.Components.IComponentas the constraint (:1-2). - Concept introduced, testing a component that renders two different shapes for the same state.
ErrorSummaryrenders 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 queriedliwould 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 publicconst"mmca-error-summary-title", the class the component puts on its title and therefore the marker that distinguishes an error summary'sMudAlertfrom 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-alertthat 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 querieslidescendants: 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 wholeTextContentordinally, 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
MudAlertinstances (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 exampleMMCA.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
IHttpClientFactorytest double that returns a brand-newHttpClienton everyCreateClientcall, 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.IHttpClientFactoryandHttpMessageHandler(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
ObjectDisposedExceptionthat 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; matchingIHttpClientFactory'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 returnsnew HttpClient(handler, disposeHandler: false) { BaseAddress = baseAddress }. ThedisposeHandler: falseflag 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 inMMCA.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
IAuthorizationServicethat 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 BCLClaimsPrincipal. 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
IAuthorizationServiceoverloads. The requirements overload (:178-182) returnsAuthorizationResult.Success()whenuser.Identity?.IsAuthenticated == trueandAuthorizationResult.Failed()otherwise, wrapped inTask.FromResultso 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
IAuthorizationServicein the BunitComponentTestBase constructor (:57), immediately afterAddAuthorizationCore()(: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,
IsMatchplus a human-readableMessage(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; areadonly record structwith 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 structwithbool IsMatch(true when the markup matched the committed baseline or was just refreshed) andstring Message(:102-104).readonlyplusstructmeans no allocation per comparison; the record shape gives value equality and a printable form. Callers use it asresult.IsMatch.Should().BeTrue(result.Message), so the diff text that MarkupSnapshot.BuildDiffMessageproduced 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 inMMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Components/PrimitivesSnapshotTests.cs:21,31,41,51,61and 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'sMudPopoverProvider,MudDialogProvider, andMudSnackbarProvider(: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, andSnackbar(:157-160), constructed once inRenderMudProviders()after the threeRender<T>()calls (:150-153).Dialogis the handle a test queries forMudMessageBoxconfirm buttons;Snackbaris 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 exampleMMCA.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 inMMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Pages/Users/UserListTests.cs:114,122-124, which waits for the confirm button to appear inproviders.Dialogand 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
AuthenticationStateProviderBunitComponentTestBase 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 BCLClaimsPrincipal. - Concept introduced, the two ways a Blazor page learns who the user is. Some components read the cascading
AuthenticationStatevalue; others injectAuthenticationStateProviderand callGetAuthenticationStateAsync()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_principalfield (:164).SetPrincipal(ClaimsPrincipal)(:166-170) assigns the field and then calls the baseNotifyAuthenticationStateChangedwith a freshAuthenticationState, so an already-rendered<AuthorizeView>re-evaluates rather than keeping its first answer.GetAuthenticationStateAsync()(:172-173) returnsTask.FromResult(new AuthenticationState(_principal)), so no async state machine is allocated per call. - Where it's used: instantiated as the base's
_authProviderfield seeded withAnonymous(:42), registered as a singletonAuthenticationStateProvider(:58), and mutated bySetUser(:121) and by everyRenderAscall 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, andSystem.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.
HttpContentis consumed when read, so handing the sameHttpResponseMessageto 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 consumedHttpContent",: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 anHttpResponseMessage(StatusCode)and, only whenJsonBodyis not null, attaches aStringContent(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'sRespondusingLastOrDefaulton 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
AbsolutePathis compared,:121-125), so two routes that differ only by query parameters cannot be registered separately. Query assertions go through CapturedRequest.PathAndQueryinstead.
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
HttpMessageHandlerthat 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.Jsonfor body serialization, andSystem.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
HttpMessageHandlerputs 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 singleJsonSerializerOptions(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 anIReadOnlyList<CapturedRequest>.SetResponse(method, absolutePath, statusCode, body = null)(:49-58) normalizes the body through a switch: null stays null (empty body), astringis treated as raw JSON and passed through untouched, and anything else is serialized withWebJson(: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 itsHeadersfromCaptureHeaders(: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 oneOrdinalIgnoreCasedictionary, 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-Matchlives on the request whileContent-Typelives 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), else404 Not Foundwith 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).
- Static
- 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 byMMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/CapturingHttpMessageHandlerTests.cs:25,37,53,65. - Caveats / not-in-source:
_requestsand_routesare plainList<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
.htmlfile 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.RegularExpressionswith source-generated regexes andSystem.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 whenUPDATE_SNAPSHOTS=1is 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 asSnapshots/{snapshotName}.htmlin 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 theUPDATE_SNAPSHOTS=1instruction (:84-86); if no line differs it reports a length mismatch (:90-91).GuidRegex(:94-95) andHex32Regex(:97-98) are[GeneratedRegex]partial properties, which is why the class ispartial: 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'sSnapshots/folder, and by the per-module ADC component snapshot suites, for exampleMMCA.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, andMMCA.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-localStable(...)helper before callingMatch, 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 aProjectReferencetoMMCA.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"andrefreshToken = "test-refresh-token"(:18-23), assigns both, and setsAccessTokenProviderto a closure returning the propertyAccessToken(:22), not the constructor argument, so later mutations are visible through the default provider. AccessTokenandRefreshToken(: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 throwsInvalidOperationExceptionandGetAccessTokenAsyncreproduces 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), andClearTokensAsync()nulls both (:53-58).
- The constructor takes
- 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
TokenStorageproperty (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 byMMCA.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
ClaimsPrincipalinstances 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 thesubclaim name. Externals: BCLSystem.Security.Claims(:1). - Concept introduced, the authenticated-identity trap. A
ClaimsIdentityis authenticated only when it was constructed with an authentication type; an identity built from claims alone reportsIsAuthenticated == falseand 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'sAnonymousprincipal 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 underClaimTypes.NameIdentifier(TestPrincipal.cs:27-28), because a real principal reaches a page under either name: a token read directly carries the rawsubthe token service emits, while the JWT bearer handler maps it ontoNameIdentifier. A page reading through ClaimsPrincipalExtensions.FindUserIdValueresolves 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 withClaimTypes.Name, thesubclaim, andClaimTypes.NameIdentifier(:24-29), appends oneClaimTypes.Roleclaim per supplied role (:30), and returns aClaimsPrincipalover aClaimsIdentitycarrying"TestAuth"(:31).Organizer(string userId = "1")(:35-36) is a named convenience over the first: an authenticated"Organizer User"carrying theOrganizerrole, 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
.RenderAsacross all three repos, for exampleMMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.UI.Tests/Pages/Order/OrderListTests.cs:92,126(a plain authenticated render and anAdmin-role render of the same page),MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Layout/NavMenuTests.cs:48(a named user), andMMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Auth/SessionsTests.cs:80. - Caveats / not-in-source:
Organizeris ADC vocabulary living in a framework package; nothing constrains a consumer to that role name, and Store's tests pass their own roles throughAuthenticatedUser(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 fromMMCA.Common.UI(:53, defined atMMCA.Common/Source/Presentation/MMCA.Common.UI/DependencyInjection.cs:162) and the two list-page state services ListPageStateService and ListPageQueryStateService (:105-106). Externals: bUnit'sBunitContextbase,JSRuntimeMode,AddBunitPersistentComponentState, andSetRendererInfo(:2), MudBlazor'sAddMudServices, the three providers, andIBrowserViewportService(:11-12),Moqfor the inert viewport double (:10), andMicrosoft.Extensions.DependencyInjectionplus itsTryAddextensions (: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 sharedResulthelpers injectIToastServiceand the confirm-dialog facade.AddCommonUiFacades()wraps the MudBlazor services registered on the line above, so it belongs immediately afterAddMudServices(); 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 usesTryAddinternally, 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 injectIStringLocalizer<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.Systembacks the staleness measurements the notification bell's poll interval and the navigation-refresh window depend on.TryAddSingletonmeans a test that drives time deterministically registers a fake provider of its own and wins. The comment also notes thatAddLoggingabove already brought in the options infrastructure, soIOptions<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 callRenderUnderTest/RenderAs(:28-35).- The vendor-neutral facades ship with the harness (
Walkthrough: fields first, then the constructor, then the helpers.
Anonymous(:40) is astatic readonly ClaimsPrincipalover a bareClaimsIdentity, 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), looseJSInterop.Mode(:55),AddAuthorizationCore()(:56), the singleton IsAuthenticatedAuthorizationService (:57), the singleton provider instance (:58),AddLogging()andAddLocalization()(:63-64), andTryAddSingleton(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 withTryAddScoped(:105-106), substitutes an inertMock.Of<IBrowserViewportService>()whenstubViewportis true (:108-111), callsAddBunitPersistentComponentState()because the list-page base persists grid state across the prerender/interactive boundary (:113-114), and finally callsSetRendererInfo(new RendererInfo(rendererName, isInteractive))(:116-117). The doc spells out the trap:SetRendererInfobuilds 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:isInteractivebecause list pages bound their prerender fetches onRendererInfo.IsInteractive, so a non-interactive renderer means no data ever loads (:93-97), andstubViewportbecause 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 plainAddrather thanTryAddon purpose:AddMudServicesalready 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 callsRenderAs(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 cascadingTask<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) rendersMudPopoverProvider,MudDialogProvider, andMudSnackbarProvideras 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.
Moqis a deliberate dependency for exactly one thing, the viewport double, because a hand-written stub would have to track every member MudBlazor adds toIBrowserViewportService(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
BunitTestBasein 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) andMMCA.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 emptyIConfiguration), plus the equivalent bases in ADC's Identity and Engagement suites and in Store's Sales, Catalog, and Identity UI test projects.ConfigureDataGridListPageHostis called from every list-page suite, for exampleMMCA.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), andMMCA.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 byMMCA.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:30has aProjectReferencetoMMCA.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 takingMMCA.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:
IHttpClientFactoryandIDisposable(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
Disposefor 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 astatic readonly Uriofhttps://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) takesaccessToken = "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 againstDefaultBaseAddress(: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) forSetResponseregistration and request assertions,BaseAddress(:55),ClientFactory(:58) typed asIHttpClientFactoryto hand to the service, andTokenStorage(:61) typed concretely so a test can reachAccessTokenProvider. Dispose()(:63) disposes only the handler. The clients are the caller's to dispose, which is consistent withdisposeHandler: falseon each created client, so the capture log outlives them.
- Why it's built this way: passing
accessToken: nullproduces 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,78covers 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.JsonContentandSystem.Net.HttpStatusCode(:1-2). - Concept introduced, canned responses that match the real API's failure shape. The response builders are not generic conveniences:
ProblemResponseemits 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 anerrorsarray throughJsonResponse<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 toBaseAddress.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 isJsonContent.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 toJsonResponsewith 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
HttpResponseMessageinstances. 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=1authenticates 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 tripleIOptionsMonitor<AuthenticationSchemeOptions>/ILoggerFactory/System.Text.Encodings.Web.UrlEncoder(MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:19-23), and BCLSystem.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.UIcomponents 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].MapRazorComponentssurfaces 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/logininstead 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) andCookieName = "gallery_auth"(:26).HandleAuthenticateAsync()(:28) is the single override. It short-circuits first: whenRequest.Cookies["gallery_auth"]is not exactly"1"it returnsAuthenticateResult.NoResult()(:30-33), which means "this scheme has no opinion", leaving the request anonymous rather than failing it. Otherwise it builds aClaimsIdentitywith oneClaimTypes.Nameclaim of"Gallery Visitor"and, critically, passesSchemeNameas the authentication type (:35-37): supplying an authentication type is what makesIdentity.IsAuthenticatedtrue. It wraps that principal in anAuthenticationTicketand returnsAuthenticateResult.Success(:38-39). Everything is synchronous throughTask.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 byAddAuthorization()(MMCA.Common.UI.Gallery/GalleryHost.cs:74) and theUseAuthentication()/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
/gridis 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 adecimal.[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 fourPropertyColumnformatting paths the page binds: a plain integer, ordinal strings, aDateTimerendered withFormat="yyyy-MM-dd", and adecimalrendered withFormat="F2"(MMCA.Common.UI.Gallery/Pages/GridGallery.razor:28-32). It ispublicrather thaninternal(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), theTof the page'sMudDataGrid(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 returningTask.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-AddUISharedstub block. In the shared UI the only constructor taking anITokenRefresherisAuthUIService(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
AddUISharedregisters 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.
AddUISharedregisters 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 anITokenStorageService(MMCA.Common.UI/Services/Auth/AuthDelegatingHandler.cs:10-11). Nothing inAddUISharedsupplies 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) andGetRefreshTokenAsync()(:14) returnTask.FromResult<string?>(null);SetTokensAsync(accessToken, refreshToken)(:16) andClearTokensAsync()(:18) discard their inputs and returnTask.CompletedTask. - Where it's used: registered scoped by GalleryHost (
MMCA.Common.UI.Gallery/GalleryHost.cs:61), inside the block placed ahead ofAddUIShared(MMCA.Common.UI.Gallery/GalleryHost.cs:56-64). - Caveats / not-in-source: unlike NoOpAuthUIService, this registration is not a
TryAddoverride.AddUISharedmakes noITokenStorageServiceregistration 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, noDateTime.UtcNow, and the one string is formatted withCultureInfo.InvariantCultureso 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 privateDataSetRowCount = 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, andEpoch(:23) a static readonly2026-01-01T00:00:00Zwith an explicitDateTimeKind.Utc.All(:25) is anIReadOnlyList<SampleGridRow>auto-property initialized once fromBuild(), so generation happens a single time per process and every request serves the same instances.Build()(:27-42) fills aSampleGridRow[RowCount]. For indexiit computes a one-basedid(:32), formatsNameasRow {id:D4}throughstring.Create(CultureInfo.InvariantCulture, ...)(:35) giving the zero-padded"Row 0001"the test asserts on, cyclesCategorywithCategories[i % Categories.Length](:36), spacesCreatedOnone hour apart withEpoch.AddHours(i)(:37), and derivesAmountasid * 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 = 200ceiling (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.razorreadsSampleGridData.RowCountfor its on-page caption (:12) andSampleGridData.Allinside its staticFetchPageAsync(:63), which sorts by the requested column and direction (:65-82), appliesSkip((pageNumber - 1) * pageSize).Take(pageSize)(:84), and returns aResult<(IReadOnlyList<SampleGridRow> Items, int TotalItems)>(:86-87). That delegate deliberately matchesIEntityService.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
Assemblyis the gallery itself, so the shared Blazor Router (Routes.razor, which scansUIModules.Select(m => m.Assembly)) discovers the gallery's own/componentsand/gridpages. 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 fromMMCA.Common.UI.Resources, plusMudBlazor.Iconsand BCLSystem.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 sameNavItemrecord the deployed apps' menus are built from, supplying its first four positional members (title key, href, icon, resource type) and leavingRequiredRole,RequiredClaim,Section, andGroupat their defaults (MMCA.Common.UI/Common/NavItem.cs:16).[Rubric §27, i18n]: passingtypeof(SharedResource)asTitleResourceopts 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-expressionIReadOnlyList<NavItem>of four entries, Login (/login), Register (/register), Components (/components), and Grid (/grid), each pairing a label, a route, a MudBlazor Material icon, andtypeof(SharedResource)(:18-21).Assembly(:24) is an expression-bodied property returningtypeof(GalleryUIModule).Assembly, so the Router additionally scans the gallery assembly for routable components, which is howMMCA.Common.UI.Gallery/Pages/ComponentsGallery.razor:1(@page "/components") andMMCA.Common.UI.Gallery/Pages/GridGallery.razor:1(@page "/grid") become reachable. The optionalIUIModulemembers the interface declares beyond these two are left at their defaults. - Where it's used: registered as a singleton
IUIModuleby 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 fromMMCA.Common.Shared.Abstractions, the UserNotificationDTO contract, and the solution-wideUserNotificationIdentifierTypealias. - 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-itemUserNotificationDTO[](:16-29): an unread "Welcome to MMCA" with a fixed UTCSentOnof 2026-01-02 09:00, and a read "Scheduled maintenance" carrying bothReadOnandSentOn, so the inbox exercises both visual states. It wraps them in aPagedCollectionResult<UserNotificationDTO>withnew 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 constant3so the bell badge renders non-empty.MarkReadAsync(id, ct)(:37-38) andMarkAllReadAsync(ct)(:40-41) are no-ops returningResult.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.Wcag21Aascan. - Caveats / not-in-source: the badge count
3(:34-35) is a hardcoded display value and does not reconcile with the two rowsGetInboxAsyncreturns; 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 submittedTitleandBodyback 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 successfulPushNotificationDTOthat echoesrequest.Titleandrequest.Bodyand fixes the rest,Id = 99,SentByUserId = 1,RecipientCount = 42,Status = "Sent",CreatedOn2026-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 withStatus = "Sent"and one withStatus = "Failed", both withRecipientCount = 128, so the history table renders both status treatments, then wraps them in aPagedCollectionResult<PushNotificationDTO>withPaginationMetadata(: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.Wcag21AaExceptMudPagerComboboxbecause 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 fromMMCA.Common.Shared.Abstractions, and the LoginRequest, RegisterRequest, AuthenticationResponse, and RefreshSessionSummaryResponse contracts fromMMCA.Common.Shared.Auth. - Concept introduced, registration order as the override mechanism. This stub is registered before
AddUIShared, whoseTryAddScoped<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 theIAuthUIServiceabstraction rather than onAuthUIService, so swapping the implementation needs no change to any page. - Walkthrough:
- Shared constants (
:16-23): twostatic readonlysession GUIDs,CurrentDeviceSessionId(all-ones) andOtherDeviceSessionId(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 sharedError.Failure("Gallery.AuthUnavailable", "The gallery has no backend.")reused by every failing member. - Credential operations (
:25-32):LoginAsync,RegisterAsync, andExchangeOAuthCodeAsynceach returnResult.Failure<AuthenticationResponse>(Unavailable), so the pages render and submit but never establish a session. - Session lifecycle (
:34-40):LogoutAsync()returnsTask.CompletedTask,TryRefreshTokenAsync(ct)returnsfalse, andChangePasswordAsync(currentPassword, newPassword, ct)returnsResult.Failure(Unavailable). - Password reset (
:44-48):RequestPasswordResetAsync(email, ct)andResetPasswordAsync(email, token, newPassword, ct)also fail withUnavailable. The comment at:42-43records 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 twoRefreshSessionSummaryResponserows, 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 nullIpAddress,IsCurrent: false). The comment at:50-52gives 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)returnsResult.Failure(Unavailable), so the button is present and wired without changing anything.
- Shared constants (
- Why it's built this way: placing the stub ahead of
AddUISharedexploits the shared UI'sTryAdd*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 failureResultrather 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-AddUISharedstub 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, soAuthorizeViewandCascadingAuthenticationStateagree with whatever GalleryFakeAuthenticationHandler decided for the request. Without thegallery_authcookie 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) andIHostEnvironmentAuthenticationStateProvider(the interface the Blazor Server host calls into) at:16-17,IHttpContextAccessorinjected as a primary-constructor parameter (:16), and BCLClaimsPrincipal/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 throughIHttpContextAccessor, and for the interactive circuit the framework pushes the handshake user in throughIHostEnvironmentAuthenticationStateProvider.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 astatic readonly AuthenticationStatewrapping an emptyClaimsPrincipal(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_hostStatefirst and returns it verbatim when present (:26-29): the circuit's handshake user always wins. Otherwise it falls back to the SSR path, readinghttpContextAccessor.HttpContext?.User(:31) and returning a newAuthenticationState(user)only whenuser?.Identity?.IsAuthenticated == true, else the sharedAnonymous(:32-34). Both branches useTask.FromResult, so no async machinery is allocated per call.SetAuthenticationState(Task<AuthenticationState>)(:37-38) is theIHostEnvironmentAuthenticationStateProviderimplementation and simply stores the task; it does not callNotifyAuthenticationStateChanged. - 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
AuthenticationStateProviderby GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:64), immediately afterAddHttpContextAccessor()(MMCA.Common.UI.Gallery/GalleryHost.cs:63), which supplies its dependency. - Caveats / not-in-source: the class implements
IHostEnvironmentAuthenticationStateProviderbut is registered only under theAuthenticationStateProviderservice type (MMCA.Common.UI.Gallery/GalleryHost.cs:64). Whether the Blazor Server host resolves this same instance when it callsSetAuthenticationState, 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.UIauth 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 insideMMCA.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 sharedMMCA.Common.UIsurface (AddUIShared, the gallery's ownApproot component inMMCA.Common.UI.Gallery/Components/App.razor, andMMCA.Common.UI._Importsas the additional-assembly marker), and SupportedCultures fromMMCA.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 inProgram.cs, so two callers share the identical configured app: thedotnet runentry point, whichRunAsync()s it (MMCA.Common.UI.Gallery/Program.cs:7-8), and the E2E collection fixture GalleryHostFixture, whichStartAsync()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'sui-e2ejob (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-6records the rationale, oneBuildAppfor both entry points avoids the separatedotnet runplus health-poll that made ADC's e2e cold start fragile. - Walkthrough:
- Assembly name and base dir (
:33-34):typeof(GalleryHost).Assembly.GetName().Nameis captured without a null-forgiving operator; the comment at:30-32explains that CI's nullable analysis treatsAssemblyName.Nameas 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.jsresolve 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.jsonand forced on withUseStaticWebAssets(). 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(), thenAddMudServices(). - Extension-point stubs, before
AddUIShared(:59-63): scopedIAuthUIService,ITokenStorageService, andITokenRefresher, thenAddHttpContextAccessor()and the scopedAuthenticationStateProvider. The ordering comment (:55-58) states the mechanism,AddUIShared'sTryAdd*registrations defer to whatever is already present. - Real authentication and authorization (
:69-73):AddAuthentication(GalleryFakeAuthenticationHandler.SchemeName)plusAddScheme<AuthenticationSchemeOptions, GalleryFakeAuthenticationHandler>(...), thenAddAuthorization(), because the guarded pages'[Authorize]surfaces as endpoint metadata (comment,:65-68). - Canned notification services (
:78-80): scopedNotificationState,INotificationInboxUIService, andIPushNotificationUIService, so the notification pages discovered from theMMCA.Common.UIassembly render populated markup (comment,:75-77). - Notification scope (
:85): scopedINotificationScopeProviderbound to the sharedNullNotificationScopeProvider, whoseGetCurrentScopeKeyAsyncreturns 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 singletonIUIModule, so the shared Router discovers the gallery's own/componentsand/gridpages (comment,:87-89). - Shared UI (
:95):AddUIShared(builder.Configuration)registers theApiSettings/LayoutSettingsbinding, the"APIClient"HttpClient, and the remaining shared services; the in-memoryApi:ApiEndpointfromappsettings.jsonsatisfies validation and deliberately points at the unroutablehttp://api.gallery.invalid(MMCA.Common.UI.Gallery/appsettings.json:2-4), and the client is never invoked becauseIAuthUIServiceis stubbed (comment,:92-94). - Build (
:97):builder.Build(), after which only middleware and endpoints are configured. - Request localization (
:104-108): buildsgalleryCulturesas[.. SupportedCultures.All, SupportedCultures.PseudoLocale]and applies it as the supported and supported-UI culture set overSupportedCultures.Default. The comment (:99-103) is explicit that this mirrors the real hosts' ADR-027 allowlist but additionally enablesqps-Plocunconditionally, 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 keepsqps-PlocDevelopment-only viaUseCommonRequestLocalization, 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()thenUseAuthorization()(WebApplication insertsUseRoutingahead of them automatically, comment:110-111), thenUseAntiforgery(), 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):MapStaticAssetsis given the gallery's own{galleryAssemblyName}.staticwebassets.endpoints.jsonfor the same in-process self-host reason as above (comment,:119-120); a/healthendpoint returnsResults.Ok("Healthy")(:124); andMapRazorComponents<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-startedWebApplication, leaving the start mode to the caller.
- Assembly name and base dir (
- Why it's built this way: keeping the whole build in
BuildApprather thanProgram.cslets the E2E fixture host the identical configured app in-process on a real bound port viaStartAsync, notWebApplicationFactory's in-memory TestServer, which Playwright cannot reach over the wire. GalleryHostFixture therefore clears the URLs, bindshttp://127.0.0.1:0, and reads the ephemeral address back fromIServerAddressesFeature(MMCA.Common.UI.E2E.Tests/Infrastructure/GalleryHostFixture.cs:26-37). This is deliberate CI infrastructure and is never shipped: the csproj setsIsPackable=falseand records that the project is deliberately outsideMMCA.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(thedotnet runentry) 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/componentswhileBuildAppalso serves the guarded notification and sessions pages and the/gridvirtualization 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
EmbeddedResourceentries in the csproj that supply the files the base reads:infra/main.bicepunder the logical nameinfra.main.bicepandinfra/OPERATIONS.mdunderinfra.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
ResourceAssemblytoGetType().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 ofMinimumAlertSpecs, 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; andEveryRunbookAlertSection_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 toinfra.main.bicepandinfra.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-testjob (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.Architecturepackage (referenced atMMCA.ADC.Architecture.Tests/MMCA.ADC.Architecture.Tests.csproj:41), plus theEmbeddedResourceitem 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
*TestsBasein the sharedMMCA.Common.Testing.Architecturepackage, 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 theLogicalNamethe csproj assigns when it embedsSource/Modules/Conference/MMCA.ADC.Conference.UI/Pages/Home/ADCHome.razor.cssas 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 factLandingPageCss_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 ownBrandColorTokenTestsguards 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 thebuild-and-testjob restores, builds, and tests on every PR and every push tomain(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 atMMCA.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_AreAllowListedfails on an unlisted[AllowAnonymous](MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:54),ScannedEndpointSet_IsNotEmptyfails when fewer thanMinimumScannedTypesendpoint types were discovered at all (:66), andAllowList_HasNoStaleEntriesfails 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 viaAssembly.Load. The base scans each for the two shapes it understands, MVC controllers (any type whose base chain reachesControllerBase,AnonymousEndpointTestsBase.cs:101-:112) and routable Blazor components (any type carrying aRouteAttribute,: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,LoginAsyncandRegisterAsync, 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: theGetAllAsync/GetAllForLookupAsync/GetByIdAsynctriple 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 bareFullNameversusFullName.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 withDeclaredOnlyandinherit: falseprecisely 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/orTests/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 capMaxDirectFiles => 12(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FolderWidthTestsBase.cs:23), an emptyExemptFolderSuffixesdefault that ADC does not override, so ADC carries no documented exemption (:29), and the single factFolders_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.razorcomponent and its co-locatedX.razor.cscode-behind count as one unit,.resxsatellites 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 namedbin,obj,Migrations,Platforms,Resources,node_modules,wwwrootor.gitare 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.NoViolationswith 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}/besidePages/{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 thebuild-and-testjob 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
.protofiles the four*.Contractsprojects 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
.protofiles off disk from the repo root, so this class takes no map and the csproj needs no reference to the*.Contractsprojects. - Concept introduced, pinning a contract that no compiler checks. A
.protofile 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 issyntax,import, andoptionlines includingcsharp_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) implementsProtoContractTestsBase.SolutionFileName(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ProtoContractTestsBase.cs:22). The rule resolves the repo root from it viaArchitectureMapBase.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.protocompiled by the four*.Contractsprojects (: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: 64message ...lines (:22-:85), one per field, each ending in= <number> : <type>, and 12service ...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 (printArchitectureRules.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
.protoof its own, so the framework does not subclass it, and a repo with a*.Contractsproject does (ProtoContractTestsBase.cs:9-:11). The four*.Contractsprojects 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
.protoadded to a*.Contractsproject is not pinned until someone lists it here. Nothing asserts thatProtoFilescovers every.protoin 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 aTemplateColumn. The subclass supplies one scan root, the whole ADCSourcetree (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
TemplateColumnhas none, soSortable="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 fromFindRepoRootrather 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 wholeSourcetree 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 isSortableColumns_ShouldNotBe_TemplateColumns(SortableColumnConventionTestsBase.cs:27), delegating toArchitectureRules.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
PropertyColumnfor 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
.razormarkup, so a grid composed at runtime, or a column whoseSortablevalue 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
*.resxunderSource/must have a complete, non-empty Spanish.es.resxsibling, 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 isTranslations_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.Testingpackage (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
TryDecorateapplies decorators in reverse registration order, so the last decorator registered becomes the outermost wrapper. That makes an innocent-looking reorder of theAddApplicationDecorators()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 bareServiceCollectionwith 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>(), andMock.Of<ICacheService>()as singletons, a scopedIUnitOfWorkfactory, and the open genericILogger<>mapped toNullLogger<>. 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(), thenScanModuleApplicationServices<MMCA.ADC.Identity.Application.ClassReference>(), thenAddApplicationDecorators()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 aValidatingQueryDecoratortoo (:67), so the two lists differ only by the transactional stage. Both are asserted byAssertPipeline(: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.
- Test doubles for the decorator constructor dependencies (
- 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.Applicationand 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 atMMCA.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
Assemblyobjects, 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'sModulegrouping 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 theFramework(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:Resultfor Shared,BaseEntity<>for Domain,EntityQueryService<,,>for Application,ApplicationDbContextfor Infrastructure, andApiControllerBasefor API. A rename or a package move breaks the compile instead of silently producing an empty scan. - Module layers (
:28-:54) use the instanceModule(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 throughAssembly.Load(for example:29,:30,:33), because those assemblies have no convenient public anchor type (:5-:6).Assembly.Loadsucceeds here only because the csproj takes aProjectReferenceon 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 byNotification.Shared.UserNotifications.UserNotificationExportItemDTO), and Api (pinned byNotification.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 aLazy<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 theAssembly.Loadcost 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 isinternal, 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
*.Contractsservice projects underSource/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.Testingpackage (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-argumentUseCommonMiddlewarePipeline(), 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), andExpectedStepNames(: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) comparesbuilder.StepNamesagainst 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) callsBuild()and requires it not to throw, becauseBuild()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_uristops being reachable), authentication immediately before tenant resolution (so the claim strategy seesHttpContext.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). NoWebApplicationis 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 ADCProgram.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 overrodeConfigurehere.
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 = trueand 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'sDeleteChildren<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), andAllowedCascadeExemptTypes(: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 isAggregatesWithChildCollections_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, aRowVersionor 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 genericMMCA.Common.Application.UseCases.Crud.DeleteEntityCommandclosed over ADC aggregates, which ADC does not author (:58-:60). (2) False positives of the scan (:62-:70): ForgotPasswordCommand and ResetPasswordCommand both carry anICommandWithRequest<T>whoseIValidator<T>is real but lives inMMCA.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, andSetUserAvatareach 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 checkCommandInventory_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
*UpdateRequestcarries 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 isUpdateRequests_ShouldNotImplement_IConcurrencyAware(:14), which delegates toArchitectureRules.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 theIf-Matchrequest 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-testjob (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
*Servicein 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'sAuthenticationServiceBaseconstructor took on the session store and its options, and ADC's subclass passes both through on top of theIExternalLoginEmailVerifierits 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), andMaxConstructorDependencies => 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 isApplicationServices_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 theAllowedPublicImplementationshook (: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, theDisabled*stub in*.Shared, and the gRPC adapter inServices/*.Contracts, and all of them areinternal 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 throughInternalsVisibleToon 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/*.Contractsprojects are not among them, so the gRPC adapters'internalvisibility 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 domainResultand neither has anything to gain from theResult-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) andControllersExemptFromApiControllerBase(: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 inheritApiControllerBaseexcept 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.mdmust 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 requiresPRIVACY.mdto say the same thing, comparing whitespace-insensitively and case-insensitively so "West US 2" matches thewestus2region 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 throughArchitectureMapBase.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 markerSQL_LOCATION_OVERRIDE:-with an ordinalIndexOf(:24-:25), asserts the marker exists with abecauseexplaining 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-levelSQL_LOCATION_OVERRIDEenv binding (deploy.yml:1111) and the shell default (:1158), so the rule depends on the marker stringSQL_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 byMaxCallDepth, 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 afterSaveChangesAsync, and after commit inside anITransactionalcommand, 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), andAllowedSavingTypes(: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 isDomainEventHandlers_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) andExtraForbiddenDomainDependencies => ["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
throwin 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 skipsResult.Combineinvariant 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 barethrow;rethrow is ignored, and theArgumentNullException.ThrowIfNullfamily emits nothrowin 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), andAllowedThrowingTypes(: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 isDomain_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 aResult.Failureor 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, theResult-returningCreatefactory, 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-Resultconstruction 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 spreadsMap.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 exampleCheckIns.EventNotPublishedreached from CheckInProcessor,RecordRoomCheckInandRecordSponsorVisit, andSessionQuestion.InvalidTransitionreached fromModerateQuestionHandlerand 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 anErrorfactory call in an ADC module into an argument of a framework member that builds theErrorinside MMCA.Common. The comment records each move (70 to 66 when fiveX.NotDeletedcodes moved intoRestoreChild(notDeletedErrorCode: ...)arguments; 66 to 57 when eight more moved intoICurrentUserService.RequireUserId(code, ...)andCommonInvariantsmembers), 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 checkErrorCodeCatalog_ShouldNotBe_Empty(:54). Prefix matching isprefix + ".", ordinal (:62-:67).
- Why it's built this way - codes are read out of IL at the
Errorfactory 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
*.IntegrationEventsnamespace 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 enforceSchemaVersion, 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, andEventUpcasters_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,
RequiredMarkersis 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'sRequired="true"/RequiredErrorpair is deliberately not inherited, because every ADC create form now sources its field rules from a form model through theMMCA.Common.UI.Validationbridge, 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) andMinimumCreateForms => 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 keyValidation.CorrectFollowing. The inherited fact that consumes them isAdminCreateForms_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.razorunderSource/Modules, skippingobjandbin(:72-:77), re-asserts theMinimumCreateFormsfloor (:79-:80), and then, for each form, appends every sibling*FormFields.razorin the same directory into one scanned buffer before looking forModelValidation.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 toSource/Modules/Identity/MMCA.ADC.Identity.UI/Pages/Users/Profile/Profile.razor(:109-:110) and asserts the file exists first, with abecauseexplaining 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 theValidateNewPassword/ValidateConfirmPasswordclient-side wiring, reporting every missing marker at once rather than failing on the first (:125-:130). Finally it counts occurrences ofRequired="true"andRequiredError, requiring at least three of each so all three password fields stay required and keep a user-facing message (:132-:135), using the localCountOccurrenceshelper (: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.razorglob (: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.razorfails 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'sDirectory.Packages.propsmust 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 isAllMmcaCommonPackages_ArePinnedToOneVersion(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FrameworkVersionConsistencyTestsBase.cs:25), and the base's non-vacuity floorMinimumCommonPackageCountstays at its default of 13 (:22). ADC currently pins 16MMCA.Common.*packages inMMCA.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) andMaxServiceConstructorParameters => 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 sharedAuthenticationServiceBaseconstructor took on the refresh-session store and its options, and ADC's subclass passes both through alongside theIExternalLoginEmailVerifierits 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
*Servicetypes in module Application assemblies). Whether they cover an identical type set is decided insideArchitectureRulesand 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
TResultmust be Result orResult<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-Keyfilter, 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 toArchitectureRules.PostActionsDeclareIdempotencyIntent(Map)(ArchitectureRules.Idempotency.cs:44-:61), which walks the concrete controllers in everyLayer.Apiassembly the map declares (:48-:54). Attributes are read withinherit: trueand abstract controller types are skipped, so an ADC controller that inherits a POST action fromAuthControllerBaseorAggregateRootEntityControllerBasealready 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
Apilayer, 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/initimmutability 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), andExpectedContract(:9-:20), a collection expression of seven strings inFullName { 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 isIntegrationEventContracts_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. TheAttendeeCheckedInentry carries its own inline justification (:15-:16):SponsorIdis 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_DeclaresEveryExpectedLayerandLayerMap_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): trimmingRequiredModuleLayerswould 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), andModuleRequiredLayerOverrides(: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:60through: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 inMMCA.Common/CLAUDE.md: the same boundaries are enforced at compile time in the framework byMMCA.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
.razoror.razor.csunderSource/(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: allowcomment 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) andMinimumScannedFiles => 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 isUserVisibleText_IsLocalized(LocalizedTextConventionTestsBase.cs:31), and the base'sAllowedFileshook (:28) is left empty, so ADC exempts nothing at file granularity. The scan covers snackbar messages, pageTitleproperties,<PageTitle>markup, breadcrumb labels, and NavItem titles, the last of which must carry aTitleResource(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
*.DomainEventsnamespace 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:13through: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
IQueryablesurfaces (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), andSystem.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 fromFindRepoRoot("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 -
AllowedFilesmatches 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 fromMap.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.Contractsin 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 toArchitectureRules.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*.Contractsor 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), andIUserNotificationExportService(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
*.Contractsprojects 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_TheirContractsandValidators_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, andExecuteDelete/ExecuteDeleteAsyncoutside 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 theMMCA.Commonnamespace 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), andAllowedHardDeleteTypes(: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 nameMMCA.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 noIsDeletedflag 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).
- EFRepository<TEntity, TIdentifierType> (
- 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_CarryNoMutableStaticStateandUiProjects_RegisterStatefulServicesScoped(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:28,:66). The base'sAllowedStaticMembershook (:25) is left at its empty default, so ADC's module UIs have zero static-state allowances. Notification declares noLayer.Uiassembly 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@codeblocks 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_StayWithinTheLineCapandRazorFiles_KeepInlineCodeBlocksSmall(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/UIArchitectureConventionTestsBase.cs:44,:63). All four tuning hooks stay at their base defaults:MaxCodeBehindLines400 (:22),MaxInlineCodeLines120 (:29),MinimumCodeBehindFiles1 (:35), and an emptyExcludedPathFragments(: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.jsonsets"runner": "Microsoft.Testing.Platform"), with AwesomeAssertions for fluent asserts, Moq for test doubles, and coverlet for coverage (see for exampleMMCA.Common/Tests/Hosting/MMCA.Common.Aspire.Hosting.Tests/MMCA.Common.Aspire.Hosting.Tests.csproj:8). The lone exception isMMCA.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 onlyAPI,Application, andIntegrationTestsprojects, because its domain and persistence live inMMCA.Commonand 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/Timeoutdecorators, 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 theActivityaggregate. Each one appears below as a named cluster in the row that owns it. - Fitness tests and shared bases live elsewhere.
MMCA.Common.Architecture.TestsandMMCA.ADC.Architecture.Tests(the NetArchTest layer/purity/extraction suites, thin subclasses of the sharedArchitectureRulesrule 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 theMMCA.Common.UI.Galleryharness. 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 intoMMCA.Common.Testing, and its Service Bus emulator fixture collapsed to a subclass the weekServiceBusEmulatorFixtureBaselanded. - Four projects sit outside
MMCA.Common.slnxon purpose.MMCA.Common.UI.Gallery,MMCA.Common.UI.E2E.Tests,MMCA.Common.Benchmarks, andMMCA.Common.Infrastructure.Redis.Testsare absent from the solution file (MMCA.Common/MMCA.Common.slnx:32-37lists theTests/Coreprojects,:39-43theTests/Presentationones and:45-49theTests/Hostingones) so thatdotnet test --solution MMCA.Common.slnxnever 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-e2eatMMCA.Common/.github/workflows/ci.yml:228,performance-smokeat:332,redis-integrationat:741). - Two integration tiers, deliberately split. Each service has a per-service
*.IntegrationTestsproject that boots one host throughWebApplicationFactory<Program>with cross-service gRPC edges faked and no broker (these run in theintegration-testsCI job and need a real SQL Server named byADC_TEST_SQL_BASE). Separately,MMCA.ADC.CrossService.IntegrationTestsandMMCA.ADC.ServiceBusEmulator.IntegrationTestsrun against Testcontainers to prove the genuine broker and gRPC round-trips: both live in the weekday-nightlyMMCA.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 thecross-service-freshnessjob atMMCA.ADC/.github/workflows/deploy.yml:815, a 5-day window set byFRESHNESS_DAYSatMMCA.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 (BaseEntity→AuditableBaseEntity→AuditableAggregateRootEntity), 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) • Index • Coverage audit ➡