plan: add Sprint-0-Testplan.md (43 test cases: unit + integration + E2E + security + perf)
+477
@@ -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 <jwt>` populates `SecurityContextHolder` with `Authentication` containing `userId`, `email`, and authority `ROLE_<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 <token>` 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).**
|
||||
Reference in New Issue
Block a user