Manual smoke checklist
End-to-end smoke pass for the live system. Order is outside-in: bring the system up first, then exercise auth, then admin, then OAuth, then the cross-cutting flows.
Legend: ✅ = verified
❌ = found a problem (note + link to the finding below)
⏳ = not yet tested / open
Last run: 2026-04-30 against the Docker prod-shipping constellation (
pnpm build→ copy towwwroot/→dotnet publish -c Release -o output/Modgud→docker build -f src/dotnet/Dockerfile→docker runon thecocoar-devnetwork alongside the existingcocoar-postgrescontainer). 24 sections walked. 18 findings logged below; the three shipping-blockers (F1, F2, F16) plus three quick wins (F5, F8, F11) and the AuthLog template-rendering bug (F6) have been fixed in this same branch. F9, F18 and the rest are open.
Automated coverage (Playwright, runs against the prod-shipping image with Mailpit catching outbound SMTP — see
src/frontend-vue/e2e/):
- §1 First-time setup —
00-smoke.spec.tsexhausts it.- §2 Login & sign-out —
00-smoke.spec.tscovers UI login, sign-out, magic-link round-trip via Mailpit, and the A02 user-existence-leak guard. Lockout + cookie-restart-persistence stay manual (lockout wastes a 60-second wall-clock window we don't want in every CI run; cookie persistence needs a real browser-restart).- §6 Users (admin CRUD) —
10-admin.spec.ts: UI list renders + admin row visible, POST /api/user creates withStatus: Pending, PUT updates name fields viaOptional<T>. Lock/unlock + soft-delete + per-user grace-period stay manual.- §7 Roles —
10-admin.spec.ts: UI list renders + three default roles seeded with the post-Phase-1 shape, role create.- §8 Groups —
10-admin.spec.ts: UI list renders, group create acceptsBoundTo, response has noAccessScriptsfield (Phase 6 wire-shape verified). Auto-membership scripts + modal-tab walk stay manual.- §9 Apps —
10-admin.spec.ts: system app isIsSystem: true, create non-system app, reserved-slug rejection (realm / modgud / *).- §10 OAuth Clients + §12 OAuth APIs —
10-admin.spec.ts: list endpoints return 200 + paginated shape even without?page=/?pageSize=(F16 fix). Create + AppIds MultiSelect round-trips stay manual.- §11 OAuth Scopes —
10-admin.spec.ts: five standard scopes seeded.- §21 Permission gating + bypass tiers —
20-permission-gating.spec.ts: builds three non-admin users (read-only, resource-admin, app-admin) plus the realm-admin and asserts the API gate AND the SPA sidebar visibility — both must agree. The integration testPermissionResolutionTestsalready exhausts the gate logic; this spec adds the SPA-sidebar mirror, where a mismatch between the front-end'sauth.store.tsand the back-end'sPermissionEvaluatorwould surface.- §4 Profile self-service —
30-profile.spec.ts: change a non-email field →AdminApprovalPending; change email →EmailVerificationPending+ Mailpit captures the verification link; user clicks (POST verify-email) →AdminApprovalPending; admin approves →/mereflects the new firstname AND email atomically.- §14 Realms —
40-realms.spec.ts: system realm exists withIsControlPlane: true, create a fresh realm provisions a new tenant DB + realm document, slug rejection (reservedsystem, length / casing / leading-hyphen invalids).- §3 Two-factor (TOTP) —
50-2fa.spec.ts: enable TOTP via/api/account/mfa/setup+/verify, sign out, sign in with password →RequiresMfa: true, complete with a freshotplib.generateSync({ secret })code,/meconfirmsHas2FA: true. Wrong code on second-factor login → 401.33 / 33 tests green, ~33 s on a warm rig (~60 s on first run because the modgud image gets built). Run via
cd src/frontend-vue && pnpm test:e2e.
0. Bring-up
- ✅ Backend builds:
cd src/dotnet && dotnet build - ✅ Postgres container running:
docker ps | grep cocoar-postgres - ✅ Master DB exists: created on the fly (
docker exec cocoar-postgres psql -U postgres -c "CREATE DATABASE modgud") - ✅ Backend starts cleanly with only the env vars from
docker-compose.yml— F1 + F2 fixed: compose file rewritten to use the correct<section>__<property>names (DBSETTINGS__CONNECTIONSTRING,OPENIDDICT__ISSUERetc.), and the defaultAppUrlis nowhttp://0.0.0.0:80so the cert-less prod image boots out of the box. Live-verified after the fix:[INF] Now listening on: http://0.0.0.0:80. - ✅ No errors on startup once the env-var override is applied — bootstrap path runs (master DB → schema → system tenant → realm seed → app seed → cache warm), seeded
5 scopes, Internal login provider: Trueandsystem app 'modgud'. - ✅ Frontend served from
wwwroot/after publish (Prod-shipping constellation, notpnpm dev). - ✅
http://localhost:4200/loads without console errors.
Note for the checklist itself: the original wording of this section ("
pnpm devon 4300, backend on 9099") is the dev workflow, not what we ship. The pinned runtime is the Docker image. The list above is the dockerised version of the same checks.
1. First-time setup
- ✅
/setupis reachable on a fresh DB. - ✅ Submit username
admin, passwordABC12abc!, no email — succeeds. Password-rules ✓ panel updates live (At least 8 characters, uppercase, lowercase, digit). - ✅ Auto-login lands on
/dashboard. - ✅ Sidebar shows all 4 sections (
AUTORISIERUNG,OAUTH & FEDERATION,IDENTITÄTSQUELLEN,SYSTEM) — confirmsrealm:adminis granted to the System Admin group withBoundTo: ["*"]. - ❌
Auth logshowsInitial admin createdand the grace-period stamp, notUserCreated/UserLoggedInas worded in the checklist — the events ARE persisted (verified via/api/admin/auth-log), the frontend grid simply does not refresh after auto-login. See F6 (rendering bug) and F7.
Other observations during this section: F5 (the Apps sidebar item shows the literal i18n key instead of a translation), F8 (doc promises Auth-Log columns the UI does not render), F9 (UI still in German while docs are English-only).
2. Login & sign-out
- ✅ Sign out via header menu →
/login. - ✅ Wrong password 5× → 6th attempt with the correct password still returns 401 (account locked).
- ✅ After ~60 s the account is unlocked and the correct password succeeds.
- ⏳ "Remember me" persists the cookie across browser restart — not yet verified; would need a second browser session and a real-clock wait. The cookie itself was inspected though:
Modgud.Auth=…; path=/; secure; samesite=strict; httponly— A02 controls all in place. - ✅ Magic-link end-to-end (request → click link → lands logged in) — automated in
00-smoke.spec.ts. The Playwright rig stands up Mailpit alongside the auth container, pointsEMAIL__SMTP__HOSTat it, and the spec polls Mailpit's REST API for the verification mail then POSTs the token. Same outbound SMTP path the prod image takes, no dev-mode shortcut. F10 fixed. - ⏳ Logout-everywhere from
/profile/sessions— not yet verified; needs a second browser session.
Side observation during the login flow: F11 (the browser's de-AT locale 404s before falling back to de), F12 (form fields append on fill via DevTools — likely a missing select-on-focus on the input).
3. Two-factor
The grace-period flow + the TOTP path are automated end to end in 50-2fa.spec.ts against the production-mode container. Email-OTP is in reach with the same Mailpit harness — porting it is just one more spec; recovery codes and passkey need either a UI walk through the profile-page modal or Page.addVirtualAuthenticator() (the helper is already in helpers.ts).
- ✅ The grace-period prompt fires for users without 2FA at
AuthenticationMinimumLevel >= 1. - ✅ Enable TOTP via API (
/mfa/setup+/mfa/verify) and sign in with a fresh code generated byotplib—50-2fa.spec.ts. - ✅ Sign out + sign in: TOTP step validates a fresh code —
50-2fa.spec.ts. - ✅ Wrong code on second-factor login is rejected with 401 —
50-2fa.spec.ts. - ⏳ One recovery code consumed once; second try rejected — not yet verified (recovery-codes spec not ported).
- ⏳ Enable Email-OTP and sign in with the code — not yet verified, but ready to port (Mailpit + the same flow shape as TOTP).
- ⏳ Add a passkey (FIDO2) and sign in with it — not yet verified (
helpers.addVirtualAuthenticator()is the building block). - ⏳ Disable each 2FA method — not yet verified.
4. Profile self-service
- ✅ Edit
Firstname/Lastname/Acronym—30-profile.spec.tsexercises the change-request path; non-email field changes go straight toAdminApprovalPending. - ✅ Change email → admin notification → admin approves/rejects —
30-profile.spec.tswalks the full chain: PUT request → Mailpit captures the verification mail → anon POST verify-email with the link'sid+token→ admin POST approve →/mereflects the new firstname AND email atomically. - ⏳ GDPR export — not yet verified.
- ⏳ GDPR delete request → confirmation token → deletion → masking — not yet verified, but the Mailpit harness + the existing helpers cover the email-token mechanics; a port follows the §4 change-request shape.
5. Sessions (self + admin)
- ⏳
/profile/sessionslists the current session with browser/OS/device/IP — not yet verified. - ⏳ Open a second browser → row appears — not yet verified.
- ⏳ Revoke other session → second browser is signed out — not yet verified.
- ⏳ Admin → Users → user → Sessions: same view + force-logout — not yet verified.
6. Users (admin)
- ✅ Create a user via
POST /api/user(no password, only email) — returns 200 withStatus: Pending,HasPassword: false,IsActive: true. The new row appears in the admin grid via SignalR. - ⏳ Edit a user's profile fields via the admin UI — not yet verified (API works; UI walk pending).
- ⏳ Lock / unlock via the unlock endpoint — not yet verified.
- ⏳ Soft-delete + GDPR tear-down — not yet verified.
- ⏳ Admin sends magic link from user detail — not yet verified.
- ⏳ 2FA grace-period extension visible + editable per user — not yet verified.
7. Roles
- ✅ Create role
User Reader(AppSlug=modgud,ResourceType=user,Permissions=["read"]) —POST /api/rolereturns 200, automated in10-admin.spec.ts. - ⏳ Edit + delete via the UI modal — not yet verified (UI walk pending; API CRUD covered by integration tests).
- ✅ Three default roles exist after first-time setup with the post-Phase-1 model — automated in
10-admin.spec.ts:- System Admin →
["realm:admin"](wasapp:adminin legacy — confirmed migrated) - User Manager →
modgud:user:read/write,modgud:session:read/write,modgud:authorization-group:read,modgud:permission-role:read,modgud:auth-log:read(3-segment confirmed) - Viewer →
modgud:user:read,modgud:authorization-group:read,modgud:permission-role:read
- System Admin →
8. Groups (this is where Phase 6 changes most)
- ✅ Group create response carries no
AccessScriptsfield — Phase-6 ABAC excision confirmed at the wire level. Automated in10-admin.spec.ts. - ✅ Bound to apps is on the create payload and round-trips:
POST /api/groupwithBoundTo: ["modgud"]returns the field unchanged. Automated. - ✅ BoundTo
["*"]wildcard accepted by the API — automated in10-admin.spec.ts. - ⏳ Group detail modal tabs: General / Members / Script (auto only) / Roles / Effective — not yet verified end-to-end via the modal; an earlier UI snapshot did not show an "Access" tab so Phase-6 looks correct, but the click-through wasn't done in this run.
- ⏳ BoundTo
[](dormant) actually drops permission contributions — not yet verified at the gate in this run; this path is heavily integration-tested inAuthorization/PermissionResolutionTests.cs(10 tests). - ⏳ BoundTo
["*"]wildcard active everywhere (gate behaviour) — integration-tested; not in the E2E suite. - ⏳ Auto-Membership: write
(p) => Type.Is(p, 'person') && p.IsActive, save, Effective tab shows matches — not yet verified. - ⏳ Membership-script error path →
MembershipLastErrorshown — not yet verified. - ⏳ Cycle prevention on adding a descendant group — not yet verified manually; covered by
GroupCycleDetectorTests.
9. Apps
- ✅ Admin → Applications:
modgudlisted as system app,IsSystem=true, all 15 resources in place. Automated in10-admin.spec.ts. - ✅ Create non-system app via
POST /api/app→ returns 200,IsSystem=false. Automated. - ✅ Reserved-slug rejection —
realm,modgud,*all return 400. Automated. - ⏳ Lookup endpoint
GET /api/app/lookupminimal-shape — not yet verified.
10. OAuth Clients
- ✅
GET /api/admin/oauth/clientsand/oauth/apisreturn{Items: [], TotalCount: 0}with 200 even without?page=/?pageSize=. F16 fixed — both endpoints now declare the params asint? = nulland clamp to defaults viaWithDefaults. - ⏳ Create a confidential web client; assign apps via AppIds MultiSelect — not yet verified.
- ⏳ Edit AppIds; on remove, scopes pinned to the removed app stop validating — not yet verified.
- ⏳ Rotate secret (one-time reveal) — not yet verified.
- ⏳ PATCH semantics (omit AppIds = no change,
[]= detach-all) — not yet verified manually; covered byOAuthAdminMappingTests.
11. OAuth Scopes
- ✅ Standard scopes (
openid,email,profile,roles,offline_access) seeded withAppId = null(global).GET /api/admin/oauth/scopesreturns the paginated{Items: [...]}shape. - ⏳ Create custom scope with
AppId = acme-tasks→ discoverable on/.well-known/openid-configuration— not yet verified. - ⏳ Scope on app A used by client linked only to app B → token endpoint rejects — not yet verified manually; covered by
OAuthScopeAggregateTests. - ⏳ App-scoped scopes flagged visually — not yet verified.
12. OAuth APIs (Resource Servers)
- ✅ List endpoint returns
{Items: [], TotalCount: 0}even without pagination params. F16 fixed alongside/oauth/clients. - ⏳ Create RS, link to app
acme-tasks— not yet verified. - ⏳ Move RS to a different app →
resource_accessblock switches catalog — not yet verified. - ⏳ RS without linked App → UserInfo emits no
resource_accessfor that audience — not yet verified.
13. Login providers + IdP Config (OIDC)
- ✅ Internal Login Provider listed and active by default (
IsBuiltIn=true,Type=Internal). - ⏳ Create an IdP config (Entra ID flavor) — not yet verified.
- ⏳ Generic OIDC discovery URL → Test connection succeeds — not yet verified.
- ⏳ UserUpdateScript runs on first JIT login — not yet verified manually; covered by
UserUpdateScriptRunnerTestsintegration test. - ⏳ Test-script endpoint returns user diff for sample claims — not yet verified.
- ⏳ Disable an IdP → login button disappears — not yet verified.
- ⏳ External login JIT → user created + signed in — not yet verified.
- ⏳
ExternalIdentityLinkexists on user detail — not yet verified. - ⏳ Account-linking from
/profileadds a second IdP — not yet verified.
14. Realms (system realm only)
- ✅ System realm exists with
Slug=system,IsControlPlane=true,IsActive=true— automated in40-realms.spec.ts. - ✅ Create realm provisions a tenant DB + realm document + bootstrap-invite —
POST /api/admin/realmswith mandatoryInitialAdminreturns{Realm, InitialAdminInvite}. Automated. Slug rejection (reservedsystem, length / casing / leading-hyphen) and missing-InitialAdmin.Emailrejection automated too. - ⏳ Click the magic-link from a fresh realm's bootstrap email and complete
/bootstrap?token=…→ land in the new realm withrealm:admin— manual, requires Host-header switch (browser atacme.localhost:4300, Vite proxychangeOrigin: false). - ⏳ Cross-realm data isolation walked manually — integration-tested at the Marten layer; an E2E port would require driving two Host headers.
- ⏳ Outside the system realm
/admin/realms→ 404 (not 403) — covered byRealmsEndpointsTestsintegration test; not in the E2E suite. - ⏳ Edit realm domain list, immutable Slug — not yet verified.
- ⏳ Deactivate realm → realm domain returns 404 — not yet verified.
15. Auth log
- ✅ Endpoint works:
GET /api/admin/auth-log?page=1&pageSize=5returns events withLevel,Message,UserName,Ip,Timestamp. - ✅ Persisted
Messagefield renders placeholders inline now —Initial admin created. User="admin" IP="172.18.0.1" DemoData=False. F6 fixed (was: rawUser={UserName}template). - ⏳ Free-text search across actor / target / event type — not yet verified.
- ⏳ Date-range filter — not yet verified.
- ⏳ Failed-login burst is visible — events fired during this run but the column-level grouping wasn't validated.
- ⏳ GDPR-erased PII fields show
***ERASED***— not yet verified.
16. Settings
- ❌ Settings page renders only the "Wartung" (Maintenance) block (
Konsistenzprüfung,Projektionen neu aufbauen). The promisedAuthenticationMinimumLeveltoggle, branding fields, andMagicLinkSelfServicetoggle are not present in the UI — and there is no/api/admin/app-settingsendpoint either. See F18. - ⏳
/plattform/settingsonly opens forrealm:admin— not yet verified manually (no second-tier-perm user created in this run).
17. Recovery CLI (break-glass)
- ⏳
dotnet Modgud.Api.dll recover list— not yet verified (would need adocker execinto the running container; out of scope for the browser smoke run). - ⏳
recover reset-2fa <username>— not yet verified. - ⏳
recover set-email <username> <new@example.com>— not yet verified. - ⏳
recover magic-link <username>— not yet verified. - ⏳
recover rebuild-projections— not yet verified.
18. OAuth flows (real RP)
All items below need a separate Relying-Party (demo SPA + demo backend). The current run only exercised the IdP-side endpoints directly.
- ⏳ Authorization Code + PKCE end-to-end — not yet verified.
- ⏳ Reference token (default) — not yet verified end-to-end; reference-vs-JWT switch is unit-tested.
- ⏳ Switch the client to JWT, decode via jwt.io with the realm issuer — not yet verified.
- ⏳ Refresh-token exchange + rotation — not yet verified.
- ⏳ RP-initiated logout (
/connect/logout) withid_token_hint— not yet verified. - ⏳ Client credentials server-to-server — not yet verified.
- ⏳ Device code flow — not yet verified.
19. Token claims (Phase 4 — Keycloak resource_access)
Bearer-token issuance needs a real auth-code flow harness — see §18. The shape itself is unit-tested (ModgudClaimsTransformationTests, 12 tests; AuthorizationEndpointHelpersTests, 16 tests) and is left deferred for the manual run.
- ⏳ UserInfo response carries
resource_accesskeyed by app slug — not yet verified manually. - ⏳
resource_access[<app>].roleslists role names (not group names) — not yet verified manually. - ⏳ No
groupsclaim on/me(IDP/IAM split) — not yet verified manually; cookie/mewas inspected and only carriesPermissions: ["realm:admin"]plus user/MFA state. - ⏳
Modgud.Client.AspNetCoreflattens roles toClaimTypes.Role— not yet verified manually; unit-tested.
21. Permission gating + bypass tiers
- ✅ Default admin user has
Permissions: ["realm:admin"]per/api/account/me. Sees the full sidebar. Phase-1 model live and correct. - ⏳
app-admin-user(<app>:admin) sees only IAM admin, 403 onacme-tasks:*— not yet verified manually; integration-tested inPermissionResolutionTests. - ⏳
resource-admin-user(<app>:<resource>:admin) — not yet verified manually; integration-tested. - ⏳
read-only-user(<app>:<resource>:read) sees only the gated item — not yet verified manually; integration-tested. - ⏳ Sidebar visibility matches the backend gate (hidden item also returns 403 by URL) — not yet verified manually; the gating logic mirrors backend strings exactly via
auth.store.ts.
22. Multi-app scenarios
- ✅ App
acme-taskscreated with no resources, then queryable via/api/app/lookup— partially verified (the slug is registered, the rest of the cross-app permission scenarios are integration-tested inPermissionResolutionTestscases #5/#9/#10). - ⏳ Manual end-to-end: same user holds
user:readinmodgudANDtodo:writeinacme-taskssimultaneously — not yet verified. - ⏳ UserInfo's
resource_access[acme-tasks]returns thetodo:writegrant — not yet verified manually. - ⏳ UserInfo emits no
resource_accessblock for an unrelated app the user is not bound to — not yet verified manually. - ⏳ Same role on a group with
BoundTo: []contributes nothing — not yet verified manually.
23. Documentation sanity
- ✅ VitePress build clean (verified during the docs sweep before the run).
- ✅ In-app build clean.
- ✅ No "Access Scripts (ABAC)" page in the sidebar.
- ✅ Concepts → ABAC and the IAM boundary page renders.
- ✅ Concepts → Authorization (RBAC) page describes the 3-segment model + 3 bypass tiers.
- ✅ Admin → Groups page mentions the no-row-level-ABAC info box.
- ✅ No remaining German prose anywhere in the rendered docs.
24. Cross-cutting smoke
- ⏳ Two-browser SignalR live update — not yet verified.
- ⏳ Change-request created in browser A appears in browser B's admin list — not yet verified.
- ⏳ F5 doesn't re-prompt for login while cookie is valid — not yet verified manually.
- ⏳ No 4xx/5xx during normal admin navigation — partially: while clicking through the sidebar I saw 200s only; when I touched the OAuth list endpoints directly (without pagination params) I got the F16 400.
- ✅ Server log clean of unexpected stack traces during a regular session — except for the F3
ProjectionCoordinatorshutdown loop after a failed boot, which has no impact in a successful run.
Findings — what broke and what I know about it
F1: docker-compose.yml env vars bind nowhere
Severity: Shipping-blocker. Section: §0.
The committed docker-compose.yml references the auth service with the env vars
DATABASE_CONNECTIONSTRING
DATABASE_PASSWORD
AUTH_COOKIESECUREPOLICY
OPENIDDICT_ISSUER
OPENIDDICT_DEVELOPMENTMODE
SMTP_HOST / SMTP_PORT / SMTP_USESSL / SMTP_FROMADDRESS / SMTP_FROMNAMENone of those names match anything in the runtime. Cocoar.Configuration v5 binds env-vars by <section>__<property> (single underscores = literal underscore in the property name, double underscore = section boundary). The actually-bound names are
DBSETTINGS__CONNECTIONSTRING
OPENIDDICT__ISSUER
OPENIDDICT__DEVELOPMENTMODE
EMAIL__SMTP__HOST
EMAIL__SMTP__PORT
EMAIL__SMTP__USESSL
EMAIL__SMTP__FROMADDRESS
EMAIL__SMTP__FROMNAMEAUTH_COOKIESECUREPOLICY has no target property at all (AppSettings has no cookie-policy field; the cookie security policy is set unconditionally in Program.cs). DATABASE_PASSWORD has no target either — credentials live in the connection string.
Effect: anyone copy-pasting the compose file gets
System.ArgumentOutOfRangeException: Either an ConnectionString or DataSource
must be supplied (Parameter 'configure')
at Marten.StoreOptions.MultiTenantedDatabasesWithMasterDatabaseTable(...)right at boot. The Modgud.Infrastructure.Persistence.Marten.Configuration.MartenConfiguration.UseMasterTableMultiTenancy guard fires because the configured connection string is "".
Fix: rename the env vars in docker-compose.yml and update the docker-deployment guide page in the same commit. The guide page already shows the correct double-underscore form (website/operate/deployment.md), so the compose file is the only place that drifted.
Status: ✅ Fixed in this branch — docker-compose.yml rewritten with the correct <section>__<property> env vars and an APPURL override; live-verified by re-deploying the published image.
F2: Default AppUrl=https://0.0.0.0:443 blocks container startup without cert
Severity: Shipping-blocker. Section: §0.
StartUpConfiguration.AppUrl defaults to https://0.0.0.0:443 and Program.cs:690 does app.Run(conf.AppUrl). This binds Kestrel to HTTPS on 443 and ignores ASPNETCORE_URLS. The shipped Docker image does not contain a dev certificate, so without an explicit APPURL=http://0.0.0.0:80 (or APPURL=https://... plus a real cert mounted in) Kestrel throws
System.InvalidOperationException: Unable to configure HTTPS endpoint.
No server certificate was specified, and the default developer
certificate could not be found or is out of date.docker-compose.yml does not set APPURL. Combined with F1 this is why a fresh docker compose up on this branch never reaches a listening state.
Fix options:
- Make the container default
AppUrltohttp://0.0.0.0:80when no cert is configured (production typically terminates HTTPS at the reverse proxy anyway). - Or: add
APPURLto the compose file with a sane default and document the override. - Or: switch from
app.Run(conf.AppUrl)to a Kestrel config that honoursASPNETCORE_URLSso the standard ASP.NET Core override story works.
Status: ✅ Fixed in this branch — StartUpConfiguration.AppUrl default changed to http://0.0.0.0:80. HTTPS-direct setups now require an explicit override + cert, which matches how the prod Dockerfile actually expects to be deployed (TLS at the reverse proxy).
F3: ProjectionCoordinator BackgroundService keeps running after Hosting fails
Severity: Medium (cosmetic in a healthy run, noisy in failure logs). Section: §0.
Once Hosting throws on Kestrel start (see F2), the host transitions to "Application is shutting down" — but the Marten Events.Daemon.Coordination.ProjectionCoordinator BackgroundService keeps trying to discover databases on a now-disposed Npgsql.PoolingDataSource and emits
System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'Npgsql.PoolingDataSource'.
at Marten.Storage.MasterTableTenancy.BuildDatabases()
at Marten.Events.Daemon.Coordination.ProjectionCoordinator.executeAsync(...)every ~6 s until the process is killed. Doesn't affect a successful boot, but a failed boot ends up with two interleaved error streams in the logs.
Fix: wire the projection coordinator to honour the host's IHostApplicationLifetime.ApplicationStopping token, so it stops on shutdown instead of polling against the disposed data source.
F4: configuration.json excluded from publish
Severity: Informational. Section: §0.
The Modgud.Api.csproj deliberately excludes data/configuration.json from publish (the comment in the csproj explains why — to stop the dev-only file from silently overriding the class defaults in prod). That's correct behaviour, just call it out in the deployment guide so an operator who looks at the dev defaults in the repo doesn't expect them to apply in the container.
Status: ✅ Fixed in this branch — website/operate/deployment.md gets a :::warning box that explicitly says "production runs on env vars + class defaults, not on the committed configuration.json", with the actual implication: tweaks at deploy time go through env vars only.
F5: Sidebar shows raw i18n key admin.apps.title
Severity: Low. Section: §1.
Under the "SYSTEM" section in the admin sidebar, the Apps item renders the literal string admin.apps.title instead of a translation. Every other item is translated. The key is missing in both de.json and (presumably) the english bundle.
Status: ✅ Fixed in this branch — added admin.apps.title = "Anwendungen" to de.json. The en.json bundle is {} (whole UI not yet translated) and is tracked separately as F9.
F6: AuthLog persists raw Serilog message templates
Severity: Medium. Section: §1, §15.
/api/admin/auth-log returns rows like
{
"Message": "Login successful User={UserName}",
"UserName": "admin",
...
}The structured fields are correct; the rendered message template is not. The audit-log message column is therefore unreadable. The AuthLogPersistenceService (AuthLog/AuthLogService.cs) needs to either render the template before persisting, or the frontend grid needs to substitute the template tokens at render time using the structured fields.
Status: ✅ Fixed in this branch — AuthLogSink.Emit switched from logEvent.MessageTemplate.Text to logEvent.RenderMessage(), removing the manual placeholder-stripping blacklist. Verified live: the message column now reads Initial admin created. User="admin" IP="172.18.0.1" DemoData=False.
F7: Frontend AuthLog grid does not refresh after login
Severity: Low. Section: §1.
Right after /setup completes and the SPA navigates to /dashboard, opening Auth Log shows the bootstrap entries but not the just-fired Login successful event for the admin. The event is present in the DB (verified directly via the API). A page reload fetches the latest rows; the grid simply doesn't subscribe to a SignalR refresh on this view.
Status: ⚠️ Mitigated in this branch — AuthLogView.vue's poll cadence dropped from 10 s to 2 s, so the worst case wait between event-fires-and-grid-shows-it is now ~2 s. A proper SignalR push (the UIHub already has the plumbing) is the right long-term fix and stays open.
F8: AuthLog doc promises columns the UI does not render
Severity: Low (doc drift). Section: §15.
website/admin/auth-log.md lists the columns as Timestamp / Event type / Actor / Target / IP address / Outcome. The actual UI columns are Zeit / Level / Ereignis / Benutzer / IP-Adresse — no Target, no Outcome, and Ereignis carries the half-rendered Serilog template (F6). Either the doc reflects an intended-future shape or the implementation is incomplete; pick one.
F9: UI still renders German strings while the rest of the product is English-only
Severity: Medium (consistency). Section: §1, §2, all admin screens.
The product strategy after Wave 8 is English-only docs. The SPA's i18n is still German by default (de.json), with a "DE" toggle in the header. Strings observed: Benutzer, Rollen, Gruppen, Einstellungen, Auth Log, Änderungsanfragen, Aktualisieren, Erstellen, Benutzername, Vorname, Nachname, Kürzel, Aktiv, IDENTITÄTSQUELLEN, AUTORISIERUNG. Either ship the English bundle and default to en, or update the strategy doc to say "docs English-only, UI bilingual".
F10: Checklist step points at dev-only endpoint not reachable in prod container
Severity: Low. Section: §2, §3, §4.
Several steps say "check the email at /api/dev/emails". The endpoint exists (Modgud.Api/Features/Dev/DevEndpoints.cs) but is gated by IsDevelopment() and so unreachable in the ASPNETCORE_ENVIRONMENT=Production container we ship. The smoke checklist therefore can't end-to-end-test the magic-link / email-OTP / change-request-verification / GDPR-confirmation flows in the prod constellation. Either set up an InMemory-mode email capture for the prod image, run the smoke against an ASPNETCORE_ENVIRONMENT=Development container, or mark the dev-only steps explicitly in the checklist.
Status: ✅ Fixed in this branch — Phase A of the Playwright port runs a real Mailpit container next to the auth API on the same docker network, points EMAIL__SMTP__HOST at it, and reads back via Mailpit's REST API on port 8025. The auth container itself stays ASPNETCORE_ENVIRONMENT=Production and ships no inspection endpoint. The dev-only /api/dev/emails route was deleted alongside the dev-mode InMemoryEmailService runtime registration; the class itself stays, used only as the in-process integration-test substitute via ModgudWebApplicationFactory's DI override. Magic-link end-to-end now lives in 00-smoke.spec.ts and the change-request flow in 30-profile.spec.ts.
F8: AuthLog doc promised columns the UI does not render
Status: ✅ Fixed in this branch — website/admin/auth-log.md rewritten to document the columns the grid actually renders (Timestamp / Level / Event / User / IP) and to note that the "Date range / Event type / Outcome" filters mentioned in the old doc aren't shipped yet (linked to this finding).
F11: i18n loader fetches /i18n/de-AT.json and takes a 404
Severity: Low. Section: §2.
A browser configured for de-AT (Austrian German — the most common locale in this codebase's home audience) triggers a GET /i18n/de-AT.json that 404s before the loader falls back to de.json. The fallback works, but every Austrian admin sees a red 404 in DevTools on every page load. Either accept regional aliases server-side or strip the country suffix client-side before fetching.
Status: ✅ Fixed in this branch — main.ts strips the country suffix before the i18n fetch (de-AT → de), so regional locales land on the base bundle on the first request.
F12: Login form fields don't clear on fill via DevTools MCP
Severity: Low (test ergonomics). Section: §2.
When DevTools-MCP fill_form writes into a CoarTextInput that already has a value, the new text is appended rather than replacing. On the user-facing flow it doesn't matter (a real user clicks into an empty field), but on automated walks the form ends up with adminadmin instead of admin. Possibly missing select-on-focus on the text input, possibly DevTools MCP behaviour. Either way it would help if the input cleared when filled programmatically.
F13: (intentionally skipped — placeholder in case I missed renumbering)
F14: (intentionally skipped — see F13)
F15: (intentionally skipped — see F13)
F16: /oauth/clients and /oauth/apis list endpoints require page+pageSize, no default
Severity: Shipping-blocker for SDK consumers. Section: §10, §12.
group.MapGet("", async (
OAuthAdminService svc,
int page, // ← required
int pageSize, // ← required
CancellationToken ct) => { ... });ASP.NET Core MinimalAPI binds non-nullable primitive query params as required. Calling GET /api/admin/oauth/clients (no query string) returns 400 Bad Request with an empty body — no model-validation detail, just 400. The handler then would have called PaginationRequest.WithDefaults(page, pageSize) to clamp 0 → defaults, but the model binder rejects before the handler runs. Same bug on OAuthApisEndpoints.
The frontend works because AG-Grid sends pagination params, but a direct curl / SDK / HTTP-browser-tool / Swagger-UI call breaks. The unit tests in OAuthAdminMappingTests already cover WithDefaults(0, 0) clamping the right way, so the helper is correct — the binding signature is the bug.
OAuthScopesEndpoints does NOT have this bug (returns 200 with defaults) and is the working reference. Replicate that signature (probably int? page = null, int? pageSize = null plus a null-aware WithDefaults overload).
Status: ✅ Fixed in this branch — both endpoints now declare int? page = null, int? pageSize = null and call PaginationRequest.WithDefaults(page ?? 0, pageSize ?? 0). Existing unit tests in OAuthAdminMappingTests already cover the clamp behaviour. Live-verified: GET /api/admin/oauth/clients and /api/admin/oauth/apis both return {Items: [], TotalCount: 0} with 200 even without query params.
F17: (intentionally skipped — see F13)
F18: Settings UI and API promised but not shipped
Severity: Medium. Section: §16.
/plattform/settings renders only a "Wartung" (Maintenance) block with two buttons (Konsistenzprüfung + Projektionen neu aufbauen). The checklist promises an AuthenticationMinimumLevel toggle, branding fields, and a MagicLinkSelfService toggle. None of those are present, and there is no /api/admin/app-settings endpoint registered either (despite the integration-test file OwaspTop10Tests.A01_AdminEndpoints_Require_Authentication testing it — that test passed because the endpoint returns 401 because it returns 404 → 401 cascade; needs cross-checking). Either build the app-settings UI + endpoint or prune the checklist + remove the doc references to a non-existent surface.
Summary
- 9 sections fully or substantially exercised: §0 through §2, §6, §7, §8 (group create), §9, §13–§15, §20 (negative path), §23.
- 6 sections with at least one positive verification but pending click-through: §3 (grace-period seen), §10–§12 (UI works via AG-Grid even though the API direct hit is broken), §22 (acme-tasks registered).
- 9 sections entirely deferred: §4, §5, §11 (custom scope), §17, §18, §19, §21 (manual user-tier setup), §24.
- 18 findings logged, of which F1, F2 and F16 are the shipping-blockers; F6, F9, F18 are operationally important; the rest are polish.
Status of the running container at the end of this run: docker rm -f modgud-test to remove. Postgres on cocoar-postgres left running (other repos depend on it).
Fixes landed in this branch (post-run)
The following findings were addressed in the same branch as the manual run, so a re-run starts from a cleaner baseline:
| # | Finding | Fix |
|---|---|---|
| F1 | docker-compose env vars bind nowhere | docker-compose.yml rewritten with the correct <section>__<property> env vars. |
| F2 | Default AppUrl=https://0.0.0.0:443 blocks cert-less startup | StartUpConfiguration.AppUrl default changed to http://0.0.0.0:80. |
| F5 | Sidebar shows raw admin.apps.title | Added admin.apps.title = "Anwendungen" to de.json. |
| F6 | AuthLog persists raw Serilog templates | AuthLogSink.Emit now uses logEvent.RenderMessage() and drops the manual placeholder-stripping blacklist. |
| F8 | AuthLog doc claimed columns the UI does not render | website/admin/auth-log.md rewritten to match what's shipped, with the missing filters explicitly tagged as future work. |
| F11 | Browser locale de-AT 404s on i18n bundle | main.ts strips the country suffix before the fetch (de-AT → de). |
| F16 | OAuth list endpoints required page + pageSize (400 without them) | Both OAuthClientsEndpoints and OAuthApisEndpoints now declare the params as int? = null and clamp to defaults via WithDefaults. |
| F4 | configuration.json publish-exclusion documentation | website/operate/deployment.md gains a :::warning callout explaining production runs on env vars + class defaults, not the committed file. |
| F7 | Frontend AuthLog grid not refreshing after login | Mitigated — poll cadence dropped from 10 s to 2 s. SignalR push remains the proper long-term fix. |
| F10 | Dev-only /api/dev/emails endpoint unreachable in prod container | Replaced with a real Mailpit container in the E2E rig; /api/dev/emails deleted from the runtime image entirely. |
Still open (not addressed in this branch): F3, F9, F12, F18.