1
Sprint 1 Testplan
Patrick Plate edited this page 2026-06-24 21:59:30 +02:00

Sprint 1 — Test Plan

Status: Draft v1 Date: 2026-06-24 Owner: Patrick Scope: Validates Sprint 1 deliverable: de.platesoft:plate-auth-starter:0.2.0 + @platesoft/auth@0.2.0 Basis: Sprint-1-Plan.md Continuation: Test IDs continue from v0.1 (Sprint-0-Testplan: T-UT15, T-IT09, T-SEC10, T-FE05, T-E2E06, T-PERF03)


1. Reading guide

This test plan enumerates every new test case for v0.2. It does not re-list v0.1 tests — those must still pass (regression gate). All v0.1 test IDs (T-UT01..15, T-IT01..09, T-SEC01..09, T-FE01..05, T-E2E01..06, T-PERF01..03) remain in effect and must be green.

Each test case has:

  • ID (continuing the v0.1 numbering: T-UTxx, T-ITxx, T-SECxx, T-FExx, T-E2Exx)
  • Type (Unit / Integration / Frontend-Unit / E2E / Security)
  • Class / spec file
  • Scenarios (Given / When / Then)
  • Expected result
  • Acceptance criterion mapped (B1..B10 from Sprint-1-Plan.md §11)

Status legend: Open · 🟡 In progress · Passed · Failed · ⏭️ Skipped


2. Test overview (master table)

ID Type Class / Spec Maps to Status
T-UT16 Unit LoginEventSinkTest (async dispatch) B3
T-UT17 Unit MailInvitationMailerTest B4
T-UT18 Unit MailAccessRequestMailerTest B4
T-UT19 Unit PlateAuthProblemDetailTest B5
T-UT20 Unit InvitationExpirationConfigTest B6
T-IT10 Integration MicrosoftEntraExchangeIT B1
T-IT11 Integration InvitationMailerIT (GreenMail) B4
T-IT12 Integration ProblemDetailResponseIT B5
T-IT13 Integration MultiProviderExchangeIT B1, B2
T-SEC11 Security Email enumeration guard (magic-link) B2
T-SEC12 Security Magic-link token replay rejected B2
T-SEC13 Security Problem Details no information leak B5
T-FE06 Frontend-Unit microsoft-provider.test.ts B1
T-FE07 Frontend-Unit email-provider.test.ts B2
T-FE08 Frontend-Unit type-exports.test.ts B7
T-FE09 Frontend-Unit zod-schemas.test.ts B7
T-FE10 Frontend-Unit edge-runtime.test.ts B8
T-E2E07 E2E InspectFlow MS Entra sign-in (regression) B1, B10

v0.2 new tests: 18 — 5 Unit, 4 Integration, 3 Security, 5 Frontend-Unit, 1 E2E.

Combined v0.1 + v0.2 total: 61 test cases (43 from v0.1 + 18 from v0.2).


3. Unit tests (backend)

T-UT16 — LoginEventSink fires asynchronously

  • Class: de.platesoft.auth.spi.LoginEventSinkTest
  • Given: A LoginEventSink that sleeps 500ms in emit(...) (simulating slow external sink). LoginEventService configured with the sink.
  • When: A successful login triggers LoginEventService.recordSuccess(...).
  • Then: The recordSuccess(...) method returns immediately (< 50ms wall clock) — the sink dispatch is async. After 600ms, the sink's emit(...) was called exactly once with the correct LoginEvent. A failing sink (throws RuntimeException) logs a WARN but does not propagate to the login path.

T-UT17 — MailInvitationMailer sends invitation email

  • Class: de.platesoft.auth.spi.defaults.MailInvitationMailerTest
  • Given: plate.auth.mail.enabled=true, JavaMailSender mocked. An Invitation with email=newuser@test.de, orgType=COMPANY, orgId=<uuid>, role=MEMBER. Accept URL = https://app.example.com/invite/accept?token=abc123.
  • When: sendInvitation(invitation, acceptUrl) called.
  • Then: JavaMailSender.send(...) invoked exactly once. The MimeMessage has:
    • To: newuser@test.de
    • From: noreply@plate-software.de (from config)
    • Subject contains the org display name (via OrgDisplayNameResolver)
    • Body (text + HTML multipart) contains the accept URL
  • Also: If JavaMailSender.send(...) throws MailSendException, sendInvitation(...) re-throws (does not swallow). Verified by a second scenario.

