Unit suites already passed individually; this is the full solution together plus what none of them could check alone: two real hosts talking to each other, a slave API key encrypted by one process and decrypted by the next one after a restart, and headers landing on actual static files instead of a simulated response.
6.0 KiB
Integration Test Instructions
Purpose
Verify everything that unit tests structurally cannot: real HTTP responses from a running host,
real static-file serving, real cross-process master/slave communication, and real Data Protection
key-ring behaviour. Every scenario below was explicitly flagged as "carried to phase-level Build and
Test" by U1, U2 or U3, because DefaultHttpContext and in-memory test doubles cannot exercise them
(e.g. Response.OnStarting is a no-op outside a real Kestrel pipeline, and a unit test cannot start
two real processes and let them talk to each other).
Setup Integration Test Environment
1. Start a database
podman start sql-server
# or, if it doesn't exist yet: see README.md § "Database opstarten"
2. Populate the static content mounts (optional but recommended)
Without this, the SPA-fallback and placeholder scenarios still work, but the "real static asset" scenarios need actual files:
dotnet publish src/SlpModularCms.Api -c Release -o ./tmp-publish-verify
This also copies the built admin SPA into src/SlpModularCms.Api/wwwroot/admin/ (via
BuildAndCopyAdminFrontend). Create a placeholder wwwroot/web/index.html by hand to exercise the
"real website content" scenarios, or leave it absent to exercise the placeholder scenario.
wwwroot/ is gitignored and must be deleted after testing — it is generated content, not part
of the repository.
3. Start both hosts
dotnet run --project src/SlpModularCms.Api --launch-profile https # port 7221
dotnet run --project src/SlpModularCms.Api.Slave --launch-profile https # port 7222
Start them one at a time, waiting for the first to finish building — starting both
simultaneously races on the shared SlpModularCms.Core build output (see build-instructions.md
troubleshooting).
Run Integration Tests
Scenario 1: Static content, SPA fallback, and header scoping (U1, U3)
| Request | Expected | Verified 2026-07-28 |
|---|---|---|
GET / with no wwwroot/web/ |
200, embedded placeholder HTML, Relaxed CSP |
✅ |
GET / with wwwroot/web/index.html present |
200, real content, Relaxed CSP |
✅ |
GET /admin |
301 → /admin/ |
✅ |
GET /admin/ |
200, admin index.html, Strict CSP (DENY, no unsafe-inline in script-src) |
✅ |
GET /admin/dashboard (client route, no extension) |
200, falls back to admin index.html |
✅ |
GET /admin/assets/{real-file}.js |
200, real file served, Content-Type: text/javascript, X-Content-Type-Options present, no CSP/X-Frame-Options/Referrer-Policy (non-HTML — FU1 = A scoping) |
✅ |
GET /admin/assets/does-not-exist.js |
404 (missing file with an extension does not fall back to HTML) | ✅ |
GET /a-website-route-with-no-extension |
200, falls back to website index.html, Relaxed CSP |
✅ |
GET /health |
200, application/json, X-Content-Type-Options present, no CSP/X-Frame-Options/Referrer-Policy (non-HTML) |
✅ |
Note on HSTS: not observed on any response above — correct, since sendHsts is deliberately
!IsDevelopment() and all of the above ran with ASPNETCORE_ENVIRONMENT=Development. Re-verify with
ASPNETCORE_ENVIRONMENT=Production before a real release if this has not already been proven in a
non-Development environment.
Scenario 2: Master ↔ Slave real communication over the persistent key ring (U2, Master module)
| Check | Expected | Verified 2026-07-28 |
|---|---|---|
Master's IntegrityCheckBackgroundService reaches a previously-registered slave |
GET https://localhost:7222/api/v1/master/registered-url → 200 |
✅ |
| Master successfully authenticates with a stored, encrypted slave API key from a previous process's key ring | POST https://localhost:7222/api/v1/master/status → 200 (not a decryption failure) |
✅ |
This is the strongest available local proof that U2's persistent, database-backed key ring
(PersistKeysToDbContext with an explicit discriminator) actually survives a process restart —
the stored API key was encrypted by an earlier process and successfully decrypted by this one.
Scenario 3: Correlation ID in structured logging (U4, OPEN-01)
| Check | Expected | Verified 2026-07-28 |
|---|---|---|
| A real request's log entries | Carry a TraceId: (W3C trace id) in the log scope |
✅ (e.g. TraceId:5a302c33b4f0fc8fb9d0b35f840934b8 on a request to /api/v1/System/capabilities) |
Scenario 4: Startup behaviour (U1, U2)
| Check | Expected | Verified 2026-07-28 |
|---|---|---|
| Both hosts start with a fresh/existing database | MigrateCoreDatabase() logs "Core database migrations applied successfully", no fail-fast |
✅ |
Missing wwwroot/admin/ and wwwroot/web/ at startup |
Warning logged per directory, host still starts and serves /api/v1 |
✅ (observed on the very first run, before publishing) |
Not Re-Verified at This Stage (out of scope, per the units' own Definition of Done)
- 503 response from the availability gate carrying security headers (U3's own carried item) —
covered by unit tests using a simulated
HttpContext; a fully authenticated real-host repeat would need a bootstrapped Owner login flow that this repository's README does not yet document (a pre-existing gap, unrelated to this feature). Indirect evidence is strong: every response observed above — 200s and 404s alike — carriedX-Content-Type-Options, confirming theOnStartinghook fires uniformly regardless of status code - Real Sentry event with the
security_eventtag, tunnel status codes, threshold behaviour — needs a configured Sentry DSN; deferred to Operations (Monitoring Setup) - UptimeRobot, real Gitea pipeline run, actual Pi/SSH/database backup — deferred to Operations per U5/U6's own Definition of Done
Cleanup
# Stop both dotnet run processes
Remove-Item -Recurse -Force src/SlpModularCms.Api/wwwroot, tmp-publish-verify
wwwroot/ must not be committed — verify git status is clean afterwards.