Table of Contents
- Sprint 1 — Test Plan
- 1. Reading guide
- 2. Test overview (master table)
- 3. Unit tests (backend)
- T-UT16 — LoginEventSink fires asynchronously
- T-UT17 — MailInvitationMailer sends invitation email
- T-UT18 — MailAccessRequestMailer sends admin + requester notifications
- T-UT19 — Problem Details format for auth errors
- T-UT20 — Configurable invitation expiration
- 4. Integration tests (backend, Testcontainers)
- T-IT10 — MS Entra ID exchange flow
- T-IT11 — Invitation mailer sends real email (GreenMail)
- T-IT12 — RFC 7807 Problem Details on HTTP layer
- T-IT13 — Multi-provider exchange (Google + MS + email)
- 5. Security tests
- T-SEC11 — Email enumeration guard (magic-link)
- T-SEC12 — Magic-link token replay rejected
- T-SEC13 — Problem Details no information leak
- 6. Frontend unit tests (@platesoft/auth)
- T-FE06 — Microsoft provider snapshot
- T-FE07 — Email provider + email-linking guard
- T-FE08 — Type exports resolve correctly
- T-FE09 — Zod schemas validate correctly
- T-FE10 — Edge-runtime safe useAccessToken()
- 7. End-to-end regression (InspectFlow)
- 8. Acceptance criteria → tests matrix
- 9. Test data + fixtures (incremental)
- 10. Test infrastructure (incremental)
- 11. Open issues / risks for the test plan
- 12. Out of scope for Sprint 1
- 13. Cross-references
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
LoginEventSinkthat sleeps 500ms inemit(...)(simulating slow external sink).LoginEventServiceconfigured 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'semit(...)was called exactly once with the correctLoginEvent. A failing sink (throwsRuntimeException) 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,JavaMailSendermocked. AnInvitationwithemail=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. TheMimeMessagehas:To: newuser@test.deFrom: 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(...)throwsMailSendException,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]. AnAccessRequestwithstatus=PENDING,requester.email=applicant@test.de. - When:
notifyAdmins(request)called. - Then:
JavaMailSender.send(...)invoked once withTo: admin1@test.de, admin2@test.de. Subject: "New access request from applicant@test.de". - When:
notifyRequester(request)called withstatus=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
PlateAuthProblemDetailHandleractive. - Scenarios (parameterized):
ExchangeHmacInvalidException→ProblemDetailwithstatus=401,type=https://plate-software.de/errors/exchange-hmac-invalid,title="Exchange HMAC invalid"ExchangeReplayException→status=409,type=.../exchange-replayExchangeExpiredException→status=401,type=.../exchange-expiredOrgValidationException→status=400,type=.../org-validation-failedBadCredentialsException→status=401,type=.../bad-credentials,detail="Invalid credentials"(no "user not found" leak)
- Then: All responses have
Content-Type: application/problem+json. EachProblemDetailhastype,title,status,detail,instancefields 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=3→InvitationService.create(...)producesexpires_at ≈ now + 3 days(±2s)plate.auth.invitation.expiration-days=14→expires_at ≈ now + 14 daysplate.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
BindValidationExceptionwith the property pathplate.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-seededUserIdentity(provider=MICROSOFT, subject=<oid>, tenant_id=<tid>)exists. - When: POST
/api/auth/exchangewith 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-smtpTestcontainer or embedded GreenMail). Admin invitesnewuser@test.de. - When:
InvitationService.create(adminId, "newuser@test.de", orgType, orgId, MEMBER). - Then: GreenMail inbox for
newuser@test.decontains 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 —LoggingInvitationMailerlogs 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/exchangewith tampered HMAC signature - POST
/api/auth/loginwith wrong password - POST
/api/auth/exchangewith replayed nonce
- POST
- Then: Each response has
Content-Type: application/problem+json. JSON body containstype,title,status,detail,instance.statusmatches expected HTTP code (401, 401, 409 respectively). No stack trace or SQL in the body. Noexceptionfield 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
UserIdentityrows created with differentprovidervalues. Each user'sLoginEventrow has the correctproviderfield.
5. Security tests
T-SEC11 — Email enumeration guard (magic-link)
- Test: Request magic link for
existing@test.de(user exists) andnonexistent@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.
T-SEC12 — Magic-link token replay rejected
- 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 duplicateLoginEventrow 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
- Stack traces (
- 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
providersarray contains a Microsoft Entra ID provider entry. ThesignIncallback builds an envelope withprovider="microsoft". The provider hastenantId: '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.
allowDangerousEmailAccountLinkingisfalsein the provider config — verified by reading the provider object from the returned config. If a consumer tries to set ittrue, the factory overrides it tofalseand 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 isMembership[](notany).useAccessToken()return type isstring | null. Verified bytsc --noEmitagainst a test file that uses these types.
T-FE09 — Zod schemas validate correctly
- Spec:
zod-schemas.test.ts - Then:
ExchangeEnvelopeSchema.parse(validEnvelope)succeedsExchangeEnvelopeSchema.parse({ provider: 'invalid' })throws (invalid enum)ExchangeEnvelopeSchema.parse({ nonce: 'not-a-uuid' })throwsTokenResponseSchema.parse(validTokenResponse)succeedsTokenResponseSchema.parse({ accessToken: 123 })throws (wrong type)- The exchange client (
exchangeWithBackend) callsTokenResponseSchema.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. CalluseAccessToken(). - 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 noimport ... from 'next/headers'or similar). - Also: In browser/jsdom environment,
useAccessToken()still works viauseSession()(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=microsoftidentity (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
UserIdentityrow from v0.1 (samesubject+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
- Sprint-1-Plan.md — implementation plan
- Sprint-1-Assessment.md — priority classification + risks
- Sprint-0-Testplan.md — v0.1 test plan (regression baseline)
- Architecture.md — SPI model, wire contract
- Roadmap.md — v0.2 scope
- Open-Questions.md — Q02, Q04, Q12 deferrals
End of Sprint-1-Testplan.md (v1).