T-UT18 — MailAccessRequestMailer sends admin + requester notifications

  • Class: de.platesoft.auth.spi.defaults.MailAccessRequestMailerTest
  • Given: plate.auth.mail.enabled=true, admin-recipients=[admin1@test.de, admin2@test.de]. An AccessRequest with status=PENDING, requester.email=applicant@test.de.
  • When: notifyAdmins(request) called.
  • Then: JavaMailSender.send(...) invoked once with To: admin1@test.de, admin2@test.de. Subject: "New access request from applicant@test.de".
  • When: notifyRequester(request) called with status=APPROVED.
  • Then: Email sent to applicant@test.de. Subject contains "approved".

T-UT19 — Problem Details format for auth errors

  • Class: de.platesoft.auth.config.PlateAuthProblemDetailTest
  • Given: Spring MVC test with PlateAuthProblemDetailHandler active.
  • Scenarios (parameterized):
    • ExchangeHmacInvalidExceptionProblemDetail with status=401, type=https://plate-software.de/errors/exchange-hmac-invalid, title="Exchange HMAC invalid"
    • ExchangeReplayExceptionstatus=409, type=.../exchange-replay
    • ExchangeExpiredExceptionstatus=401, type=.../exchange-expired
    • OrgValidationExceptionstatus=400, type=.../org-validation-failed
    • BadCredentialsExceptionstatus=401, type=.../bad-credentials, detail="Invalid credentials" (no "user not found" leak)
  • Then: All responses have Content-Type: application/problem+json. Each ProblemDetail has type, title, status, detail, instance fields populated. No stack trace, no SQL fragment, no internal class name in any field.

T-UT20 — Configurable invitation expiration

  • Class: de.platesoft.auth.config.InvitationExpirationConfigTest
  • Scenarios (parameterized):
    • plate.auth.invitation.expiration-days=3InvitationService.create(...) produces expires_at ≈ now + 3 days (±2s)
    • plate.auth.invitation.expiration-days=14expires_at ≈ now + 14 days
    • plate.auth.invitation.expiration-days=0 → fails startup (@Min(1) violated)
    • plate.auth.invitation.expiration-days=100 → fails startup (@Max(90) violated)
    • No config → default 7 days (v0.1 behavior preserved)
  • Then: Each scenario produces the expected result. Startup failures throw BindValidationException with the property path plate.auth.invitation.expiration-days.

4. Integration tests (backend, Testcontainers)

Same strategy as v0.1: @SpringBootTest + PostgreSQLContainer (Testcontainers Postgres 16).

T-IT10 — MS Entra ID exchange flow

  • Class: de.platesoft.auth.exchange.MicrosoftEntraExchangeIT
  • Given: Starter booted with plate.auth.providers.microsoft.enabled=true. MS JWKS endpoint mocked (wiremock or mock-server). A pre-seeded UserIdentity(provider=MICROSOFT, subject=<oid>, tenant_id=<tid>) exists.
  • When: POST /api/auth/exchange with envelope {provider="microsoft", providerSubject=<oid>, email=user@test.de, tenantId=<tid>, nonce=<uuid>, iat=<now>} signed with the exchange secret.
  • Then: 200 response with accessToken (valid JWT for the seeded user) + refreshToken + user + memberships. The existing Google exchange flow (T-IT03) is unaffected — verified by running both in the same test class.

T-IT11 — Invitation mailer sends real email (GreenMail)

  • Class: de.platesoft.auth.invitation.InvitationMailerIT
  • Given: Starter booted with plate.auth.mail.enabled=true + GreenMail SMTP test server (greenmail-smtp Testcontainer or embedded GreenMail). Admin invites newuser@test.de.
  • When: InvitationService.create(adminId, "newuser@test.de", orgType, orgId, MEMBER).
  • Then: GreenMail inbox for newuser@test.de contains exactly one email. Subject contains the org display name. Body contains the accept URL. HTML + plain-text multipart present.
  • Also: With plate.auth.mail.enabled=false (default), no email is sent — LoggingInvitationMailer logs instead. Verified by a second scenario.

T-IT12 — RFC 7807 Problem Details on HTTP layer

  • Class: de.platesoft.auth.config.ProblemDetailResponseIT
  • Given: Full Spring Boot test context with Testcontainers Postgres.
  • When:
    • POST /api/auth/exchange with tampered HMAC signature
    • POST /api/auth/login with wrong password
    • POST /api/auth/exchange with replayed nonce
  • Then: Each response has Content-Type: application/problem+json. JSON body contains type, title, status, detail, instance. status matches expected HTTP code (401, 401, 409 respectively). No stack trace or SQL in the body. No exception field leaked.

