Closes construction - build and test across all seven units

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.
This commit is contained in:
2026-07-28 16:47:45 +02:00
parent 73126f5a9c
commit 9ab30fe2a6
8 changed files with 285 additions and 6 deletions
@@ -0,0 +1,72 @@
# Build and Test Summary
**Date**: 2026-07-28
## Build Status
- **Build Tool**: .NET 10 SDK (`dotnet build`), pnpm 9 / Vite (`pnpm run build`)
- **Build Status**: Success — `dotnet build SlpModularCms.sln -c Release`, 0 errors, 12 warnings
(all pre-existing `NU1510` "will not be pruned" informational notices)
- **Build Artifacts**: standard per-project `bin/Release/net10.0/` output; `frontend/dist/`
- **Build Time**: ~15s backend, ~1s frontend (incremental)
## Test Execution Summary
### Unit Tests
- **Total Tests**: 609 (372 backend + 237 frontend)
- **Passed**: 609
- **Failed**: 0
- **Coverage**: not tracked (D-10 — deliberate, no threshold enforced)
- **Status**: Pass
### Integration Tests
- **Test Scenarios**: 4 (static content/SPA-fallback/header-scoping, master↔slave real
communication over the persistent key ring, correlation-ID logging, startup behaviour) — see
`integration-test-instructions.md` for the full scenario table
- **Passed**: 4/4 scenarios, all individual checks within them passed
- **Failed**: 0
- **Status**: Pass
- **Method**: both hosts (`SlpModularCms.Api`, `SlpModularCms.Api.Slave`) run live against the
local SQL Server container, admin SPA published and served for real, verified with direct HTTP
requests rather than simulated `HttpContext` — the specific gap unit tests structurally cannot
close, per U1/U2/U3's own carried-forward Definition of Done items
### Performance Tests
- **Status**: N/A — no performance/throughput requirements exist for this feature (see
`performance-test-instructions.md`)
### Additional Tests
- **Contract Tests**: N/A — no microservice boundary changed by this feature; the U5↔U6
`workflow_call` interface is validated by YAML structure checks during Code Generation, not a
runtime contract test
- **Security Tests**: Vulnerability scan — `dotnet list SlpModularCms.sln package --vulnerable
--include-transitive` — **0 vulnerable packages across all 10 projects** (was 2 packages, multiple
advisories each, closed at U5 — FR-22, OPEN-03)
- **E2E Tests**: covered by the integration scenarios above (real requests through a real host); no
browser-driven UI E2E suite exists for the admin SPA in this repository
## Overall Status
- **Build**: Success
- **All Tests**: Pass
- **Ready for Operations**: Yes
## What This Stage Confirmed That Per-Unit Checks Could Not
- Static-file serving and SPA-fallback actually work end-to-end with real published output, not
just against `DefaultHttpContext` (U1)
- HTTP security headers are correctly scoped by content type on **real** responses — HTML gets the
full set, non-HTML gets only the always-on pair, confirmed on genuine static assets and the
`/health` JSON response (U3)
- The persistent Data Protection key ring genuinely survives a process restart: a slave API key
encrypted by an earlier process was successfully decrypted and used for authenticated master↔slave
calls by a freshly started process (U2, closing the loop on ASM-08)
- The W3C trace id correlation mechanism is present in real console log output (U4, OPEN-01)
## What Remains Genuinely Out of Scope for This Stage
Real Sentry events, real UptimeRobot monitors, an actual Gitea pipeline run against the self-hosted
runner, and a real Raspberry Pi deploy — all explicitly deferred to the Operations phase by U5 and
U6's own Definition of Done, because they require infrastructure (a live Sentry project, a live
Gitea instance, the actual Pi with SSH access) that does not exist yet at this stage of the workflow.
## Next Steps
Ready to proceed to the Operations phase (Deployment Setup → Monitoring Setup → Production Readiness
Validation), per this feature's `## Operations Configuration: Include Operations Phase: Yes`
(decided at Requirements Analysis).
@@ -0,0 +1,43 @@
# Build Instructions
## Prerequisites
- **Build Tool**: .NET 10 SDK, Node.js 20+, pnpm 9+
- **Dependencies**: NuGet packages restore automatically on build; frontend needs `pnpm install`
- **Environment Variables**: none required for a plain build/test pass. `appsettings.local.json` (backend) and `.env.local` (frontend) are only needed to actually *run* a host against a real database — see `README.md`
- **System Requirements**: any OS .NET 10 and Node 20 support. A SQL Server instance (e.g. via Podman/Docker, per `README.md` § "Database opstarten") is only needed to run a host, not to build or run the unit-test suites
## Build Steps
### 1. Install Dependencies
```powershell
dotnet restore SlpModularCms.sln
cd frontend
pnpm install --frozen-lockfile
```
### 2. Configure Environment
No environment configuration is needed for build or unit tests. `dotnet build`/`dotnet test` do not require a database connection.
### 3. Build All Units
```powershell
dotnet build SlpModularCms.sln -c Release
```
### 4. Verify Build Success
- **Expected Output**: `Build succeeded. 0 Error(s).`
- **Build Artifacts**: per-project `bin/Release/net10.0/` output
- **Common Warnings**: `NU1510` ("will not be pruned") on `Microsoft.Extensions.*.Abstractions` and `System.Security.Cryptography.Xml` — pre-existing, informational, not a build defect. No `NU1903` (vulnerability) warnings should appear; if one does, a package has drifted below its pinned patched version (see FR-22 in `requirements.md`)
## Troubleshooting
### Build Fails with Dependency Errors
- **Cause**: stale NuGet cache after a package pin change
- **Solution**: `dotnet nuget locals all --clear` then `dotnet restore SlpModularCms.sln`
### Build Fails with Compilation Errors
- **Cause**: none expected at this stage — all 7 units were individually build-verified during Code Generation (Step 13.5)
- **Solution**: if one appears, it indicates a cross-unit conflict (e.g. two units editing the same file inconsistently) that per-unit builds could not catch; check `git log --oneline` for the unit commits and diff the affected file
### Running Two Hosts Concurrently Locks the Shared Core Build
- **Cause**: `SlpModularCms.Api` and `SlpModularCms.Api.Slave` both reference `SlpModularCms.Core`. Starting `dotnet run` for both at the same moment races on `SlpModularCms.Core`'s build output directory
- **Solution**: start one host, wait for it to finish building, then start the second — this only affects local `dotnet run`, not `dotnet build`/`dotnet test` on the full solution
@@ -0,0 +1,101 @@
# 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.
@@ -0,0 +1,14 @@
# Performance Test Instructions
## Status: N/A for this feature
`requirements.md` § 56 (Non-Functional Requirements) defines no throughput, latency or concurrent-user
targets for this feature — it is CI/CD and deployment tooling, not a user-facing capacity change.
The one performance-adjacent decision made (`TracesSampleRate: 0.1` for Sentry, `TunnelMaxPayloadBytes:
204800`) is a cost/quota control for observability, not a system performance requirement, and was
already verified during U4's code generation.
If load/throughput requirements are identified later (e.g. for the deploy pipeline's own duration, or
for the public website once real content is deployed into `wwwroot/web/`), they belong to a
follow-up feature — this deployment-workflow feature does not change the CMS's own runtime capacity
characteristics.
@@ -0,0 +1,29 @@
# Unit Test Execution
## Run Unit Tests
### 1. Execute All Unit Tests
```powershell
dotnet test SlpModularCms.sln -c Release
cd frontend
pnpm run test
```
### 2. Review Test Results
- **Expected — backend**: 372 tests pass, 0 failures (Core 196, Availability 82, Master 57, Identity 37). `SlpModularCms.Api` and `SlpModularCms.Api.Slave` have no test project by convention (CLAUDE.md) — their behaviour is covered by this stage's live integration verification instead
- **Expected — frontend**: 237 tests pass, 0 failures, across 38 test files
- **Test Coverage**: no coverage threshold is enforced (D-10 — deliberate decision, not an oversight)
- **Test Report Location**: console output; `dotnet test` and `vitest run` both exit non-zero on any failure, which is what the CI gates (U5) act on
### 3. Fix Failing Tests
If tests fail:
1. Identify which project/file failed from the console output
2. Since every unit was already build-and-test-verified individually during Code Generation, a failure here means a **cross-unit** interaction broke something that passed in isolation — check what changed in the units involved
3. Fix and rerun until green
## Result at This Stage (2026-07-28)
- Backend: **372 passed, 0 failed**
- Frontend: **237 passed, 0 failed**
- Both counts are unchanged from the Round 2 and Round 3 per-unit baselines — confirms the full suite passes together, not just in isolation