2
Sprint 0 Testplan
Patrick Plate edited this page 2026-06-24 15:23:58 +02:00

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


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)

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) — called N times.
  • Then: Returns true (default-accept) every time, and emits one WARN log entry per call with message containing "OrgValidator default permissive — override de.platesoft.auth.spi.OrgValidator bean before production". Assert WARN count == N (not throttled, not one-shot).

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, plus the index idx_user_identities_microsoft_tenant_id from V5. flyway_schema_history_auth table has 6 rows (V1..V6). 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 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)

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; for v0.1 we accept refresh re-use as-is and document in 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:

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 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 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)
TR-5 No staging environment for plate-auth alone Library has no runtime of its own — InspectFlow CI is the staging

14. Cross-references


End of Sprint-0-Testplan.md (v1).