T-IT13 — Multi-provider exchange (Google + MS + email)

  • Class: de.platesoft.auth.exchange.MultiProviderExchangeIT
  • Given: Starter booted with all three providers enabled. MS JWKS mocked. Email provider configured.
  • When: Three separate exchange requests: {provider="google"}, {provider="microsoft"}, {provider="email"}.
  • Then: Each returns 200 with valid JWT. Three distinct UserIdentity rows created with different provider values. Each user's LoginEvent row has the correct provider field.

5. Security tests

  • Test: Request magic link for existing@test.de (user exists) and nonexistent@test.de (no user). Both via the email magic-link flow.
  • Then: Both responses are identical — same status code (200), same body ("If an account exists for this email, a sign-in link has been sent"). No timing difference

    50ms between the two. A magic link is sent only for the existing email (verified via mailer mock). No information leak that distinguishes existing from non-existing users.

  • Test: Complete a magic-link sign-in (receive token, click, exchange). Then attempt to reuse the same magic-link token for a second sign-in.
  • Then: Second use of the token is rejected — NextAuth Email provider tokens are single-use. The exchange envelope nonce is also deduped by ExchangeService (v0.1 protection). No duplicate LoginEvent row created.

T-SEC13 — Problem Details no information leak

  • Test: Trigger various error conditions (tampered HMAC, expired JWT, SQL-injection probe on /auth/login, invalid invitation token). Capture the raw HTTP response body.
  • Then: Response body (application/problem+json) contains none of:
    • Stack traces (at de.platesoft...)
    • SQL fragments (SELECT, WHERE, table names)
    • Internal class names (org.hibernate..., com.zaxxer...)
    • Environment variables or secrets
    • Full JWT tokens
  • Method: Regex scan of the response body against a denylist pattern. Fails if any pattern matches.

6. Frontend unit tests (@platesoft/auth)

Vitest + jsdom (same as v0.1). v0.2 tests live alongside v0.1 tests in the same test directories.

T-FE06 — Microsoft provider snapshot

  • Spec: microsoft-provider.test.ts
  • Given: createAuthConfig({ providers: { microsoft: { clientId: 'x', clientSecret: 'y', tenantId: 'common' } } }).
  • Then: Returned NextAuth config providers array contains a Microsoft Entra ID provider entry. The signIn callback builds an envelope with provider="microsoft". The provider has tenantId: 'common'.

T-FE07 — Email provider + email-linking guard

  • Spec: email-provider.test.ts
  • Given: createAuthConfig({ providers: { email: { server: 'smtp://...', from: 'noreply@test.de' } } }).
  • Then: Returned config contains an Email provider entry. allowDangerousEmailAccountLinking is false in the provider config — verified by reading the provider object from the returned config. If a consumer tries to set it true, the factory overrides it to false and logs a WARN.

T-FE08 — Type exports resolve correctly

  • Spec: type-exports.test.ts
  • Then: All of the following resolve without TypeScript errors:
    import { Membership, MembershipRole, MembershipStatus,
             Invitation, InvitationStatus,
             AccessRequest, AccessRequestStatus,
             TokenResponse, PlateAuthUser } from '@platesoft/auth';
    
    useMemberships() return type is Membership[] (not any). useAccessToken() return type is string | null. Verified by tsc --noEmit against a test file that uses these types.

T-FE09 — Zod schemas validate correctly

  • Spec: zod-schemas.test.ts
  • Then:
    • ExchangeEnvelopeSchema.parse(validEnvelope) succeeds
    • ExchangeEnvelopeSchema.parse({ provider: 'invalid' }) throws (invalid enum)
    • ExchangeEnvelopeSchema.parse({ nonce: 'not-a-uuid' }) throws
    • TokenResponseSchema.parse(validTokenResponse) succeeds
    • TokenResponseSchema.parse({ accessToken: 123 }) throws (wrong type)
    • The exchange client (exchangeWithBackend) calls TokenResponseSchema.parse() on the backend response — verified by mocking the fetch and checking that a malformed response throws.

T-FE10 — Edge-runtime safe useAccessToken()

  • Spec: edge-runtime.test.ts
  • Given: Test environment configured with @edge-runtime/vm (or vitest Edge environment).
  • When: import { useAccessToken } from '@platesoft/auth/client' in Edge runtime. Call useAccessToken().
  • Then: Does not throw. Returns string | null. No Node-only API (getSession(), cookies(), headers()) is imported in the module graph — verified by bundle analysis (the Edge entry point contains no import ... from 'next/headers' or similar).
  • Also: In browser/jsdom environment, useAccessToken() still works via useSession() (v0.1 behavior preserved).

7. End-to-end regression (InspectFlow)

