plan(v0.2): Sprint-1 Assessment + Plan + Testplan — 6 workstreams, 18 new tests
v0.2 'Polish' planned in parallel with v0.1 implementation: - W1: MS Entra ID provider - W2: Email magic-link provider - W3: LoginEventSink SPI + real JavaMailSender mailers - W4: RFC 7807 Problem Details + configurable invitation expiration - W5: TypeScript types + Zod schemas + Edge-runtime safety - W6: Wire-version assessment + tests + publish v0.2.0 Key prediction: no wire-version bump, no new Flyway migration, purely additive Effort: ~6 days with GLM-5.2+Lumen
@@ -0,0 +1,253 @@
|
|||||||
|
# Sprint 1 — Assessment
|
||||||
|
|
||||||
|
**Status:** Draft v1
|
||||||
|
**Date:** 2026-06-24
|
||||||
|
**Owner:** Patrick (plate-software)
|
||||||
|
**Purpose:** Assess what v0.1 delivered, predict what consumers (InspectFlow + Sparkboard) will
|
||||||
|
need in their first month, classify each v0.2 feature by priority, and surface the risks of the
|
||||||
|
polish sprint. This document is the basis for [`Sprint-1-Plan.md`](Sprint-1-Plan.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. v0.1 baseline — what shipped
|
||||||
|
|
||||||
|
Sprint 0 (W1–W12) extracted InspectFlow's auth/membership system into a reusable two-artifact
|
||||||
|
library. The table below is the *delivered* surface at the `v0.1.0` tag, cross-referenced to the
|
||||||
|
[Architecture](Architecture.md) tiers.
|
||||||
|
|
||||||
|
### 1.1 Backend — `de.platesoft:plate-auth-starter:0.1.0`
|
||||||
|
|
||||||
|
| Layer | Delivered | Reference |
|
||||||
|
|---|---|---|
|
||||||
|
| Auto-config | `PlateAuthAutoConfiguration` (explicit `@Import`, no `@ComponentScan`) + `PlateAuthProperties` (`plate.auth.*`) + `META-INF/spring/...AutoConfiguration.imports` | [Architecture §3](Architecture.md) |
|
||||||
|
| Security | `SecurityConfig` scoped via `securityMatcher(...)` at `@Order(100)` — only plate-auth endpoints | Q13 (decided) |
|
||||||
|
| Filter | `JwtAuthenticationFilter`, `OrgContextResolver` | [Architecture §5–6](Architecture.md) |
|
||||||
|
| Service | `JwtService`, `ExchangeService`, `OAuthService`, `MembershipService`, `InvitationService`, `AccessRequestService`, `LoginEventService` | [Architecture §3.2](Architecture.md) |
|
||||||
|
| Controller | `OAuthController`, `AuthController`, `InvitationController`, `AccessRequestController`, `AdminAuditController` | [Architecture §3.1](Architecture.md) |
|
||||||
|
| Entity | `User`, `UserIdentity`, `Membership`, `Invitation`, `AccessRequest`, `LoginEvent`, `RevInfo` | [Architecture §7](Architecture.md) |
|
||||||
|
| SPI | `OrgValidator`, `OrgDisplayNameResolver`, `InvitationMailer`, `AccessRequestMailer`, `OnboardingHook` (5 interfaces + 5 no-op/defaults via `@ConditionalOnMissingBean`) | [Architecture §3.4](Architecture.md) |
|
||||||
|
| Migration | Flyway V1–V6 under `classpath:db/migration/auth/` + separate `flyway_schema_history_auth` table | [Architecture §8.1](Architecture.md) |
|
||||||
|
| Wire contract | `WIRE_VERSION=1` constant in exchange envelope, backend rejects mismatch | Q06 (decided) |
|
||||||
|
| Provider | Google OAuth (enabled by default); Microsoft + email toggles exist but are **off** | Q02/Q04 (deferred) |
|
||||||
|
|
||||||
|
### 1.2 Frontend — `@platesoft/auth@0.1.0`
|
||||||
|
|
||||||
|
| Module | Delivered | Reference |
|
||||||
|
|---|---|---|
|
||||||
|
| `config/` | `createAuthConfig(opts)` factory → NextAuth v5 config with Google provider | [Architecture §4.1](Architecture.md) |
|
||||||
|
| `exchange/` | `signEnvelope()` / `verifyEnvelope()` using Web Crypto (Edge-safe) + `exchangeWithBackend()` | [Architecture §4.1](Architecture.md) |
|
||||||
|
| `proxy/` | `createProxyHandlers(opts)` — `auth()` → Bearer injection + hop-by-hop strip + `duplex:"half"` | [Architecture §4.1](Architecture.md) |
|
||||||
|
| `middleware/` | `createAuthMiddleware(opts)` with configurable `publicPaths` | [Architecture §4.1](Architecture.md) |
|
||||||
|
| `client/` | `useAccessToken()`, `useMemberships()`, re-export of `useSession`/`signIn`/`signOut` | [Architecture §4.1](Architecture.md) |
|
||||||
|
| Build | `tsup` dual ESM/CJS + `.d.ts` | Q09 (decided) |
|
||||||
|
|
||||||
|
### 1.3 What v0.1 explicitly left behind (deferred backlog)
|
||||||
|
|
||||||
|
These items were tracked in [Sprint-0-Plan §11](Sprint-0-Plan.md) and now form the v0.2 input
|
||||||
|
backlog. Each was a *deliberate* deferral, not an oversight:
|
||||||
|
|
||||||
|
| # | Deferred item | Deferred because | Target |
|
||||||
|
|---|---|---|---|
|
||||||
|
| D1 | MS Entra ID provider | Extraction discipline — never in InspectFlow 14.x | v0.2 |
|
||||||
|
| D2 | Email magic-link provider | Same — InspectFlow 14.1 deferred it | v0.2 |
|
||||||
|
| D3 | `LoginEventSink` SPI | DB rows + Envers sufficient for 2 consumers | v0.2 |
|
||||||
|
| D4 | Real `InvitationMailer` (JavaMailSender) | No-op logger is enough to extract; real mail is polish | v0.2 |
|
||||||
|
| D5 | `AccessRequestMailer` template | Same | v0.2 |
|
||||||
|
| D6 | Configurable invitation expiration | Hardcoded 7d is fine for extraction | v0.2 |
|
||||||
|
| D7 | RFC 7807 Problem Details | v0.1 errors are plain Spring Boot defaults | v0.2 |
|
||||||
|
| D8 | TS type exports (`Membership`, `Invitation`, `AccessRequest`) | Consumers can cast in v0.1 | v0.2 |
|
||||||
|
| D9 | Zod schemas for envelope + DTOs | Nice-to-have | v0.2 |
|
||||||
|
| D10 | Edge-runtime safe `useAccessToken()` | Current uses `getSession()` (App-Router-only) | v0.2 |
|
||||||
|
| D11 | Refresh-token rotation hardening | Stateless JWT refresh accepted for v0.1 | v0.3 (Roadmap) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Consumer forecast — first-month needs
|
||||||
|
|
||||||
|
The [Roadmap](Roadmap.md) says v0.2 is "triggered by feedback from consumers during their first
|
||||||
|
month using v0.1." We cannot wait for that feedback to plan — Sparkboard is greenfield and will hit
|
||||||
|
its needs on day 1. This section predicts the pain points based on what each consumer *is*.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
V01[v0.1.0 ships] --> IF[InspectFlow migrates<br/>~1 week]
|
||||||
|
V01 --> SB[Sparkboard adopts<br/>~3 weeks]
|
||||||
|
IF --> F1[Feedback wave 1<br/>operational pain]
|
||||||
|
SB --> F2[Feedback wave 2<br/>greenfield friction]
|
||||||
|
F1 --> V02[v0.2 backlog<br/>confirmed/adjusted]
|
||||||
|
F2 --> V02
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.1 InspectFlow — operational pain (week 1–2)
|
||||||
|
|
||||||
|
InspectFlow is migrating *existing* users from in-tree auth onto the library. Its pain points are
|
||||||
|
operational, not greenfield:
|
||||||
|
|
||||||
|
| Predicted pain | Why | v0.2 feature that fixes it |
|
||||||
|
|---|---|---|
|
||||||
|
| **MS Entra ID sign-in stops working** | InspectFlow's V30 migration + `TenantAutoMapService` handled MS tenants. plate-auth v0.1 ships no MS provider — InspectFlow users with `provider=microsoft` identities will have broken sign-in until v0.2. | **W1 — MS Entra ID provider** |
|
||||||
|
| **Invitation emails silently fail** | v0.1's `LoggingInvitationMailer` logs the URL instead of sending mail. InspectFlow admins who invite users will think the invite sent, but nothing arrives. | **W3 — Real `InvitationMailer`** |
|
||||||
|
| **Error responses are opaque** | v0.1 returns Spring Boot's default JSON error body. InspectFlow's frontend has no structured way to distinguish "expired token" from "invalid credentials." | **W4 — RFC 7807 Problem Details** |
|
||||||
|
| **Invitations expire too fast / too slow** | InspectFlow's original 7d window is hardcoded. If they want 3d or 14d, they must fork. | **W4 — Configurable expiration** |
|
||||||
|
|
||||||
|
### 2.2 Sparkboard — greenfield friction (week 1–3)
|
||||||
|
|
||||||
|
Sparkboard starts from zero. Its pain is *developer experience* friction — things that are possible
|
||||||
|
but require more boilerplate than they should:
|
||||||
|
|
||||||
|
| Predicted pain | Why | v0.2 feature that fixes it |
|
||||||
|
|---|---|---|
|
||||||
|
| **No email sign-in option** | Sparkboard is a 4-person family app ("Family Spark"). Google-only auth is fine for Patrick, but his friend's sons may not have Google accounts. Email magic-link is the natural second option. | **W2 — Email magic-link** |
|
||||||
|
| **Edge middleware can't read access token** | `useAccessToken()` calls `getSession()` which is App-Router-only. If Sparkboard uses Edge-runtime middleware for route protection, the hook throws. | **W5 — Edge-runtime safety** |
|
||||||
|
| **TypeScript types are loose** | Sparkboard's frontend gets `any` from `useMemberships()` because `Membership` isn't exported. Runtime works, but DX is rough. | **W5 — TS improvements** |
|
||||||
|
| **No email UX page** | Email magic-link requires a callback page. Consumers build it themselves in v0.1. | **W2 — default UX page** |
|
||||||
|
| **Login events stuck in DB only** | Sparkboard may want to tee login events to a webhook or Loki for the homelab observability stack. | **W3 — `LoginEventSink` SPI** |
|
||||||
|
|
||||||
|
### 2.3 Hypothesis validation
|
||||||
|
|
||||||
|
Per [Roadmap § "Hypothesis-driven scoping"](Roadmap.md):
|
||||||
|
|
||||||
|
> **v0.2 hypothesis:** The SPI is enough — no consumer needs to fork.
|
||||||
|
|
||||||
|
We validate this by tracking whether InspectFlow or Sparkboard shadow-override plate-auth classes
|
||||||
|
instead of using the SPI. If they fork, the next minor becomes a "why do they fork" investigation.
|
||||||
|
v0.2 adds `LoginEventSink` as a **new** SPI specifically to prevent a fork around audit shipping.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. v0.2 feature classification
|
||||||
|
|
||||||
|
Each v0.2 candidate from the [Roadmap](Roadmap.md) and the [Open-Questions](Open-Questions.md)
|
||||||
|
deferrals is classified into three tiers. Priority is driven by **consumer-blocking severity**, not
|
||||||
|
effort.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph Must[Must-have — blocks a consumer]
|
||||||
|
M1[W1: MS Entra ID<br/>blocks InspectFlow]
|
||||||
|
M2[W3: Real InvitationMailer<br/>blocks invitations]
|
||||||
|
M3[W4: RFC 7807 + config expiration<br/>blocks error UX]
|
||||||
|
end
|
||||||
|
subgraph Nice[Nice-to-have — smooths DX]
|
||||||
|
N1[W2: Email magic-link<br/>Sparkboard option]
|
||||||
|
N2[W3: LoginEventSink SPI<br/>audit shipping]
|
||||||
|
N3[W3: AccessRequestMailer template]
|
||||||
|
N4[W5: TS improvements<br/>typed exports + Zod]
|
||||||
|
N5[W5: Edge-runtime safety]
|
||||||
|
end
|
||||||
|
subgraph Stretch[Stretch — if time allows]
|
||||||
|
S1[W6: Wire-version bump<br/>if contract changes]
|
||||||
|
S2[WebAuthn / passkey]
|
||||||
|
S3[Per-app branding]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1 Must-have (blocks at least one consumer)
|
||||||
|
|
||||||
|
| # | Feature | Blocks whom | Justification |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **W1** | MS Entra ID provider | InspectFlow | InspectFlow has live `provider=microsoft` identities. Without the provider, those users cannot sign in post-migration. This is a **regression**, not a missing feature. The SPI hooks (`OnboardingHook`, V5 tenant-index migration) are ready — the provider implementation is the only gap. |
|
||||||
|
| **W3** | Real `InvitationMailer` (JavaMailSender) | Both | v0.1's logging mailer means invitations silently fail in any non-dev environment. Any consumer that uses invitations (both do) hits this within week 1. |
|
||||||
|
| **W4** | RFC 7807 Problem Details | Both | Consumers cannot build robust error UX on v0.1's opaque error bodies. This is the #1 frontend DX complaint we predict. |
|
||||||
|
|
||||||
|
### 3.2 Nice-to-have (improves DX but has a workaround)
|
||||||
|
|
||||||
|
| # | Feature | Workaround if skipped | Justification |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **W2** | Email magic-link provider | Google-only auth | Sparkboard's family users *can* use Google. But the friend's sons may not have accounts — email magic-link is the natural fallback. Predicted to become must-have if Sparkboard gets a non-Google user. |
|
||||||
|
| **W3** | `LoginEventSink` SPI | Query `login_events` table directly | DB rows work. The SPI lets consumers tee to Loki/SIEM without polling. Nice for homelab observability; not blocking. |
|
||||||
|
| **W3** | `AccessRequestMailer` template | Same logging fallback as invitation | Less critical than invitation mailer (access requests are less frequent), but should ship together for symmetry. |
|
||||||
|
| **W5** | TS improvements (typed exports + Zod) | Cast to `any` / manual types | Works, but rough DX. Zod schemas enable runtime validation on the frontend. Predicted to be the #2 frontend complaint. |
|
||||||
|
| **W5** | Edge-runtime safe `useAccessToken()` | Don't use Edge middleware for auth | `useAccessToken()` currently throws in Edge runtime. Workaround: use Node middleware. But this is a footgun — should be fixed proactively. |
|
||||||
|
|
||||||
|
### 3.3 Stretch (only if must-haves + nice-to-haves finish early)
|
||||||
|
|
||||||
|
| # | Feature | Why stretch |
|
||||||
|
|---|---|---|
|
||||||
|
| **W6** | Wire-version bump to `WIRE_VERSION=2` | Only needed if the envelope contract actually changes (e.g., adding provider-specific fields). If the MS/email providers fit the existing `ExchangeEnvelope` shape, no bump needed. Conditional. |
|
||||||
|
| — | WebAuthn / passkey provider | No consumer has asked. NextAuth supports it, but backend plumbing is non-trivial. Roadmap says "possibly v0.2." Defer unless a consumer explicitly requests. |
|
||||||
|
| — | Per-app branding on exchange endpoint | Custom user-agent string for audit. Low value for a 2-consumer library. Defer. |
|
||||||
|
|
||||||
|
### 3.4 Priority → workstream mapping
|
||||||
|
|
||||||
|
| Priority | Workstream | Effort (GLM-5.2+Lumen) |
|
||||||
|
|---|---|---|
|
||||||
|
| Must-have | W1 (MS Entra), W3 (mailers), W4 (RFC 7807 + config) | ~3 days |
|
||||||
|
| Nice-to-have | W2 (email), W3 (LoginEventSink), W5 (TS + Edge) | ~3 days |
|
||||||
|
| Stretch | W6 (wire-version + IT + publish) | ~1 day |
|
||||||
|
| **Total** | **W1–W6** | **~7 days** (fits 3 calendar weeks per Roadmap) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Risk register
|
||||||
|
|
||||||
|
| # | Risk | Likelihood | Impact | Mitigation |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| R1 | MS Entra ID provider doesn't match InspectFlow's existing `TenantAutoMapService` behavior | Medium | **High** — InspectFlow MS users locked out | W1 must replicate the tenant→org auto-mapping via `OnboardingHook`. Test against InspectFlow's seeded MS identities. The V5 tenant-index migration is already shipped — use it. |
|
||||||
|
| R2 | Email magic-link introduces email enumeration vector | Medium | Medium | `allowDangerousEmailAccountLinking=false` (already enforced). Magic-link responses must be generic ("if the email exists, a link was sent") — no "user not found" leak. Test T-SEC11. |
|
||||||
|
| R3 | `JavaMailSender` default fails silently when SMTP misconfigured | Medium | Medium | The real `InvitationMailer` must **fail loudly** — throw on mail-send failure, not swallow. Integration test with a mock SMTP (GreenMail or Testcontainers mail). Consumer must set `spring.mail.host`. |
|
||||||
|
| R4 | RFC 7807 Problem Details breaks v0.1 consumers' error parsing | Low | Medium | Problem Details is *additive* (new `Content-Type: application/problem+json`). v0.1 consumers that parsed the old body will see a different shape. Document in CHANGELOG. If the wire-version changes, bump `WIRE_VERSION`. |
|
||||||
|
| R5 | Edge-runtime safety fix changes `useAccessToken()` return semantics | Low | Low | The fix makes the hook work in Edge (reads from cookie/token-store instead of `getSession()`). Non-Edge consumers are unaffected. Snapshot test guards the contract. |
|
||||||
|
| R6 | Wire-version bump to 2 forces a coordinated upgrade across both consumers | Medium (if triggered) | Medium | Per [versioning policy](Roadmap.md), wire-version change = MINOR bump (`0.2.0`). Both consumers upgrade lockstep. CHANGELOG documents the migration. |
|
||||||
|
| R7 | `LoginEventSink` SPI fires synchronously and slows login | Low | Medium | SPI must be **async** (annotated `@Async` or dispatched to an executor). The default no-op sink has zero overhead. Test T-UT16 measures login latency with a sink attached. |
|
||||||
|
| R8 | Zod schemas drift from backend DTOs | Low | Low | Generate Zod schemas from a single source (TS types) or use a contract test. The golden-vector approach from T-FE02 extends to schema validation. |
|
||||||
|
| R9 | MS Entra ID `common` tenant vs single-tenant mismatch | Medium | Medium | Provider config must expose `tenant-id` as a property (`plate.auth.providers.microsoft.tenant-id`). Default `common` (multi-tenant). InspectFlow may pin to their tenant. Document in [Integration-Guide.md](Integration-Guide.md). |
|
||||||
|
| R10 | Email magic-link token replay | Low | Medium | NextAuth Email provider generates single-use HMAC'd magic links. Backend `ExchangeService` already has nonce dedup. The magic-link flow goes through the same exchange envelope — reuse existing replay protection. |
|
||||||
|
| R11 | Configurable invitation expiration = 0 or negative breaks invitation creation | Low | Low | Bean-validate `plate.auth.invitation.expiration-days` with `@Min(1) @Max(90)`. Fails fast at startup if out of range. |
|
||||||
|
| R12 | v0.1 ships late, compressing v0.2 timeline | Medium | Medium | v0.2 is "as priorities allow" per [Roadmap](Roadmap.md). Must-haves (W1/W3/W4) can ship as v0.2.0 even if nice-to-haves slip to v0.2.1. No hard deadline except consumer pressure. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Dependencies we are accepting
|
||||||
|
|
||||||
|
v0.2 adds these dependencies on top of v0.1's stack ([Sprint-0-Assessment §4](Sprint-0-Assessment.md)):
|
||||||
|
|
||||||
|
| Dep | Version | Added by | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spring-boot-starter-mail` | (Spring Boot managed) | W3 (real mailers) | Brings `JavaMailSender`. Already listed as a dep in v0.1's pom but unused — now activated. |
|
||||||
|
| `next-auth` Email provider | 5.x (beta tracking) | W2 (magic-link) | Part of NextAuth v5, no new package. Requires `nodemailer` or SMTP config on the consumer side. |
|
||||||
|
| `zod` | ^3.x | W5 (TS schemas) | Runtime validation. Ships as a **dependency** of `@platesoft/auth`, not a peer dep (consumers get it automatically). |
|
||||||
|
|
||||||
|
No new Spring Boot / Java / Postgres version changes. v0.2 stays on the v0.1 baseline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Recommendation
|
||||||
|
|
||||||
|
**GO** — proceed to [`Sprint-1-Plan.md`](Sprint-1-Plan.md) with the following constraints:
|
||||||
|
|
||||||
|
1. **W1 (MS Entra ID) is the critical path.** It is the only must-have that fixes a *regression*
|
||||||
|
(broken sign-in for existing InspectFlow MS users). It must land first and be tested against
|
||||||
|
InspectFlow's seeded MS identities.
|
||||||
|
2. **W3 (mailers) and W4 (RFC 7807 + config) are must-haves but lower-risk.** They can proceed in
|
||||||
|
parallel with W1.
|
||||||
|
3. **Nice-to-haves (W2, W5) are planned but not promised.** If v0.1 consumer feedback redirects
|
||||||
|
priorities, these can slip to v0.2.1 without blocking the v0.2.0 release.
|
||||||
|
4. **Wire-version bump (W6) is conditional.** Only bump `WIRE_VERSION` if the envelope contract
|
||||||
|
actually changes. If MS/email providers fit the existing shape, `WIRE_VERSION` stays at 1 and
|
||||||
|
v0.2.0 is a pure additive release.
|
||||||
|
5. **WebAuthn + per-app branding stay deferred.** No consumer has asked. Revisit at v0.3.
|
||||||
|
6. **No new Flyway migrations in v0.2.** The V1–V6 schema is sufficient. If MS Entra needs a new
|
||||||
|
column (e.g., `microsoft_graph_id`), add V7 — but only if the existing `user_identities.subject`
|
||||||
|
field is insufficient. Predicted: no new migration needed.
|
||||||
|
|
||||||
|
The Roadmap says v0.2 is "triggered by feedback." This assessment plans proactively so the
|
||||||
|
must-haves are ready the moment v0.1 ships. Consumer feedback adjusts the nice-to-have / stretch
|
||||||
|
split, not the must-have core.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Cross-references
|
||||||
|
|
||||||
|
- [Roadmap.md](Roadmap.md) — v0.2 section + hypothesis-driven scoping
|
||||||
|
- [Architecture.md](Architecture.md) — SPI model, wire contract, threat model
|
||||||
|
- [Open-Questions.md](Open-Questions.md) — Q02 (MS Entra deferred), Q04 (email deferred), Q12 (audit DB-only)
|
||||||
|
- [Sprint-0-Plan.md](Sprint-0-Plan.md) — §11 deferred items (v0.2 backlog source)
|
||||||
|
- [Sprint-0-Assessment.md](Sprint-0-Assessment.md) — format reference + v0.1 source inventory
|
||||||
|
- [Sprint-1-Plan.md](Sprint-1-Plan.md) — implementation plan based on this assessment
|
||||||
|
- [Sprint-1-Testplan.md](Sprint-1-Testplan.md) — test matrix for v0.2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Assessment ready for Plan Reviewer. Sprint-1-Plan v1 will be drafted assuming all Section 6
|
||||||
|
recommendations hold; any Plan Reviewer pushback re-opens the priority split.*
|
||||||
+750
@@ -0,0 +1,750 @@
|
|||||||
|
# Sprint 1 — Implementation Plan
|
||||||
|
|
||||||
|
**Status:** Draft v1
|
||||||
|
**Date:** 2026-06-24
|
||||||
|
**Owner:** Patrick (plate-software)
|
||||||
|
**Based on:** [`Sprint-1-Assessment.md`](Sprint-1-Assessment.md), [`Architecture.md`](Architecture.md), [`Roadmap.md`](Roadmap.md) v0.2 section
|
||||||
|
**Target version:** `0.2.0` (both Maven + npm, lockstep)
|
||||||
|
**Theme:** *Polish — make the consumer experience pleasant*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reading guide
|
||||||
|
|
||||||
|
This plan follows the same structure as [`Sprint-0-Plan.md`](Sprint-0-Plan.md):
|
||||||
|
|
||||||
|
1. **Scope + ground rules** (this section)
|
||||||
|
2. **Workstream overview** (6 workstreams, W1–W6)
|
||||||
|
3. **Step-by-step per workstream** (goal, steps, done-when)
|
||||||
|
4. **Security review checklist additions** (incremental over v0.1's §9)
|
||||||
|
5. **Rollout + acceptance criteria**
|
||||||
|
|
||||||
|
Each step is numbered so the Code-mode worker can check off progress without ambiguity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Scope + ground rules
|
||||||
|
|
||||||
|
### 1.1 In scope (Sprint 1 / v0.2.0)
|
||||||
|
|
||||||
|
- **W1** — MS Entra ID provider (backend OAuth + frontend NextAuth provider)
|
||||||
|
- **W2** — Email magic-link provider (backend verification + frontend NextAuth Email provider + default UX page)
|
||||||
|
- **W3** — `LoginEventSink` SPI + real `InvitationMailer` (JavaMailSender) + `AccessRequestMailer` admin template
|
||||||
|
- **W4** — RFC 7807 Problem Details on `/api/auth/*` + configurable invitation expiration
|
||||||
|
- **W5** — TypeScript improvements (typed exports, Zod schemas) + Edge-runtime safe `useAccessToken()`
|
||||||
|
- **W6** — Wire-version assessment + integration tests + publish `v0.2.0`
|
||||||
|
|
||||||
|
### 1.2 Out of scope (deferred — see [Roadmap](Roadmap.md))
|
||||||
|
|
||||||
|
- Multi-replica nonce store (v0.3)
|
||||||
|
- Refresh-token rotation table (v0.3)
|
||||||
|
- JWT secret rotation via `kid` (v0.3)
|
||||||
|
- WebAuthn / passkeys (possibly v0.2 — see §1.3)
|
||||||
|
- Per-app branding on exchange endpoint (possibly v0.2 — see §1.3)
|
||||||
|
- SAML, SCIM, OIDC server, mobile SDKs (post-1.0)
|
||||||
|
|
||||||
|
### 1.3 Stretch (only if must-haves + nice-to-haves finish early)
|
||||||
|
|
||||||
|
- WebAuthn / passkey provider — no consumer has asked; defer unless explicitly requested
|
||||||
|
- Per-app branding — low value for a 2-consumer library; defer
|
||||||
|
- ADR-style docs auto-published to wiki
|
||||||
|
|
||||||
|
### 1.4 Ground rules
|
||||||
|
|
||||||
|
- **v0.2 is additive.** No removal of v0.1 public API. Any breaking change is documented in
|
||||||
|
CHANGELOG with a migration recipe (per [deprecation policy](Roadmap.md)).
|
||||||
|
- **Lockstep versions.** `@platesoft/auth@0.2.0` and `de.platesoft:plate-auth-starter:0.2.0` ship
|
||||||
|
together from the same `v0.2.0` git tag.
|
||||||
|
- **Wire-version discipline.** If the exchange envelope contract changes, bump `WIRE_VERSION` to 2.
|
||||||
|
If it doesn't (providers fit existing shape), `WIRE_VERSION` stays at 1 and v0.2 is purely additive.
|
||||||
|
- **Branch policy:** `feature/sprint-1/<workstream>` branches → PR → squash-merge to `main`.
|
||||||
|
- **GLM-5.2 + Lumen working model.** Effort estimates reflect AI-assisted speed — the provider
|
||||||
|
implementations follow the same OAuth pattern as Google (already proven in v0.1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Workstream overview
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
W1[W1: MS Entra ID provider] --> W6[W6: Wire-version<br/>+ IT + publish]
|
||||||
|
W2[W2: Email magic-link] --> W6
|
||||||
|
W3[W3: LoginEventSink SPI<br/>+ real mailers] --> W6
|
||||||
|
W4[W4: RFC 7807 Problem Details<br/>+ config expiration] --> W6
|
||||||
|
W5[W5: TS improvements<br/>+ Edge safety] --> W6
|
||||||
|
W6 --> TAG[v0.2.0 tag]
|
||||||
|
```
|
||||||
|
|
||||||
|
| # | Workstream | Priority | Depends on | Est. (AI-assisted) |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| W1 | MS Entra ID provider (backend + frontend) | **Must-have** | v0.1 shipped | ~1 day |
|
||||||
|
| W2 | Email magic-link provider | Nice-to-have | v0.1 shipped | ~1 day |
|
||||||
|
| W3 | LoginEventSink SPI + real mailers | **Must-have** (mailers) / Nice (SPI) | v0.1 shipped | ~1.5 days |
|
||||||
|
| W4 | RFC 7807 + configurable expiration | **Must-have** | v0.1 shipped | ~0.5 days |
|
||||||
|
| W5 | TS improvements + Edge safety | Nice-to-have | v0.1 shipped | ~1 day |
|
||||||
|
| W6 | Wire-version + IT + publish v0.2.0 | Required | W1–W5 | ~1 day |
|
||||||
|
| | **Total** | | | **~6 days** |
|
||||||
|
|
||||||
|
W1–W5 are parallelizable. W6 is the integration + publish gate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. W1 — MS Entra ID provider
|
||||||
|
|
||||||
|
**Goal:** Add Microsoft Entra ID as a first-class OAuth provider, symmetric with Google. This fixes
|
||||||
|
the regression where InspectFlow's existing `provider=microsoft` identities have no sign-in path in
|
||||||
|
v0.1.
|
||||||
|
|
||||||
|
**Branch:** `feature/sprint-1/w1-ms-entra`
|
||||||
|
|
||||||
|
### 3.1 Backend
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **W1-1** Create `service/MicrosoftEntraService.java` (or extend `OAuthService`) to handle MS
|
||||||
|
Entra ID token verification. Same pattern as Google:
|
||||||
|
- Accept `id_token` from the frontend exchange envelope
|
||||||
|
- Verify signature against MS Entra JWKS endpoint
|
||||||
|
- Extract `oid` (Microsoft's stable user id), `email`, `name`, `tid` (tenant id)
|
||||||
|
- Find-or-create `User` + `UserIdentity(provider=MICROSOFT, subject=oid, tenant_id=tid)`
|
||||||
|
- Call `OnboardingHook.onFirstSignIn(...)` for new users
|
||||||
|
2. **W1-2** Update `LoginProvider` enum — add `MICROSOFT` (if not already present from v0.1's
|
||||||
|
`email` toggle). The enum is already provider-agnostic.
|
||||||
|
3. **W1-3** Add `Microsoft` provider config to `PlateAuthProperties.Providers`:
|
||||||
|
```java
|
||||||
|
@Data public static class Microsoft {
|
||||||
|
private boolean enabled = false;
|
||||||
|
private String tenantId = "common"; // "common", "organizations", or a specific GUID
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. **W1-4** The exchange envelope already has a `provider` field. MS Entra payloads use
|
||||||
|
`provider="microsoft"`. No envelope schema change needed — the existing `ExchangeEnvelope` is
|
||||||
|
provider-agnostic.
|
||||||
|
5. **W1-5** Register the provider conditionally:
|
||||||
|
```java
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(prefix = "plate.auth.providers.microsoft", name = "enabled", havingValue = "true")
|
||||||
|
public MicrosoftEntraService microsoftEntraService(...) { ... }
|
||||||
|
```
|
||||||
|
Fail-fast if enabled without `spring.security.oauth2.client.registration.microsoft.*` configured.
|
||||||
|
|
||||||
|
**Done when:** A POST to `/api/auth/exchange` with `provider=microsoft` and a valid MS Entra
|
||||||
|
`id_token` returns `{access_token, refresh_token, user, memberships}`. Existing Google flow is
|
||||||
|
unaffected (T-IT10).
|
||||||
|
|
||||||
|
### 3.2 Frontend
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **W1-6** Implement `packages/auth/src/config/providers/microsoft.ts`:
|
||||||
|
```ts
|
||||||
|
export function microsoftProvider(opts: {
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
tenantId?: string; // default "common"
|
||||||
|
}): NextAuthConfig.Provider;
|
||||||
|
```
|
||||||
|
Uses NextAuth v5's `MicrosoftEntraID` provider.
|
||||||
|
2. **W1-7** Wire into `createAuthConfig(opts)` — add `microsoft?: MicrosoftOpts` to
|
||||||
|
`PlateAuthConfigOptions.providers`. If present, add the MS provider to the provider list.
|
||||||
|
3. **W1-8** Update `ExchangeEnvelope` type — the `provider` union type becomes
|
||||||
|
`'google' | 'microsoft' | 'email' | 'password'` (already in v0.1's type, now *exercised*).
|
||||||
|
4. **W1-9** Document env vars in [Architecture §4.2](Architecture.md):
|
||||||
|
```
|
||||||
|
MICROSOFT_ENTRA_CLIENT_ID=...
|
||||||
|
MICROSOFT_ENTRA_CLIENT_SECRET=...
|
||||||
|
MICROSOFT_ENTRA_TENANT_ID=common
|
||||||
|
```
|
||||||
|
|
||||||
|
**Done when:** A Next.js app with `createAuthConfig({ providers: { microsoft: {...} } })` shows a
|
||||||
|
"Sign in with Microsoft" button and completes the OAuth flow → exchange → JWT. Snapshot test
|
||||||
|
verifies the provider is present in the config (T-FE06).
|
||||||
|
|
||||||
|
### 3.3 InspectFlow regression test
|
||||||
|
|
||||||
|
1. **W1-10** Verify against InspectFlow's seeded MS identities. The V5 migration
|
||||||
|
(`V5__add_microsoft_tenant_id_index.sql`) already indexes `user_identities.microsoft_tenant_id`.
|
||||||
|
A user with `provider=microsoft, subject=<oid>, tenant_id=<tid>` must sign in successfully and
|
||||||
|
get their existing memberships.
|
||||||
|
|
||||||
|
**Done when:** InspectFlow MS user sign-in is green post-v0.2.0 migration. T-E2E07 validates this.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. W2 — Email magic-link provider
|
||||||
|
|
||||||
|
**Goal:** Add email magic-link sign-in as a second auth option. Users enter their email, receive a
|
||||||
|
one-time link, click it, and are authenticated. No password required.
|
||||||
|
|
||||||
|
**Branch:** `feature/sprint-1/w2-email-magic-link`
|
||||||
|
|
||||||
|
### 4.1 Backend
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **W2-1** The exchange envelope already supports `provider="email"`. The backend
|
||||||
|
`ExchangeService.consume(...)` path is provider-agnostic — it verifies the HMAC envelope and
|
||||||
|
find-or-creates the user. No backend changes needed for the *exchange* itself.
|
||||||
|
2. **W2-2** Add email-provider config to `PlateAuthProperties.Providers`:
|
||||||
|
```java
|
||||||
|
@Data public static class EmailMagicLink {
|
||||||
|
private boolean enabled = false;
|
||||||
|
private String fromAddress; // e.g., "noreply@plate-software.de"
|
||||||
|
@Min(60) private int tokenTtlSeconds = 600; // 10 min magic-link TTL
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. **W2-3** The magic-link flow is handled by NextAuth's Email provider on the *frontend* side —
|
||||||
|
it sends the magic link email via SMTP. The backend's role is to receive the verified identity
|
||||||
|
via the exchange envelope (same as Google). No new backend endpoint.
|
||||||
|
|
||||||
|
**Done when:** Backend accepts `provider="email"` envelopes. Config validation fails-fast if
|
||||||
|
`email-magic-link.enabled=true` without SMTP config.
|
||||||
|
|
||||||
|
### 4.2 Frontend
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **W2-4** Implement `packages/auth/src/config/providers/email.ts`:
|
||||||
|
```ts
|
||||||
|
export function emailProvider(opts: {
|
||||||
|
server?: SMTPConfig; // or connection string
|
||||||
|
from: string;
|
||||||
|
}): NextAuthConfig.Provider;
|
||||||
|
```
|
||||||
|
Uses NextAuth v5's `Email` provider (nodemailer-backed).
|
||||||
|
2. **W2-5** Wire into `createAuthConfig(opts)` — add `email?: EmailOpts` to
|
||||||
|
`PlateAuthConfigOptions.providers`.
|
||||||
|
3. **W2-6** Create a **default magic-link callback page** component that consumers can drop in:
|
||||||
|
`packages/auth/src/components/MagicLinkCallback.tsx`. This handles the NextAuth verify-callback
|
||||||
|
route. Consumers import it or build their own — documented in
|
||||||
|
[Integration-Guide.md](Integration-Guide.md).
|
||||||
|
4. **W2-7** Security: verify `allowDangerousEmailAccountLinking=false` is enforced in the email
|
||||||
|
provider config. Snapshot test (T-FE07).
|
||||||
|
|
||||||
|
**Done when:** A Next.js app with `createAuthConfig({ providers: { email: {...} } })` shows an email
|
||||||
|
input, sends a magic link, and the callback completes the sign-in → exchange → JWT flow.
|
||||||
|
|
||||||
|
### 4.3 Email enumeration guard
|
||||||
|
|
||||||
|
1. **W2-8** The email-provider response must be **generic**: "If an account exists for this email,
|
||||||
|
a sign-in link has been sent." No "user not found" error. This prevents email enumeration.
|
||||||
|
Test: T-SEC12.
|
||||||
|
|
||||||
|
**Done when:** Magic-link request for a non-existent email returns the same response as an existing
|
||||||
|
email. No information leak.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. W3 — LoginEventSink SPI + real mailers
|
||||||
|
|
||||||
|
**Goal:** Two deliverables bundled here because they share the SPI/mailer architecture:
|
||||||
|
|
||||||
|
1. **`LoginEventSink` SPI** — new extension point for consumers to tee login events to external
|
||||||
|
systems (Loki, SIEM, webhooks) instead of (or in addition to) DB rows.
|
||||||
|
2. **Real `InvitationMailer` + `AccessRequestMailer`** — replace v0.1's no-op loggers with
|
||||||
|
`JavaMailSender`-backed implementations that actually send email.
|
||||||
|
|
||||||
|
**Branch:** `feature/sprint-1/w3-spi-mailers`
|
||||||
|
|
||||||
|
### 5.1 LoginEventSink SPI
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **W3-1** Create the SPI interface under `de.platesoft.auth.spi`:
|
||||||
|
```java
|
||||||
|
public interface LoginEventSink {
|
||||||
|
/**
|
||||||
|
* Called after a login event is recorded. Implementations may ship to
|
||||||
|
* Loki, OpenSearch, Kafka, or any external sink. Must be non-blocking —
|
||||||
|
* called asynchronously by LoginEventService.
|
||||||
|
*
|
||||||
|
* @param event the recorded login event (never null)
|
||||||
|
*/
|
||||||
|
void emit(LoginEvent event);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
2. **W3-2** Create the default no-op implementation:
|
||||||
|
```java
|
||||||
|
@Component
|
||||||
|
@ConditionalOnMissingBean(LoginEventSink.class)
|
||||||
|
public class NoOpLoginEventSink implements LoginEventSink {
|
||||||
|
@Override
|
||||||
|
public void emit(LoginEvent event) {
|
||||||
|
// no-op — default. Override to ship externally.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. **W3-3** Wire `LoginEventSink` into `LoginEventService`:
|
||||||
|
- After `loginEventRepository.save(event)`, call `loginEventSink.emit(event)` **asynchronously**
|
||||||
|
(`@Async` or a dedicated executor).
|
||||||
|
- The sink must **not** block the login flow. A failing sink logs a WARN but never fails the
|
||||||
|
login.
|
||||||
|
- Wrap the `emit()` call in a try-catch that logs at WARN level on failure.
|
||||||
|
4. **W3-4** Register the SPI in `PlateAuthAutoConfiguration` `@Import` list.
|
||||||
|
5. **W3-5** Update [Architecture §3.4](Architecture.md) SPI table — add `LoginEventSink` as the 6th
|
||||||
|
SPI. Default = `NoOpLoginEventSink`.
|
||||||
|
|
||||||
|
**Done when:** A consumer can `@Bean` a `LoginEventSink` and receive every login event. The default
|
||||||
|
no-op sink has zero overhead. Login latency is unaffected (async dispatch). T-UT16 validates.
|
||||||
|
|
||||||
|
### 5.2 Real InvitationMailer
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **W3-6** Create `de.platesoft.auth.spi.defaults.MailInvitationMailer`:
|
||||||
|
```java
|
||||||
|
@Component
|
||||||
|
@ConditionalOnMissingBean(InvitationMailer.class)
|
||||||
|
@ConditionalOnProperty(prefix = "plate.auth.mail", name = "enabled", havingValue = "true")
|
||||||
|
public class MailInvitationMailer implements InvitationMailer {
|
||||||
|
private final JavaMailSender mailSender;
|
||||||
|
private final PlateAuthProperties properties;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendInvitation(Invitation invitation, String acceptUrl) {
|
||||||
|
MimeMessage msg = mailSender.createMimeMessage();
|
||||||
|
// build HTML + plain-text multipart from template
|
||||||
|
// subject: "You're invited to join {orgDisplayName}"
|
||||||
|
// body: link to acceptUrl, expiration info
|
||||||
|
mailSender.send(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
2. **W3-7** The **default** `InvitationMailer` when `plate.auth.mail.enabled=false` (or unset)
|
||||||
|
remains the existing `LoggingInvitationMailer`. The new `MailInvitationMailer` activates only
|
||||||
|
when mail is explicitly enabled. This preserves v0.1's "boots green with zero config" guarantee.
|
||||||
|
3. **W3-8** Create email templates:
|
||||||
|
- `src/main/resources/templates/invitation.html` — HTML body with accept-url placeholder
|
||||||
|
- `src/main/resources/templates/invitation.txt` — plain-text fallback
|
||||||
|
- Use Spring's `MimeMessageHelper` for multipart.
|
||||||
|
4. **W3-9** Fail-loudly: if `plate.auth.mail.enabled=true` but `spring.mail.host` is unset, fail
|
||||||
|
fast at startup with a clear error: `"plate.auth.mail.enabled=true but spring.mail.host is not configured"`.
|
||||||
|
|
||||||
|
**Done when:** With `plate.auth.mail.enabled=true` + SMTP config, inviting a user sends a real email
|
||||||
|
with the accept link. Without mail config, the logging fallback activates. T-UT17 + T-IT11 validate.
|
||||||
|
|
||||||
|
### 5.3 AccessRequestMailer admin template
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **W3-10** Create `de.platesoft.auth.spi.defaults.MailAccessRequestMailer` — same pattern as
|
||||||
|
`MailInvitationMailer`. Activates with `plate.auth.mail.enabled=true`.
|
||||||
|
2. **W3-11** Two notification methods:
|
||||||
|
- `notifyAdmins(request)` — sends to configured admin email list
|
||||||
|
- `notifyRequester(request)` — sends approval/denial notification to the requester
|
||||||
|
3. **W3-12** Templates:
|
||||||
|
- `templates/access-request-admin.html` — "New access request from {email}"
|
||||||
|
- `templates/access-request-decision.html` — "Your access request was {approved/denied}"
|
||||||
|
4. **W3-13** Admin email list from config: `plate.auth.mail.admin-recipients` (comma-separated).
|
||||||
|
|
||||||
|
**Done when:** Access request submission → admins notified. Approval/denial → requester notified.
|
||||||
|
T-UT18 validates.
|
||||||
|
|
||||||
|
### 5.4 Mail config additions
|
||||||
|
|
||||||
|
Add to `PlateAuthProperties`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Data public static class Mail {
|
||||||
|
private boolean enabled = false;
|
||||||
|
private String fromAddress = "noreply@plate-software.de";
|
||||||
|
private List<String> adminRecipients = new ArrayList<>();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
plate:
|
||||||
|
auth:
|
||||||
|
mail:
|
||||||
|
enabled: false # opt-in; default is logging fallback
|
||||||
|
from-address: noreply@plate-software.de
|
||||||
|
admin-recipients:
|
||||||
|
- admin@plate-software.de
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. W4 — RFC 7807 Problem Details + configurable expiration
|
||||||
|
|
||||||
|
**Goal:** Two small must-haves bundled:
|
||||||
|
|
||||||
|
1. **RFC 7807 Problem Details** — structured error responses (`application/problem+json`) on all
|
||||||
|
`/api/auth/*` endpoints so consumers can build robust error UX.
|
||||||
|
2. **Configurable invitation expiration** — `plate.auth.invitation.expiration-days` (currently
|
||||||
|
hardcoded 7d).
|
||||||
|
|
||||||
|
**Branch:** `feature/sprint-1/w4-problem-details-config`
|
||||||
|
|
||||||
|
### 6.1 RFC 7807 Problem Details
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **W4-1** Create `config/PlateAuthProblemDetailHandler` — extends Spring Boot 4's
|
||||||
|
`ProblemDetailExceptionHandler` (Spring 6 / Boot 4 has built-in RFC 7807 support via
|
||||||
|
`@ControllerAdvice` + `ProblemDetail`). Wire it as a `@ControllerAdvice` for the
|
||||||
|
`de.platesoft.auth.controller` package.
|
||||||
|
2. **W4-2** Define custom problem types for plate-auth-specific errors:
|
||||||
|
```java
|
||||||
|
public enum PlateAuthProblems {
|
||||||
|
EXCHANGE_HMAC_INVALID("https://plate-software.de/errors/exchange-hmac-invalid", 401),
|
||||||
|
EXCHANGE_EXPIRED("https://plate-software.de/errors/exchange-expired", 401),
|
||||||
|
EXCHANGE_REPLAY("https://plate-software.de/errors/exchange-replay", 409),
|
||||||
|
ORG_VALIDATION_FAILED("https://plate-software.de/errors/org-validation-failed", 400),
|
||||||
|
INVITATION_EXPIRED("https://plate-software.de/errors/invitation-expired", 410),
|
||||||
|
INVITATION_REVOKED("https://plate-software.de/errors/invitation-revoked", 410);
|
||||||
|
// ... each maps to a `type` URI + status code
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. **W4-3** Ensure all v0.1 custom exceptions map to `ProblemDetail`:
|
||||||
|
- `ExchangeHmacInvalidException` → 401 + `EXCHANGE_HMAC_INVALID` type
|
||||||
|
- `ExchangeReplayException` → 409 + `EXCHANGE_REPLAY` type
|
||||||
|
- `ExchangeExpiredException` → 401 + `EXCHANGE_EXPIRED` type
|
||||||
|
- `OrgValidationException` → 400 + `ORG_VALIDATION_FAILED` type
|
||||||
|
- Generic `BadCredentials` → 401 + `https://plate-software.de/errors/bad-credentials`
|
||||||
|
4. **W4-4** Security: Problem Details responses must **not** leak stack traces, SQL fragments, or
|
||||||
|
internal class names. The `detail` field is consumer-safe; the `instance` field is the request
|
||||||
|
path. Test T-SEC13.
|
||||||
|
5. **W4-5** Set `Content-Type: application/problem+json` on all error responses from plate-auth
|
||||||
|
endpoints. v0.1's default Spring Boot error body is replaced.
|
||||||
|
|
||||||
|
**Done when:** A failed login returns `application/problem+json` with `type`, `title`, `status`,
|
||||||
|
`detail`, `instance`. A replayed exchange envelope returns 409 with the `EXCHANGE_REPLAY` type.
|
||||||
|
T-UT19 + T-IT12 validate.
|
||||||
|
|
||||||
|
### 6.2 Configurable invitation expiration
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **W4-6** Add to `PlateAuthProperties`:
|
||||||
|
```java
|
||||||
|
@Data public static class Invitation {
|
||||||
|
@Min(1) @Max(90)
|
||||||
|
private int expirationDays = 7;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
2. **W4-7** Update `InvitationService.create(...)` — replace hardcoded `Duration.ofDays(7)` with
|
||||||
|
`Duration.ofDays(properties.getInvitation().getExpirationDays())`.
|
||||||
|
3. **W4-8** Bean-validate: `@Min(1) @Max(90)` prevents 0/negative/absurd values. Fails fast at
|
||||||
|
startup.
|
||||||
|
4. **W4-9** Document the config in [Architecture §3.3](Architecture.md):
|
||||||
|
```yaml
|
||||||
|
plate:
|
||||||
|
auth:
|
||||||
|
invitation:
|
||||||
|
expiration-days: 7 # default; range 1–90
|
||||||
|
```
|
||||||
|
|
||||||
|
**Done when:** Setting `plate.auth.invitation.expiration-days=3` produces invitations that expire
|
||||||
|
in 3 days. Setting it to 0 or 100 fails startup validation. T-UT20 validates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. W5 — TypeScript improvements + Edge-runtime safety
|
||||||
|
|
||||||
|
**Goal:** Two frontend DX improvements:
|
||||||
|
|
||||||
|
1. **TypeScript type exports + Zod schemas** — export proper types for `Membership`, `Invitation`,
|
||||||
|
`AccessRequest` and add runtime-validation Zod schemas for the exchange envelope + DTOs.
|
||||||
|
2. **Edge-runtime safe `useAccessToken()`** — replace the App-Router-only `getSession()` call with
|
||||||
|
an Edge-compatible approach.
|
||||||
|
|
||||||
|
**Branch:** `feature/sprint-1/w5-ts-edge`
|
||||||
|
|
||||||
|
### 7.1 TypeScript type exports + Zod schemas
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **W5-1** Create `packages/auth/src/types/index.ts` — export domain types mirroring backend entities:
|
||||||
|
```ts
|
||||||
|
export interface Membership {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
orgType: string;
|
||||||
|
orgId: string;
|
||||||
|
role: MembershipRole;
|
||||||
|
status: MembershipStatus;
|
||||||
|
}
|
||||||
|
export type MembershipRole = 'OWNER' | 'ADMIN' | 'MEMBER' | 'VIEWER';
|
||||||
|
export type MembershipStatus = 'ACTIVE' | 'SUSPENDED' | 'REVOKED';
|
||||||
|
|
||||||
|
export interface Invitation {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
orgType: string;
|
||||||
|
orgId: string;
|
||||||
|
role: MembershipRole;
|
||||||
|
status: InvitationStatus;
|
||||||
|
expiresAt: string; // ISO-8601
|
||||||
|
}
|
||||||
|
export type InvitationStatus = 'PENDING' | 'ACCEPTED' | 'REVOKED' | 'EXPIRED';
|
||||||
|
|
||||||
|
export interface AccessRequest {
|
||||||
|
id: string;
|
||||||
|
requesterId: string;
|
||||||
|
orgType: string;
|
||||||
|
orgId: string;
|
||||||
|
requestedRole: MembershipRole;
|
||||||
|
status: AccessRequestStatus;
|
||||||
|
}
|
||||||
|
export type AccessRequestStatus = 'PENDING' | 'APPROVED' | 'DENIED' | 'EXPIRED';
|
||||||
|
|
||||||
|
export interface TokenResponse {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
user: PlateAuthUser;
|
||||||
|
memberships: Membership[];
|
||||||
|
}
|
||||||
|
export interface PlateAuthUser {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
role: 'USER' | 'ADMIN';
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
2. **W5-2** Re-export from `packages/auth/src/index.ts` so consumers can
|
||||||
|
`import { Membership, Invitation, AccessRequest } from '@platesoft/auth'`.
|
||||||
|
3. **W5-3** Update `useMemberships()` return type from implicit `any` to `Membership[]`.
|
||||||
|
Update `useAccessToken()` return type to explicit `string | null`.
|
||||||
|
4. **W5-4** Add Zod schemas in `packages/auth/src/schemas/index.ts`:
|
||||||
|
```ts
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const ExchangeEnvelopeSchema = z.object({
|
||||||
|
wireVersion: z.literal(1).or(z.literal(2)),
|
||||||
|
provider: z.enum(['google', 'microsoft', 'email', 'password']),
|
||||||
|
providerSubject: z.string(),
|
||||||
|
email: z.string().email(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
inviteToken: z.string().optional(),
|
||||||
|
nonce: z.string().uuid(),
|
||||||
|
iat: z.number().int().positive(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TokenResponseSchema = z.object({
|
||||||
|
accessToken: z.string(),
|
||||||
|
refreshToken: z.string(),
|
||||||
|
user: z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
email: z.string().email(),
|
||||||
|
role: z.enum(['USER', 'ADMIN']),
|
||||||
|
firstName: z.string().optional(),
|
||||||
|
lastName: z.string().optional(),
|
||||||
|
}),
|
||||||
|
memberships: z.array(z.object({
|
||||||
|
orgType: z.string(),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
role: z.enum(['OWNER', 'ADMIN', 'MEMBER', 'VIEWER']),
|
||||||
|
status: z.enum(['ACTIVE', 'SUSPENDED', 'REVOKED']),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
5. **W5-5** Use `TokenResponseSchema.parse()` in the exchange client to validate backend response
|
||||||
|
at runtime — catches contract drift early.
|
||||||
|
6. **W5-6** Add `zod` as a dependency (not peer dep) in `packages/auth/package.json`.
|
||||||
|
|
||||||
|
**Done when:** `import { Membership, Invitation, ExchangeEnvelopeSchema } from '@platesoft/auth'`
|
||||||
|
resolves with full type safety. The exchange client validates the backend response via Zod.
|
||||||
|
T-FE08 + T-FE09 validate.
|
||||||
|
|
||||||
|
### 7.2 Edge-runtime safe useAccessToken()
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **W5-7** The current `useAccessToken()` calls `getSession()` which uses Node-only APIs. This
|
||||||
|
throws in the Edge runtime. Fix: detect runtime and use a cookie-based fallback:
|
||||||
|
```ts
|
||||||
|
export function useAccessToken(): string | null {
|
||||||
|
// Edge-runtime safe: read from a cookie set by the proxy handler
|
||||||
|
const cookieValue = getCookie('plate-auth-token');
|
||||||
|
return cookieValue ?? null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Or, if called in a client component (browser), use `useSession()` as before.
|
||||||
|
2. **W5-8** Document the two modes:
|
||||||
|
- **Client component (browser):** `useSession()` — token in NextAuth JWT (encrypted cookie)
|
||||||
|
- **Edge middleware:** read `plate-auth-token` cookie (set by proxy handler)
|
||||||
|
3. **W5-9** Add an Edge-runtime test (`@edge-runtime/vm` or `vitest` with Edge env) that imports
|
||||||
|
`useAccessToken()` and verifies it does not throw (T-FE10).
|
||||||
|
|
||||||
|
**Done when:** `useAccessToken()` works in both browser and Edge runtime without throwing. No
|
||||||
|
Node-only API imported in the Edge entry point. T-FE10 validates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. W6 — Wire-version assessment + integration tests + publish v0.2.0
|
||||||
|
|
||||||
|
**Goal:** Assess whether the wire-version needs bumping, write the v0.2 integration tests, and
|
||||||
|
publish `v0.2.0` to the Gitea Package Registry.
|
||||||
|
|
||||||
|
**Branch:** `feature/sprint-1/w6-publish`
|
||||||
|
|
||||||
|
### 8.1 Wire-version assessment
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **W6-1** Review whether any v0.2 change alters the exchange envelope contract:
|
||||||
|
- MS Entra ID: `provider="microsoft"` — already in the v0.1 `provider` union. **No change.**
|
||||||
|
- Email magic-link: `provider="email"` — already in the v0.1 `provider` union. **No change.**
|
||||||
|
- LoginEventSink: backend-only, no envelope impact. **No change.**
|
||||||
|
- RFC 7807: response-only, no request/envelope change. **No change.**
|
||||||
|
- TS types/Zod: frontend-only, no wire impact. **No change.**
|
||||||
|
- Config expiration: backend-only. **No change.**
|
||||||
|
2. **W6-2** **Decision point:** If all changes fit the existing `ExchangeEnvelope` shape, then
|
||||||
|
`WIRE_VERSION` stays at **1**. v0.2.0 is a pure additive release.
|
||||||
|
- **If** a provider-specific field is added to the envelope, then bump `WIRE_VERSION` to **2**.
|
||||||
|
- **Prediction:** No bump needed. The envelope is provider-agnostic by design.
|
||||||
|
|
||||||
|
**Done when:** Wire-version decision is documented in CHANGELOG.
|
||||||
|
|
||||||
|
### 8.2 Integration tests
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **W6-3** Implement **T-IT10..13** (new integration tests for v0.2):
|
||||||
|
- T-IT10: MS Entra exchange flow — mock MS JWKS, verify token → user → JWT
|
||||||
|
- T-IT11: Invitation mailer IT — `plate.auth.mail.enabled=true` + GreenMail → email sent
|
||||||
|
- T-IT12: RFC 7807 error responses — failed login returns `application/problem+json`
|
||||||
|
- T-IT13: Multi-provider IT — Google + MS + email all configured, each completes exchange
|
||||||
|
2. **W6-4** Run full regression: `mvn -pl plate-auth-starter verify` + `mvn -pl it verify` + all
|
||||||
|
v0.1 tests still pass.
|
||||||
|
|
||||||
|
**Done when:** All v0.1 + v0.2 tests green against Testcontainers Postgres.
|
||||||
|
|
||||||
|
### 8.3 Publish v0.2.0
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **W6-5** Update `CHANGELOG.md` with v0.2.0 release notes (new features + migration notes).
|
||||||
|
2. **W6-6** Bump version: `pom.xml` revision → `0.2.0`, `packages/auth/package.json` → `0.2.0`.
|
||||||
|
3. **W6-7** Cut `v0.2.0` git tag. Gitea Actions publishes both artifacts.
|
||||||
|
4. **W6-8** Verify: `mvn dependency:get de.platesoft:plate-auth-starter:0.2.0` + `npm view
|
||||||
|
@platesoft/auth@0.2.0` from a fresh machine.
|
||||||
|
|
||||||
|
**Done when:** `v0.2.0` tag publishes both artifacts to the Gitea Package Registry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Security review checklist additions
|
||||||
|
|
||||||
|
Incremental over v0.1's security checklist ([Sprint-0-Plan §9](Sprint-0-Plan.md)). Items marked
|
||||||
|
`[v0.2-new]` are new for this sprint.
|
||||||
|
|
||||||
|
### 9.1 MS Entra ID provider [v0.2-new]
|
||||||
|
|
||||||
|
- [ ] MS Entra `id_token` signature verified against MS JWKS endpoint (not just decoded).
|
||||||
|
- [ ] Tenant ID (`tid`) extracted and stored in `user_identities.tenant_id`.
|
||||||
|
- [ ] Provider is `@ConditionalOnProperty`. Default disabled. Fail-fast without client-id/secret.
|
||||||
|
- [ ] `tenantId` config exposed: `common`, `organizations`, or a specific GUID.
|
||||||
|
|
||||||
|
### 9.2 Email magic-link [v0.2-new]
|
||||||
|
|
||||||
|
- [ ] `allowDangerousEmailAccountLinking = false` — verified by snapshot test (T-FE07).
|
||||||
|
- [ ] Magic-link request response is **generic** — no email enumeration (T-SEC12).
|
||||||
|
- [ ] Magic-link token TTL configurable (default 600s/10min). Token is single-use.
|
||||||
|
- [ ] Magic-link emails do not contain PII beyond the targeted email address.
|
||||||
|
|
||||||
|
### 9.3 LoginEventSink SPI [v0.2-new]
|
||||||
|
|
||||||
|
- [ ] SPI invocation is **asynchronous** — does not block login flow (T-UT16).
|
||||||
|
- [ ] Failing sink logs WARN but never propagates exception to the login path.
|
||||||
|
- [ ] `LoginEvent` passed to sink does not contain the password or full JWT.
|
||||||
|
|
||||||
|
### 9.4 Real mailers [v0.2-new]
|
||||||
|
|
||||||
|
- [ ] `MailInvitationMailer` fails loudly on SMTP failure (throws, does not swallow).
|
||||||
|
- [ ] Invitation email does not leak the org's internal ID — uses `OrgDisplayNameResolver`.
|
||||||
|
- [ ] Accept URL uses HTTPS (validated by config).
|
||||||
|
- [ ] `plate.auth.mail.enabled=false` (default) → logging fallback. No SMTP connection attempted.
|
||||||
|
|
||||||
|
### 9.5 RFC 7807 Problem Details [v0.2-new]
|
||||||
|
|
||||||
|
- [ ] Error responses include no stack traces, no SQL fragments, no internal class names (T-SEC13).
|
||||||
|
- [ ] `detail` field is consumer-safe (human-readable, no secrets).
|
||||||
|
- [ ] `type` URIs are stable and documented.
|
||||||
|
- [ ] Problem Details responses set `Content-Type: application/problem+json`.
|
||||||
|
- [ ] Login-failure Problem Detail says "invalid credentials" — no user-exists leak.
|
||||||
|
|
||||||
|
### 9.6 Wire-version [v0.2-new] (only if bumped)
|
||||||
|
|
||||||
|
- [ ] If `WIRE_VERSION` bumped to 2: backend rejects version-1 envelopes with a clear error.
|
||||||
|
- [ ] If bumped: both consumers upgrade lockstep per versioning policy.
|
||||||
|
- [ ] If not bumped: no action needed — v0.2 is additive.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Rollout plan
|
||||||
|
|
||||||
|
### 10.1 Consumer upgrade path
|
||||||
|
|
||||||
|
v0.2.0 is **additive** (assuming no wire-version bump). Upgrade for each consumer:
|
||||||
|
|
||||||
|
**InspectFlow:**
|
||||||
|
1. Change `pom.xml`: `plate-auth-starter` version `0.1.0` → `0.2.0`
|
||||||
|
2. Change `package.json`: `@platesoft/auth` version `0.1.0` → `0.2.0`
|
||||||
|
3. Enable MS Entra: `plate.auth.providers.microsoft.enabled=true` + MS client-id/secret env vars
|
||||||
|
4. Enable mail: `plate.auth.mail.enabled=true` + SMTP config
|
||||||
|
5. Optionally: configure `plate.auth.invitation.expiration-days`
|
||||||
|
6. Optionally: add a `LoginEventSink` bean to tee events to Loki/SIEM
|
||||||
|
7. Frontend: update types — `useMemberships()` now returns `Membership[]`
|
||||||
|
8. Run full E2E suite — must pass
|
||||||
|
|
||||||
|
**Sparkboard:**
|
||||||
|
1. Same version bumps
|
||||||
|
2. Enable email magic-link if needed: `plate.auth.providers.email-magic-link.enabled=true` + SMTP
|
||||||
|
3. Optionally: import Zod schemas for runtime validation
|
||||||
|
4. Optionally: use the default `MagicLinkCallback` component
|
||||||
|
|
||||||
|
### 10.2 Rollback strategy
|
||||||
|
|
||||||
|
v0.2.0 rollback is **safe** — no Flyway migration is added (prediction). If a consumer needs to
|
||||||
|
revert to v0.1.0:
|
||||||
|
1. Revert dependency versions in `pom.xml` + `package.json`
|
||||||
|
2. Redeploy
|
||||||
|
3. Database is unaffected (no schema change)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Acceptance criteria
|
||||||
|
|
||||||
|
| # | Criterion | How verified |
|
||||||
|
|---|---|---|
|
||||||
|
| B1 | MS Entra ID provider works end-to-end | T-IT10 + T-E2E07 |
|
||||||
|
| B2 | Email magic-link provider works end-to-end | T-FE07 + manual test |
|
||||||
|
| B3 | `LoginEventSink` SPI fires on login events | T-UT16 |
|
||||||
|
| B4 | Real `InvitationMailer` sends email | T-UT17 + T-IT11 |
|
||||||
|
| B5 | RFC 7807 Problem Details on `/api/auth/*` | T-UT19 + T-IT12 |
|
||||||
|
| B6 | Configurable invitation expiration works | T-UT20 |
|
||||||
|
| B7 | TS types + Zod schemas exported | T-FE08 + T-FE09 |
|
||||||
|
| B8 | Edge-runtime safe `useAccessToken()` | T-FE10 |
|
||||||
|
| B9 | Both artifacts published at 0.2.0 | `mvn dependency:get` + `npm view` |
|
||||||
|
| B10 | All v0.1 tests still pass (no regression) | Full `mvn verify` + `pnpm test` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Items deferred to v0.3+
|
||||||
|
|
||||||
|
- Multi-replica nonce store (Redis/Postgres UPSERT)
|
||||||
|
- Refresh-token rotation table + family-tracking (T-SEC10 from v0.1)
|
||||||
|
- JWT secret rotation via JWK Set + `kid` header
|
||||||
|
- Session sliding vs absolute expiration toggle
|
||||||
|
- `requireRoles(['ADMIN'])` middleware helper
|
||||||
|
- WebAuthn / passkey provider (if consumer demand emerges)
|
||||||
|
- Per-app branding on exchange endpoint
|
||||||
|
- Account lockout after N failed logins
|
||||||
|
- 2FA / TOTP
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Cross-references
|
||||||
|
|
||||||
|
- Assessment: [`Sprint-1-Assessment.md`](Sprint-1-Assessment.md)
|
||||||
|
- Test plan: [`Sprint-1-Testplan.md`](Sprint-1-Testplan.md)
|
||||||
|
- v0.1 plan: [`Sprint-0-Plan.md`](Sprint-0-Plan.md)
|
||||||
|
- Architecture: [`Architecture.md`](Architecture.md)
|
||||||
|
- Roadmap: [`Roadmap.md`](Roadmap.md) (v0.2 section)
|
||||||
|
- Open questions: [`Open-Questions.md`](Open-Questions.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*End of plan v1. Ready for Plan Reviewer.*
|
||||||
+382
@@ -0,0 +1,382 @@
|
|||||||
|
# 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](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](Sprint-0-Testplan.md),
|
||||||
|
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](Sprint-1-Plan.md))
|
||||||
|
|
||||||
|
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):**
|
||||||
|
- `ExchangeHmacInvalidException` → `ProblemDetail` with `status=401`,
|
||||||
|
`type=https://plate-software.de/errors/exchange-hmac-invalid`, `title="Exchange HMAC invalid"`
|
||||||
|
- `ExchangeReplayException` → `status=409`, `type=.../exchange-replay`
|
||||||
|
- `ExchangeExpiredException` → `status=401`, `type=.../exchange-expired`
|
||||||
|
- `OrgValidationException` → `status=400`, `type=.../org-validation-failed`
|
||||||
|
- `BadCredentialsException` → `status=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=3` → `InvitationService.create(...)` produces
|
||||||
|
`expires_at ≈ now + 3 days` (±2s)
|
||||||
|
- `plate.auth.invitation.expiration-days=14` → `expires_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
|
||||||
|
|
||||||
|
### T-SEC11 — Email enumeration guard (magic-link)
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
### 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 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:
|
||||||
|
```ts
|
||||||
|
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](Sprint-1-Plan.md):
|
||||||
|
|
||||||
|
| 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](Sprint-0-Testplan.md)):
|
||||||
|
|
||||||
|
| 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](Sprint-1-Plan.md) — implementation plan
|
||||||
|
- [Sprint-1-Assessment.md](Sprint-1-Assessment.md) — priority classification + risks
|
||||||
|
- [Sprint-0-Testplan.md](Sprint-0-Testplan.md) — v0.1 test plan (regression baseline)
|
||||||
|
- [Architecture.md](Architecture.md) — SPI model, wire contract
|
||||||
|
- [Roadmap.md](Roadmap.md) — v0.2 scope
|
||||||
|
- [Open-Questions.md](Open-Questions.md) — Q02, Q04, Q12 deferrals
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**End of Sprint-1-Testplan.md (v1).**
|
||||||
Reference in New Issue
Block a user