# 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 ```powershell 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: ```powershell 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 ```powershell 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 — carried `X-Content-Type-Options`, confirming the `OnStarting` hook fires uniformly regardless of status code - **Real Sentry event with the `security_event` tag, 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 ```powershell # 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.