From 13e706277f40cc0aa079ef65ee3f8d351fbab6e7 Mon Sep 17 00:00:00 2001 From: Patrick Plate Date: Wed, 24 Jun 2026 14:25:56 +0200 Subject: [PATCH] plan: add Sprint-0-Testplan.md (43 test cases: unit + integration + E2E + security + perf) --- Sprint-0-Testplan.md | 477 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 477 insertions(+) create mode 100644 Sprint-0-Testplan.md diff --git a/Sprint-0-Testplan.md b/Sprint-0-Testplan.md new file mode 100644 index 0000000..f87d23c --- /dev/null +++ b/Sprint-0-Testplan.md @@ -0,0 +1,477 @@ +# Sprint 0 — Test Plan + +**Status:** Draft v1 +**Date:** 2026-06-24 +**Owner:** Patrick +**Scope:** Validates Sprint 0 deliverable: `de.platesoft:plate-auth-starter:0.1.0` + `@platesoft/auth:0.1.0` +**Basis:** [Sprint-0-Plan.md](Sprint-0-Plan.md) + +--- + +## 1. Reading guide + +This test plan enumerates every test case (T-IDs) needed to validate the Sprint 0 carve-out. It does **not** re-test the InspectFlow product surface — the existing InspectFlow E2E suite serves as our regression net (see § 5). + +Each test case has: + +- **ID** (T-UTxx unit / T-ITxx integration / T-FExx frontend / T-E2Exx end-to-end / T-SECxx security / T-PERFxx perf) +- **Type** (Unit / Integration / Frontend-Unit / E2E / Security / Performance) +- **Class / spec file** +- **Scenarios** (Given / When / Then) +- **Expected result** +- **Acceptance criterion mapped** (A1..A8 from [Sprint-0-Plan.md § 10.5](Sprint-0-Plan.md)) + +Status legend: ⬜ Open · 🟡 In progress · ✅ Passed · ❌ Failed · ⏭️ Skipped + +--- + +## 2. Test overview (master table) + +| ID | Type | Class / Spec | Maps to | Status | +|----|------|--------------|---------|--------| +| T-UT01 | Unit | `JwtServiceTest` | A4 | ⬜ | +| T-UT02 | Unit | `JwtServiceTest` (refresh) | A4 | ⬜ | +| T-UT03 | Unit | `JwtServiceTest` (invalid token) | A4 | ⬜ | +| T-UT04 | Unit | `ExchangeServiceTest` (mint) | A2, A4 | ⬜ | +| T-UT05 | Unit | `ExchangeServiceTest` (consume happy) | A2, A4 | ⬜ | +| T-UT06 | Unit | `ExchangeServiceTest` (nonce replay) | A2 | ⬜ | +| T-UT07 | Unit | `ExchangeServiceTest` (HMAC tamper) | A2 | ⬜ | +| T-UT08 | Unit | `ExchangeServiceTest` (clock skew) | A2 | ⬜ | +| T-UT09 | Unit | `HmacEnvelopeTest` | A2 | ⬜ | +| T-UT10 | Unit | `MembershipServiceTest` (rank) | A1 | ⬜ | +| T-UT11 | Unit | `MembershipServiceTest` (polymorphic FK validation) | A1, A5 | ⬜ | +| T-UT12 | Unit | `InvitationServiceTest` | A1 | ⬜ | +| T-UT13 | Unit | `AccessRequestServiceTest` | A1 | ⬜ | +| T-UT14 | Unit | `PlateAuthPropertiesValidationTest` | A4 | ⬜ | +| T-UT15 | Unit | `OrgContextResolverTest` (SPI fallback) | A5 | ⬜ | +| T-IT01 | Integration | `PlateAuthFlywayMigrationIT` | A3 | ⬜ | +| T-IT02 | Integration | `AuthBootstrapIT` (auto-config wiring) | A1, A4 | ⬜ | +| T-IT03 | Integration | `ExchangeFlowIT` (sign-in → mint → consume) | A2 | ⬜ | +| T-IT04 | Integration | `JwtAuthenticationFilterIT` | A1 | ⬜ | +| T-IT05 | Integration | `MembershipRepositoryIT` | A1 | ⬜ | +| T-IT06 | Integration | `InvitationFlowIT` | A1 | ⬜ | +| T-IT07 | Integration | `AccessRequestFlowIT` | A1 | ⬜ | +| T-IT08 | Integration | `LoginEventAuditIT` | A1 | ⬜ | +| T-IT09 | Integration | `ProviderSpiSwapIT` (default vs custom `OrgValidator`) | A5 | ⬜ | +| T-FE01 | Frontend-Unit | `createAuthConfig.test.ts` | A6 | ⬜ | +| T-FE02 | Frontend-Unit | `hmac-edge.test.ts` (Web Crypto sign + verify) | A2, A6 | ⬜ | +| T-FE03 | Frontend-Unit | `proxy-headers.test.ts` (hop-by-hop strip) | A6 | ⬜ | +| T-FE04 | Frontend-Unit | `proxy-handler.test.ts` (auth() → fetch) | A6 | ⬜ | +| T-FE05 | Frontend-Unit | `package-exports.test.ts` (conditional exports) | A6 | ⬜ | +| T-E2E01 | E2E | InspectFlow `e2e/auth-flow.unauth.spec.ts` | A7 | ⬜ | +| T-E2E02 | E2E | InspectFlow Google sign-in scenario | A7 | ⬜ | +| T-E2E03 | E2E | InspectFlow password login scenario | A7 | ⬜ | +| T-E2E04 | E2E | InspectFlow invitation accept scenario | A7 | ⬜ | +| T-E2E05 | E2E | InspectFlow access-request approve scenario | A7 | ⬜ | +| T-E2E06 | E2E | InspectFlow admin audit endpoint visibility | A7 | ⬜ | +| T-SEC01 | Security | HMAC tamper rejected (envelope mutated) | A2 | ⬜ | +| T-SEC02 | Security | Nonce replay rejected within TTL | A2 | ⬜ | +| T-SEC03 | Security | Envelope rejected after max-age | A2 | ⬜ | +| T-SEC04 | Security | Expired JWT rejected | A4 | ⬜ | +| T-SEC05 | Security | Missing JWT secret fails startup | A4 | ⬜ | +| T-SEC06 | Security | Short JWT secret (<32 chars) fails startup | A4 | ⬜ | +| T-SEC07 | Security | CORS unknown origin rejected | A4 | ⬜ | +| T-SEC08 | Security | SQL injection probe on `/auth/login` rejected | A1 | ⬜ | +| T-SEC09 | Security | Constant-time HMAC compare (no timing oracle) | A2 | ⬜ | +| T-SEC10 | Security | Refresh-token rotation: old refresh invalidated | A4 | ⬜ | +| T-PERF01 | Performance | `/auth/exchange/consume` p95 < 50ms | A8 | ⬜ | +| T-PERF02 | Performance | `/auth/login` p95 < 300ms (incl. bcrypt) | A8 | ⬜ | +| T-PERF03 | Performance | JWT filter overhead per request p95 < 5ms | A8 | ⬜ | + +**Total:** 43 test cases — 15 Unit, 9 Integration, 5 Frontend-Unit, 6 E2E, 10 Security, 3 Performance. + +--- + +## 3. Unit tests (backend) + +### T-UT01 — JwtService generates valid access token + +- **Class:** `de.platesoft.auth.service.JwtServiceTest` +- **Method:** `generateAccessToken_validInputs_returnsParseableToken()` +- **Given:** `JwtService` configured with HMAC secret ≥ 32 chars, 15min expiration, issuer `plate-auth`. +- **When:** `generateAccessToken(userId=UUID, email="a@b.de", role="USER")`. +- **Then:** Returned token parses with `jjwt`, contains claims `sub`, `email`, `role`, `iss=plate-auth`, `exp ≈ now + 15min` (±2s). + +### T-UT02 — JwtService generates refresh token with longer expiration + +- **Method:** `generateRefreshToken_validInputs_hasLongerExpiration()` +- **Given:** Same config. +- **When:** `generateRefreshToken(userId)` called. +- **Then:** Token has `exp ≈ now + 30 days`, different `jti` than access token, claim `type="refresh"`. + +### T-UT03 — JwtService rejects invalid token + +- **Method:** `isTokenValid_tamperedToken_returnsFalse()` +- **Given:** Valid token, then last 5 chars replaced with random. +- **When:** `isTokenValid(tampered)`. +- **Then:** Returns `false`. No exception leaks out (caught internally and logged at DEBUG). + +### T-UT04 — ExchangeService mints envelope + +- **Class:** `de.platesoft.auth.service.ExchangeServiceTest` +- **Method:** `mint_validUser_returnsSignedEnvelope()` +- **Given:** Exchange secret ≥ 32 chars, nonce-ttl=5min. +- **When:** `mint(userId, email, role, orgContext)`. +- **Then:** Returns envelope JSON with fields `nonce` (UUID format), `iat` (epoch seconds), `userId`, `email`, `role`, `orgContext`, `sig` (Base64 SHA-256 HMAC over canonical concat). HMAC verifies against the secret. + +### T-UT05 — ExchangeService consumes envelope happy path + +- **Method:** `consume_validFreshEnvelope_returnsTokens()` +- **When:** `consume(envelope)` within `max-age` window. +- **Then:** Returns `TokenResponse(accessToken, refreshToken)`. Nonce is now in the consumed-set. + +### T-UT06 — ExchangeService rejects nonce replay + +- **Method:** `consume_replayedNonce_throws()` +- **Given:** Envelope already consumed once. +- **When:** `consume(sameEnvelope)` called again. +- **Then:** Throws `ExchangeReplayException` (HTTP-mappable to 409 Conflict). Audit event emitted. + +### T-UT07 — ExchangeService rejects HMAC tamper + +- **Method:** `consume_tamperedField_throws()` +- **Given:** Envelope with `role` changed from `USER` to `ADMIN` post-signing. +- **When:** `consume(tampered)`. +- **Then:** Throws `ExchangeHmacInvalidException`. Audit event `EXCHANGE_HMAC_FAILED` emitted. + +### T-UT08 — ExchangeService rejects clock skew beyond max-age + +- **Method:** `consume_envelopeBeyondMaxAge_throws()` +- **Given:** Envelope `iat` set to `now - 70s` (max-age=60s). +- **When:** `consume(...)`. +- **Then:** Throws `ExchangeExpiredException`. + +### T-UT09 — HmacEnvelope sign/verify symmetry + +- **Class:** `de.platesoft.auth.crypto.HmacEnvelopeTest` +- **Method:** `signThenVerify_sameSecret_succeeds()` + `verify_differentSecret_fails()` +- **Then:** Round-trip succeeds; wrong secret returns `false`. Compare uses `MessageDigest.isEqual(byte[], byte[])` (constant-time). + +### T-UT10 — MembershipService computes effective rank + +- **Class:** `de.platesoft.auth.service.MembershipServiceTest` +- **Method:** `effectiveRole_userWithMultipleMemberships_returnsHighest()` +- **Given:** User has membership `USER` in Org A, `ADMIN` in Org B. +- **When:** `effectiveRole(userId, orgId=B)`. +- **Then:** Returns `ADMIN`. Helper `effectiveRole(userId)` (no org) returns `ADMIN` (max). + +### T-UT11 — MembershipService rejects invalid (org_type, org_id) via SPI + +- **Method:** `addMembership_orgValidatorRejects_throws()` +- **Given:** Test `OrgValidator` SPI implementation that returns `false` for unknown org IDs. +- **When:** Adding a membership with `(org_type="UNKNOWN", org_id=42)`. +- **Then:** Throws `OrgValidationException`. No row inserted. + +### T-UT12 — InvitationService creates invitation with hashed token + +- **Class:** `de.platesoft.auth.service.InvitationServiceTest` +- **Method:** `create_validInput_storesHashedTokenOnly()` +- **Then:** DB row has bcrypt or SHA-256 hash, **not** the plaintext token. Plaintext is returned to caller exactly once. Expiration set to `now + 7 days`. + +### T-UT13 — AccessRequestService transitions states + +- **Class:** `de.platesoft.auth.service.AccessRequestServiceTest` +- **Method:** `approve_pendingRequest_createsMembership()` +- **Then:** Request status → `APPROVED`, Membership row created with requested role, `AccessRequestMailer` SPI invoked. + +### T-UT14 — PlateAuthProperties bean validation + +- **Class:** `de.platesoft.auth.config.PlateAuthPropertiesValidationTest` +- **Scenarios (parameterized):** + - JWT secret 31 chars → fail (`@Size(min=32)`) + - Exchange secret null → fail (`@NotBlank`) + - `cors.allowed-origins` malformed URL → fail + - All valid → pass +- **Then:** ApplicationContext fails fast with `BindValidationException` referencing the invalid property path. + +### T-UT15 — OrgContextResolver falls back when SPI absent + +- **Class:** `de.platesoft.auth.spi.OrgContextResolverTest` +- **Given:** No user-provided `OrgValidator` bean; default `PermissiveOrgValidator` in effect. +- **When:** Resolving any `(org_type, org_id)`. +- **Then:** Returns `true` (default-accept), logs a WARN once on startup that no `OrgValidator` is configured. + +--- + +## 4. Integration tests (backend, Testcontainers) + +Strategy: each integration test boots Spring with `@SpringBootTest(classes = PlateAuthAutoConfiguration.class)` plus a minimal test JPA entity-scan and uses a `PostgreSQLContainer` (Testcontainers). + +### T-IT01 — Flyway migrations apply cleanly + +- **Class:** `de.platesoft.auth.flyway.PlateAuthFlywayMigrationIT` +- **Given:** Empty Postgres 16 container. +- **When:** Boot starter with default config; Flyway runs. +- **Then:** Schema contains tables `users`, `user_identities`, `memberships`, `invitations`, `access_requests`, `login_events`. `flyway_schema_history_auth` table has 5 rows (V1..V5). All migrations are non-failed. + +### T-IT02 — Auto-config wires required beans + +- **Class:** `de.platesoft.auth.config.AuthBootstrapIT` +- **Then:** Context contains `JwtService`, `ExchangeService`, `JwtAuthenticationFilter`, `SecurityFilterChain`, default `OrgValidator`, default mailers. No bean is `@ConditionalOnMissingBean`-overridden when no user bean is provided. + +### T-IT03 — Full exchange flow + +- **Class:** `de.platesoft.auth.exchange.ExchangeFlowIT` +- **Given:** Seeded user. +- **When:** POST `/auth/exchange/mint` then POST `/auth/exchange/consume` with returned envelope. +- **Then:** `consume` returns 200 with `accessToken` + `refreshToken`. Access token is a valid JWT for the seeded user. + +### T-IT04 — JWT filter populates SecurityContext + +- **Class:** `de.platesoft.auth.filter.JwtAuthenticationFilterIT` +- **Mirrors:** [`JwtAuthenticationFilter.java`](backend/src/main/java/de/platesoft/inspectflow/filter/JwtAuthenticationFilter.java:21) behavior. +- **Then:** Request with valid `Authorization: Bearer ` populates `SecurityContextHolder` with `Authentication` containing `userId`, `email`, and authority `ROLE_`. Invalid token → no auth, filter chain continues, downstream returns 401 via Spring Security entry point. + +### T-IT05 — MembershipRepository queries + +- **Class:** `de.platesoft.auth.repository.MembershipRepositoryIT` +- **Then:** `findByUserIdAndOrgTypeAndOrgId`, `findByUserId`, `findAdminsByOrg` return expected rows. Unique constraint on `(user_id, org_type, org_id)` is enforced. + +### T-IT06 — Invitation accept flow + +- **Class:** `de.platesoft.auth.invitation.InvitationFlowIT` +- **Then:** Inviting an email → token in mailer mock. Accepting with token + signup → user created, membership created, invitation marked `ACCEPTED`. Second accept with same token returns 410. + +### T-IT07 — Access request approve flow + +- **Class:** `de.platesoft.auth.accessrequest.AccessRequestFlowIT` +- **Then:** End-to-end: anonymous request → admin sees pending list → admin approves → membership created → requester notified. + +### T-IT08 — Login event audit row written + +- **Class:** `de.platesoft.auth.audit.LoginEventAuditIT` +- **Then:** Every login attempt (success or failure) writes a `login_events` row with IP, user-agent, outcome (`SUCCESS` / `BAD_CREDENTIALS` / `EXPIRED` / `LOCKED`). Failed attempts do not write the password. + +### T-IT09 — SPI swap: custom OrgValidator + +- **Class:** `de.platesoft.auth.spi.ProviderSpiSwapIT` +- **Given:** Test config provides a `StrictOrgValidator` that only accepts `(COMPANY, 1)`. +- **Then:** Default `PermissiveOrgValidator` is **not** instantiated. Adding membership with `(COMPANY, 2)` fails. + +--- + +## 5. Frontend unit tests (`@platesoft/auth`) + +Vitest + jsdom. All tests live in `frontend/plate-auth/test/`. + +### T-FE01 — createAuthConfig factory + +- **Spec:** `createAuthConfig.test.ts` +- **Then:** Returned NextAuth config has `trustHost: true`, `pages.signIn = opts.signInPage ?? "/login"`, `providers` array contains entries for each provider explicitly enabled in `opts`, `callbacks.session` returns JWT-derived shape. + +### T-FE02 — Edge HMAC sign + verify + +- **Spec:** `hmac-edge.test.ts` +- **Then:** `signEnvelope(payload, secret)` produces output identical to backend `HmacEnvelope.sign(...)` (golden vector fixture). `verifyEnvelope(envelope, secret)` round-trips. Tampered envelope returns `false`. Uses Web Crypto (`crypto.subtle.importKey` + `crypto.subtle.sign('HMAC', ...)`) — no Node `crypto` import. + +### T-FE03 — Proxy strips hop-by-hop headers + +- **Spec:** `proxy-headers.test.ts` +- **Then:** Helper `sanitizeHopByHop(headers)` removes `connection`, `keep-alive`, `proxy-authenticate`, `proxy-authorization`, `te`, `trailer`, `transfer-encoding`, `upgrade`, `host`. Case-insensitive. Custom request headers are preserved. + +### T-FE04 — Proxy handler uses NextAuth v5 auth() + +- **Spec:** `proxy-handler.test.ts` +- **Then:** When `auth()` returns null → handler returns `Response(401)`. When `auth()` returns a session with `accessToken` → handler forwards request with `Authorization: Bearer ` and `duplex: "half"` on POST/PUT/PATCH bodies. Hop-by-hop headers from upstream are stripped on response. + +### T-FE05 — Conditional exports resolve correctly + +- **Spec:** `package-exports.test.ts` +- **Then:** `import {createAuthConfig} from "@platesoft/auth/server"` resolves; `import {createProxyHandlers} from "@platesoft/auth/edge"` resolves; `import {AuthProvider} from "@platesoft/auth/react"` resolves. Tree-shaking: edge bundle does not contain server-only imports (verified via `@arethetypeswrong/cli` smoke). + +--- + +## 6. End-to-end regression (InspectFlow as test bed) + +After Sprint-0-Plan § 10.2 Step 2 (InspectFlow refactored onto plate-auth 0.0.1), the existing InspectFlow Playwright suite **is** the E2E test for plate-auth. We do not duplicate these — we add a checkmark per scenario. + +### T-E2E01 — Anonymous flow (existing) + +- **Spec:** [`frontend/e2e/auth-flow.unauth.spec.ts`](frontend/e2e/auth-flow.unauth.spec.ts:1) +- **Then:** Unauth user redirected to `/login`. Sign-up disabled when `plate.auth.registration.enabled=false`. Password reset link visible. + +### T-E2E02 — Google sign-in + +- **Then:** OAuth callback works end-to-end against a mocked Google provider. New user → access request flow triggered (via `OnboardingHook` SPI). Existing user → tokens issued and redirected to dashboard. + +### T-E2E03 — Password login + +- **Then:** Valid credentials → tokens issued, dashboard loads. Wrong password → error message, no token, login_event row with `BAD_CREDENTIALS`. + +### T-E2E04 — Invitation accept + +- **Then:** Admin sends invite from `/admin/users`. Mailer mock captures URL. Invitee opens URL, sets password, lands in app with correct memberships. + +### T-E2E05 — Access request approve + +- **Then:** New user requests access. Admin sees pending request in `/admin/access-requests`. Approve → user receives email, can log in. + +### T-E2E06 — Admin audit endpoint visible + +- **Then:** Admin can view `/admin/audit` showing login_events. Non-admin gets 403. + +**Pass criterion (A7):** All 6 scenarios green on InspectFlow CI after the swap. If any test fails, treat as a Sprint 0 blocker — fix in plate-auth or revert. + +--- + +## 7. Security tests + +These overlap with unit/integration tests above but are extracted as a security suite for the Sprint 0 security review. + +### T-SEC01 — HMAC tamper rejected + +- **Same as T-UT07.** Mutate `role` field, expect 401 + audit event. + +### T-SEC02 — Nonce replay rejected + +- **Same as T-UT06.** Hit `/auth/exchange/consume` twice with same envelope, second call returns 409. + +### T-SEC03 — Envelope max-age rejected + +- **Same as T-UT08.** Clock-skew `iat` by 70s (max-age=60s), expect 401. + +### T-SEC04 — Expired JWT rejected + +- **Test:** Issue JWT with `exp = now - 1s`, send to `/api/me`. +- **Then:** 401. No SecurityContext populated. + +### T-SEC05 — Missing JWT secret fails startup + +- **Test:** Run Spring Boot integration test with `plate.auth.jwt.secret` unset. +- **Then:** Context fails to start with `BindValidationException` mentioning `jwt.secret` — `@NotBlank` violated. + +### T-SEC06 — Short JWT secret fails startup + +- **Test:** `plate.auth.jwt.secret=tooShort` (8 chars). +- **Then:** Context fails to start — `@Size(min=32)` violated. Clear error message. + +### T-SEC07 — CORS unknown origin rejected + +- **Test:** Preflight `OPTIONS /auth/login` with `Origin: https://attacker.example`. +- **Then:** No `Access-Control-Allow-Origin` returned. Browser would block the actual request. + +### T-SEC08 — SQL injection probe + +- **Test:** POST `/auth/login` body `{"email":"a@b.de' OR 1=1 --","password":"x"}`. +- **Then:** 401 (bad credentials), no SQL error leaks. Verifies JPA parameter binding, not string concat. + +### T-SEC09 — Constant-time HMAC compare + +- **Test:** Static-analysis spot check (`HmacEnvelope.verify` uses `MessageDigest.isEqual`). Also a microbenchmark comparing two HMACs that differ at byte 0 vs byte 31 — timing variance < 5%. + +### T-SEC10 — Refresh-token rotation + +- **Test:** Issue tokens via `/auth/exchange/consume`. Use refresh once → new token pair. Old refresh used again → 401. +- **Note:** This is a v0.2 candidate per [Roadmap.md](Roadmap.md); for v0.1 we accept refresh re-use as-is and document in [Open-Questions.md](Open-Questions.md). + +--- + +## 8. Performance smoke + +Run a JMH or simple `k6` script against a Postgres-backed test instance (Testcontainers or local Docker). Goal is **regression detection**, not absolute benchmarking — record numbers as baseline for v0.2. + +### T-PERF01 — Exchange consume p95 < 50ms + +- **Method:** 1000 sequential `POST /auth/exchange/consume` (single-replica) with valid envelopes. +- **Then:** p95 latency < 50ms on dev hardware. If > 100ms, flag for optimization (likely DB index or nonce lookup). + +### T-PERF02 — Login p95 < 300ms + +- **Method:** 500 sequential `POST /auth/login` with valid credentials. +- **Then:** p95 < 300ms (includes bcrypt cost factor 10 = 60-100ms baseline). If > 500ms, investigate. + +### T-PERF03 — JWT filter overhead per request + +- **Method:** Microbenchmark: 10,000 requests to a no-op endpoint protected by `JwtAuthenticationFilter`. +- **Then:** Filter overhead p95 < 5ms. + +--- + +## 9. Acceptance criteria → tests matrix + +Mapping back to [Sprint-0-Plan.md § 10.5](Sprint-0-Plan.md): + +| A# | Acceptance criterion | Test IDs | +|----|---------------------|----------| +| A1 | Backend artifact builds + publishes | Build pipeline (W6) + T-UT10..13, T-IT02, T-IT04..08 | +| A2 | Exchange flow works artifact-to-artifact | T-UT04..09, T-IT03, T-FE02, T-SEC01..03, T-SEC09 | +| A3 | Flyway applies on a fresh DB | T-IT01 | +| A4 | Config namespace `plate.auth.*` | T-UT01..03, T-UT14, T-IT02, T-SEC04..06 | +| A5 | SPI seams are clean | T-UT11, T-UT15, T-IT09 | +| A6 | Frontend artifact bundles + ships ESM | T-FE01..05 | +| A7 | InspectFlow refactor green | T-E2E01..06 | +| A8 | No regressions vs Sprint 14.6 baseline | T-E2E01..06 + T-PERF01..03 | + +Every Sprint 0 acceptance criterion has at least one mapped test. **A7** is the integration gate — the InspectFlow E2E suite must remain green after the dependency swap. + +--- + +## 10. Test data + fixtures + +- **Users:** `admin@plate.test` (USER+ADMIN in Org 1), `user@plate.test` (USER in Org 1), `outsider@plate.test` (no memberships). +- **Orgs:** Test SPI declares `(COMPANY, 1)` and `(COMPANY, 2)` as valid. +- **Secrets:** Test profile uses 32-char hex strings (`plate.auth.jwt.secret=0000...`) — not production-grade entropy. +- **Time:** Tests requiring clock control use `java.time.Clock` injected via test config (NOT `Thread.sleep`). +- **Mailers:** SPI default replaced by `RecordingMailer` that captures invocations. + +Fixtures live in `backend/src/test/resources/sql/seed-plate-auth.sql` and `frontend/plate-auth/test/fixtures/`. + +--- + +## 11. Test infrastructure + +| Layer | Tooling | +|-------|---------| +| Backend unit | JUnit 5 + Mockito + AssertJ | +| Backend integration | `@SpringBootTest` + Testcontainers Postgres 16 | +| Frontend unit | Vitest + jsdom | +| Frontend HMAC golden vector | Hand-built fixture pulled from backend test output (committed) | +| E2E | InspectFlow Playwright suite (no changes to plate-auth wiki) | +| Performance | `k6` script or JMH (TBD — pick simplest; not blocking Sprint 0) | +| CI | Gitea Actions `ci.yml` (Sprint-0-Plan § 8.2) — runs unit + integration on every push; E2E runs on InspectFlow's pipeline post-swap | + +--- + +## 12. Out of scope for Sprint 0 + +The following are explicitly **not** tested in Sprint 0 and are deferred to v0.2+: + +- Multi-replica nonce store (Redis-backed `NonceStore` SPI) → v0.3 +- Refresh-token rotation with revocation list → v0.2 +- Microsoft Entra ID provider → v0.2 (see [Open-Questions.md](Open-Questions.md) Q02) +- Email magic-link provider → v0.2 (see Q04) +- Account lockout after N failed logins → v0.2 +- 2FA / TOTP → v1.0 +- SAML / SCIM → never (see [Vision.md](Vision.md) non-goals) +- Load testing > 1000 req/s → not a v0.x concern +- Penetration testing (formal) → v1.0 + +--- + +## 13. Open issues / risks for the test plan + +| ID | Issue | Mitigation | +|----|-------|-----------| +| TR-1 | Performance numbers depend on dev hardware → not portable | Capture baseline + measure deltas, not absolutes | +| TR-2 | Testcontainers startup adds ~20s per test class | Use `@Testcontainers(disabledWithoutDocker = true)` + shared static container per test suite | +| TR-3 | Frontend HMAC vector must stay in sync with backend | Add a one-shot script `generate-hmac-fixture.sh` that re-emits the golden vector from backend test output | +| TR-4 | InspectFlow E2E couples our release to InspectFlow's CI health | Acceptable — InspectFlow is the first consumer by design (see [Migration-InspectFlow.md](Migration-InspectFlow.md)) | +| TR-5 | No staging environment for plate-auth alone | Library has no runtime of its own — InspectFlow CI **is** the staging | + +--- + +## 14. Cross-references + +- [Home.md](Home.md) +- [Vision.md](Vision.md) +- [Architecture.md](Architecture.md) +- [Roadmap.md](Roadmap.md) +- [Sprint-0-Assessment.md](Sprint-0-Assessment.md) +- [Sprint-0-Plan.md](Sprint-0-Plan.md) +- [Open-Questions.md](Open-Questions.md) +- [Integration-Guide.md](Integration-Guide.md) +- [Migration-InspectFlow.md](Migration-InspectFlow.md) + +--- + +**End of Sprint-0-Testplan.md (v1).**