Skip to content

Automated tests

Inventory of what the unit and integration suites pin. Every entry is at least one file under src/dotnet/Modgud.Tests.Unit/ or src/dotnet/Modgud.Api.Tests/.

Two test projects

ProjectPurposeRun timeNeeds Docker?
Modgud.Tests.UnitPure logic — pinning behaviour of helpers, evaluators, aggregates, flavors. No web host, no Marten, no Wolverine.~1 s test execution, ~3 s wall-clock with buildno
Modgud.Api.TestsIntegration — full WebApplicationFactory against a real Testcontainers Postgres. End-to-end HTTP through the actual middleware stack.~6-7 minyes

Run commands:

bash
cd src/dotnet

# Fast feedback — recommended default during development
dotnet test Modgud.Tests.Unit

# Full integration suite (Docker must be running)
dotnet test Modgud.Api.Tests

# Both
dotnet test

Unit-test inventory

1283 tests in Modgud.Tests.Unit (dotnet test Modgud.Tests.Unit). The per-area numbers below are snapshots and will drift as tests are added; if a row's count is off, trust the file's own [Fact] count over this table.

Authorization slice

AreaFile(s)TestsWhat's pinned
Permission evaluationAuthorization/PermissionEvaluatorTests.cs152-segment <resource>:<action> matching inside an App's catalog, realm:admin realm-wide bypass, <resource>:admin resource-wide bypass, no cross-app/cross-resource leak, no substring match (oauth:admin does NOT cover oauth-client:read), null/empty argument guards
Resource registryResources/ResourceRegistryTests.cs16(appSlug, resource)-keyed registration, permission listing, case-sensitive lookup
Person principalAuthorization/Principals/PersonTests.cs12DisplayName fallback chain (Acronym → Name → AccountName → Id), whitespace-only-fields filter
Group principalAuthorization/Principals/GroupTests.cs15GetEmailsAsync over Shared / ExpandToMembers / Shared-without-Email-fallback, inactive/deleted/dangling-member skips, nested recursion, cycle detection (a group graph with a cycle must not infinite-loop or stack-overflow)
ServiceAccount principalAuthorization/Principals/ServiceAccountTests.cs4type discriminator, DisplayName, capability-interface set
App aggregate + projectionAuthorization/Apps/*Tests.cs11Create / Setters / Delete / Replay; IsSystem flag for the seeded modgud app
App slug rulesAuthorization/Apps/AppSlugRulesTests.csreserved set (realm, *, modgud), 3-63 chars, lowercase + digits + hyphens, no leading/trailing hyphen
Membership-script JsEval security (adversarial)Authorization/MembershipSecurityTests.cs30+JsEval threat-model suite, six test groups mirroring attacker classes A1-A6: A1 resource exhaustion/DoS (depth/length caps, transpile+eval timeouts), A2 native-host escape (eval/Function/globalThis/require/import rejected at translation, dangerous globals removed, prototype pollution neutralized, DocumentSession not reachable), A3 type confusion (BigInt/NaN/overflow/negative-zero), A4 cross-tenant probe (unknown property/identifier rejected at translation), A5 SQL-injection via LINQ (metacharacters stay literal constants, malformed GUID input rejected), A6 information disclosure (translator errors never leak Acornima internals or stack traces)

Realms

AreaFile(s)TestsWhat's pinned
Realm slug grammarRealms/RealmSlugRulesTests.cs33length 3–63, leading letter, trailing letter/digit, lowercase + digits + hyphen, reserved-set with case-insensitive checks
Realm cache lookupRealms/RealmCacheLookupTests.cs13exact host match, localhost fallback to single active realm, multi-realm safety

OAuth domain + wire format

AreaFile(s)TestsWhat's pinned
OAuth Application aggregateOAuth/OAuthApplicationAggregateTests.cs14Create / Setters / Delete / Replay, AppIds n:m semantics + legacy-AppIdChanged replay
OAuth Scope aggregateOAuth/OAuthScopeAggregateTests.cs12Create / Setters / Delete / Replay; AppId null-vs-set
OAuth Api aggregateOAuth/OAuthApiAggregateTests.cs12Create / Setters / Enable+Disable / Delete / Replay; AppId 1:1 link
LoginProvider aggregateIdentity/LoginProviderAggregateTests.cs14Create / Setters / Delete / Replay (incl. Configuration defensive copy)
StandardScopes constant setOAuth/StandardScopesTests.cs7the seeded built-in scopes are stable
OAuth wire-format constantsOAuth/OAuthApplicationKeysTests.cs (25), OAuth/OAuthConstantsTests.cs (32), OAuth/ScopePropertyKeysTests.cs (7)64every permission prefix (scp:/gt:/rst:/ept:), grant-type strings (incl. RFC-8628 device-code URN), client/consent types, cocoar: setting + property keys, distinctness across namespaces
OAuthAdminMapping (extracted)Application/OAuthAdminMappingTests.cs70+BuildClientPermissions, grant-type round-trip, BuildClient* defaults + property survival, MapClient/MapScope, MapApiState (id-stringification, defensive list copies), MergeClientSettings/MergeClientProperties partial-PATCH semantics (omit-preserve / value-overwrite / list-replace / no-mutation), BCrypt hash+verify round-trip and malformed-hash safety
OAuth *StateProjection (3) + LoginProviderInfrastructure/Persistence/Marten/Projections/OAuth/*Tests.cs + LoginProviders/...Tests.cs54Create + every Apply + replay (incl. AccessTokenType case-sensitive parse bug pinning, AppIds n:m projection, AppId set/null/created-default for Scope + Api)

ClaimsTransformation library

AreaFile(s)TestsWhat's pinned
ModgudClaimsTransformationClient/AspNetCore/ModgudClaimsTransformationTests.cs12per-app role flattening from resource_access[<app>].roles to ClaimTypes.Role, cross-app isolation, malformed JSON tolerance, idempotence, anonymous short-circuit, AppSlug configuration validation

ExternalAuth (OIDC IdP federation)

AreaFile(s)TestsWhat's pinned
EntraId flavorExternalAuth/EntraIdFlavorTests.cs15identity, config schema, v2-authority shape, common multi-tenant alias, throws on missing TenantId
Generic OIDC flavorExternalAuth/GenericOidcFlavorTests.cs15identity, config schema, well-known suffix-strip incl. Keycloak realm path
Flavor registryExternalAuth/FlavorRegistryTests.cs10case-insensitive Get/TryGet, KeyNotFoundException with key listing, duplicate-key construction throws

Authentication slice

AreaFile(s)TestsWhat's pinned
Domain typesAuthentication/Domain/{EmailOtpChallenge, MagicLinkChallenge, UserSecurityData, UserSession, ApplicationUser}Tests.cs51OTP/Magic-Link expiry + match semantics, security-stamp rotation asymmetry, session expiry, ApplicationUser default state
ExtensionsAuthentication/ExtensionMethods/{HttpContextExtensions, HttpRequestExtensions, ErrorOrExtensions}Tests.cs25tenant accessor on HttpContext, source-IP resolution incl. the X-Forwarded-For pinning bug, ErrorOr → ProblemDetails mapping
TwoFactorEnforcementMiddlewareAuthentication/Account/TwoFactorEnforcementMiddlewareTests.cs23whitelist paths, federated-MFA AMR detection, early-exit branches; DB branches unit-untested by design
Sessions / SessionTrackerAuthentication/Sessions/SessionTrackerTests.cs5best-effort tracking, swallows failures from ISessionService
Device info parsingSessions/DeviceInfoServiceTests.cs8Wangkanai.Detection mapping pins driven by a fake IDetectionService: browser/platform/device → DeviceInfo, "Others" collapse to "Unknown", version-zero collapse to null, defensive throw-swallow. Mac-Safari-as-Mobile pin gone (fix landed with the swap)
EmailOtpConfigurationAuthentication/Identity/EmailOtpConfigurationTests.cs2default values
TwoFactorHelper (extracted)Authentication/Account/Services/TwoFactorHelperTests.cs10BuildMethodsList order/conditions (TOTP/email-with-address-required/passkey count), TryExpireSetupGrace exempt-bypass + DueAt overwrite

Infrastructure + Api glue

AreaFile(s)TestsWhat's pinned
Email templates / In-memory serviceInfrastructure/Email/{EmailTemplateStore, InMemoryEmailService}Tests.cs20placeholder substitution + every template enum value, in-memory capture/recall/Clear
UserView mapper + projectionInfrastructure/Persistence/Marten/Mappers/UserViewMapperTests.cs (6) + .../Projections/Users/UserViewTests.cs (8)14DTO mapping, ShortGuid encoding, GetDisplayLabel fallback (incl. whitespace-pinning bug)
ViewRef recordInfrastructure/Persistence/Marten/Projections/ViewRefTests.cs5record value-equality
TenantConstantsInfrastructure/Persistence/Tenancy/TenantConstantsTests.cs3wire-format string contract: "system", "TenantId" HttpContext key
Tenant context middlewareApi/TenantContextMiddlewareTests.cs5sets IMessageBus.TenantId from HttpContext.Items["TenantId"], falls back to system, ignores non-string values
SignalR side-effect messagesInfrastructure/Events/SignalRSideEffectMessagesTests.cs6record shape + enum integer values (over-the-wire format)
ProjectionSideEffectsInfrastructure/Events/ProjectionSideEffectsTests.cs1smoke

Api features (extracted helpers)

AreaFile(s)TestsWhat's pinned
Consent-URL helperApi/Features/Auth/OAuth/ConsentUrlHelperTests.cs13ParseAuthorizationUrl on /connect/authorize URLs (params, missing, malformed-URI → null+[], NRE on null bubbles up to guard against catch-widening regressions), AppendErrorToUrl with proper URL encoding
Authorization-endpoint claim helpersApi/Features/Auth/OAuth/AuthorizationEndpointHelpersTests.cs16GetDisplayName fallback chain (Firstname Lastname → UserName → Email), GetDestinations claim-type→token-target switch — name/preferred_username/given_name/family_name (profile scope), email/email_verified (email scope), role (roles scope), SecurityStamp suppressed, unknown-claim default to AccessToken
PaginationRequest.WithDefaultsApplication/PaginationRequestTests.cs6non-positive raw page/pageSize clamped to 1/20, valid passthrough, parameterless-ctor and clamp targets agree
Group-cycle detectorApi/Features/Admin/GroupCycleDetectorTests.cs10DetectCycles on linear / branching / no-cycle / self-loop / 2-node / 3-node cycles
Realms endpoint MapToDto + filterApi/Features/Admin/RealmsEndpointsTests.cs6RealmDto mapping, RequireControlPlaneFilter 404-on-missing
Auto-membership sync paths + ShouldSyncApi/Features/Groups/AutoMembershipSyncHandlersTests.cs18PrincipalPaths constants pinning, ShouldSync per handler (UserCreated, UserUpdated, UserDeleted, UserActivated/Deactivated, GroupCreated/Updated/Deleted)

Integration-test inventory

485 tests in Modgud.Api.Tests, all green against Testcontainers PostgreSQL (~6-7 min, Docker required). The per-folder breakdown below is a snapshot and will drift as tests are added; dotnet test Modgud.Api.Tests --list-tests is authoritative.

FolderFilesWhat's covered
Users/5User CRUD via the singular /api/user endpoint, account lifecycle (soft-delete/recycle-bin/sweep), GDPR deletion settings, configurable required registration fields
Security/20AuthEnforcement (grace period, whitelist), MFA (TOTP), EmailOtp, MagicLink, ProfileSelfService (UserChangeRequest), OWASP Top 10 (see below), security-stamp/session revocation on password/2FA/credential changes, control-plane transfer + separation, passkey hardening, service-account revocation
Authorization/37End-to-end permission gating (PermissionResolutionTests), plus the newer feature surfaces: invite-code self-registration, per-realm auth rate limits, CIMD, native cookieless grants (OTP/magic-link/passkey, incl. per-client WebAuthn RP-ID), the device authorization flow, dynamic client registration, OIDC federation (issuer anchoring, first-signal consistency), application settings + the settings cascade, signing-key rotation, roles/groups endpoint robustness
ColdStart/15Full-process boot + declarative realm provisioning: cold-start bootstrap, login/magic-link/passkey contracts, realm create/hard-delete, manifest export/apply/parity, the provisioning test kit, recovery CLI commands
ExternalAuth/13OIDC IdpConfig CRUD, ExternalLoginProcessor (JIT account creation + linking), DynamicOidcSchemeManager, FlavorRegistry, ExternalIdentityLink aggregate + lifecycle, UserUpdateScriptRunner (JsEval), federation
Admin/1Projection-rebuild endpoint
Audit/5Audit endpoint, GDPR erasure survival in the audit trail, auth-audit-view projection, login-failure-streak emission, security audit store
Observability/1OpenTelemetry log redaction
Principals/1PrincipalEmailResolver (group expansion)
Infrastructure/10Shared test fixtures and harnesses (Testcontainers Postgres fixture, cold-start web-application factory, CLI harness, software WebAuthn authenticator) — not test classes themselves

OWASP Top 10 (2021)

Security/OwaspTop10Tests.cs, 12 tests, tagged with the xUnit trait OWASP=Top10. Run a focused pass with dotnet test --filter "OWASP=Top10".

CategoryTestsWhat's pinned
A01 Broken Access Control3All admin endpoints require auth (anon → 401), authentication alone is not enough (403 without permission), no horizontal escalation (regular user cannot read another user's sessions)
A02 Cryptographic Failures3Auth cookie is HttpOnly, no PasswordHash field in any user-detail response, login does not reveal user existence (identical 401 + body for unknown user vs. wrong password on existing user)
A03 Injection1SQL-injection payload in login username returns vanilla 401 — never crashes (Marten parameterises every query)
A05 Security Misconfiguration1Public-facing error responses do not leak .NET stack-trace markers or DB-driver internals
A07 Identification and Authentication Failures4Brute-force lockout after 5 failed attempts, weak passwords rejected by Identity, deactivated user cannot sign in (same 401 as unknown), forgot-password always returns 200 with byte-identical body for known and unknown users

A04 (Insecure Design), A06 (Vulnerable Components), A08 (Software and Data Integrity), A09 (Logging and Monitoring), A10 (SSRF) are addressed at the architecture / dependency / event-sourcing level rather than via assertable HTTP contracts; see the file's class-level comment for the rationale.

What we deliberately do NOT unit-test

These are listed so we don't have the same "should we test this?" conversation again.

  • Pure DTOs / records with no logicLoginProviderState, OAuth*State, Realm, SessionDtos, ProfileUpdateDto, UserChangeRequest, StoredPasskeyCredential, IdpConfig, ExternalIdentityLink, AuthLogDocument, UserDeletionState. The compiler is the test.
  • Pure enums + interfacesEmailMode, MembershipMode, IdpFlavor, IPrincipal, all IAuthSettings / IMagicLinkConfiguration / IServerConfiguration interfaces, IEmailService, IGlobalStore, IMasterConnectionString, ITenantSessionFactory, ISessionService, IDeviceInfoService. Nothing to assert.
  • Mapperly-generated mappers — generated code, no behaviour of ours.
  • External librariesCocoar.Json.Mutable's MutableJsonMerge, Cocoar.JsEval, BCrypt, Wangkanai.Detection. They have their own tests; we test our use of them, not them.
  • Heavy services with DB / JsEval / HTTP / DIOAuthAdminService (after the pure-helper extraction above, the only remaining instance method is MapApiAsync, a one-line DB-load wrapper around the pure MapApiState helper), MembershipEvaluator (Jint.Engine + JsExpressionTranslator on the membership-script path), RealmProvisioningService, RealmCache (lookup logic already extracted to RealmCacheLookup and tested), SmtpEmailService, PostmarkEmailService, AdminNotifier, EventSourcedUserStore, EmailOtpService, AuthLogPersistenceService, RecoveryCli. These belong in integration tests; they don't survive the no-Docker contract.
  • OpenIddict pipeline handlersAccessTokenTypeHandler, RealmIssuerHandler. Need OpenIddict server pipeline-context types not constructible without server DI.
  • Marten OpenIddict stores (×4) and OpenIddictExtensions, OAuthMartenSetup, OAuthRealmSeeder, MartenConfiguration, TenantedSessionFactory. Marten/DI heavy by design.
  • Setup / DI registrationDependencyInjection.cs, DependencyInjectionExtensions.cs, MartenStoreOptionsExtensions.cs. Wiring, no logic.
  • Minimal-API endpoint files (*Endpoints.cs) — need WebApplicationFactory. Tested at integration level if at all.

Several of the source files listed above (PermissionEvaluator, RealmSlugRules, RealmCacheLookup, OAuthAdminMapping, ConsentUrlHelper, AuthorizationEndpointHelpers, GroupCycleDetector, TwoFactorHelper, …) started life as private helpers inside a larger service and were pulled out to internal static so they could be unit-tested directly, with no behaviour change. That's why the test-file column above often names a small static helper class rather than the endpoint or service file itself.

Released under the Apache-2.0 License.