T-E2E07 — InspectFlow MS Entra sign-in (post-v0.2 upgrade)

  • Spec: InspectFlow Playwright suite (new scenario added for v0.2)
  • Given: InspectFlow migrated to plate-auth v0.2.0 with MS Entra ID enabled. plate.auth.providers.microsoft.enabled=true.
  • When: A user with an existing provider=microsoft identity (seeded from InspectFlow's pre-migration data) signs in via Microsoft.
  • Then: OAuth callback → exchange → JWT issued. User lands on dashboard with their existing memberships. No data loss — the UserIdentity row from v0.1 (same subject + tenant_id) is matched, not duplicated.
  • Pass criterion (B1 + B10): This test must be green. If it fails, it means the MS Entra provider doesn't match InspectFlow's existing identities — this is a release blocker.

All v0.1 E2E tests (T-E2E01..06) must also remain green — v0.2 must not regress InspectFlow.


8. Acceptance criteria → tests matrix

Mapping back to Sprint-1-Plan.md §11:

B# Acceptance criterion Test IDs
B1 MS Entra ID provider works T-IT10, T-IT13, T-FE06, T-E2E07
B2 Email magic-link works T-IT13, T-FE07, T-SEC11, T-SEC12
B3 LoginEventSink SPI fires async T-UT16
B4 Real InvitationMailer sends email T-UT17, T-UT18, T-IT11
B5 RFC 7807 Problem Details T-UT19, T-IT12, T-SEC13
B6 Configurable invitation expiration T-UT20
B7 TS types + Zod schemas exported T-FE08, T-FE09
B8 Edge-runtime safe hooks T-FE10
B9 Both artifacts published at 0.2.0 mvn dependency:get + npm view
B10 All v0.1 tests still pass T-UT01..15, T-IT01..09, T-SEC01..09, T-FE01..05, T-E2E01..07

Every v0.2 acceptance criterion has at least one mapped test. B10 is the regression gate — all 43 v0.1 tests + the 18 new v0.2 tests must pass.


9. Test data + fixtures (incremental)

Building on v0.1's fixtures (Sprint-0-Testplan §10):

Fixture Purpose
MS JWKS mock (wiremock) T-IT10, T-IT13 — mock the Microsoft Entra JWKS endpoint with a test key pair
GreenMail SMTP container T-IT11 — in-memory mail server for real mailer tests
RecordingLoginEventSink T-UT16 — test sink that captures emit() calls and can simulate delay/failure
Zod test fixtures T-FE09 — valid + invalid envelope/token-response JSON for schema validation tests
Edge-runtime config T-FE10 — vitest config with @edge-runtime/vm environment

10. Test infrastructure (incremental)

Layer Tooling v0.2 additions
Backend unit JUnit 5 + Mockito + AssertJ Same as v0.1
Backend integration @SpringBootTest + Testcontainers Postgres 16 + GreenMail + GreenMail for mailer ITs
MS Entra mock WireMock or mock-server New — mocks MS JWKS endpoint
Frontend unit Vitest + jsdom Same
Frontend Edge @edge-runtime/vm New — Edge-runtime test environment
E2E InspectFlow Playwright suite + T-E2E07 MS Entra scenario
CI Gitea Actions ci.yml Runs v0.1 + v0.2 tests on every push

11. Open issues / risks for the test plan

ID Issue Mitigation
TR-6 MS Entra JWKS mock must match real MS token format Use a test key pair + jose library to sign realistic JWTs. Verify against a real MS Entra test tenant in a manual smoke test before v0.2.0 tag
TR-7 GreenMail SMTP in Testcontainers adds startup time Share the GreenMail container across test classes (static container, same as PostgresContainer pattern)
TR-8 Edge-runtime test environment may not exist for vitest yet If @edge-runtime/vm is unavailable, use vitest with environment: 'edge-runtime' or fall back to a bundle-analysis test (check the output for Node-only imports)
TR-9 InspectFlow MS Entra E2E requires a real MS Entra test tenant Manual smoke test before the automated E2E. The automated test can mock MS Entra for CI; the manual test validates against the real provider
TR-10 Wire-version bump (if triggered) changes exchange contract mid-sprint Lock the wire-version decision early (W6-2). If bumped, update T-IT03 + T-IT10..13 to send the correct wireVersion field.

12. Out of scope for Sprint 1

Deferred to v0.3+:

  • Multi-replica nonce store tests (Redis-backed) → v0.3
  • Refresh-token rotation tests (T-SEC10 from v0.1) → v0.3
  • WebAuthn / passkey provider tests → possibly v0.2 stretch
  • Per-app branding tests → deferred
  • Load testing > 1000 req/s → not a v0.x concern
  • Penetration testing (formal) → v1.0

13. Cross-references


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