diff --git a/Sprint-1-Testplan.md b/Sprint-1-Testplan.md new file mode 100644 index 0000000..12f1c78 --- /dev/null +++ b/Sprint-1-Testplan.md @@ -0,0 +1,375 @@ +# Sprint 1 Testplan — "Spark" + +**Status:** Draft v1 +**Sprint:** 1 — "Spark" +**Date:** 2026-06-24 +**Owner:** Patrick + Roo-Planner +**Basis:** [Sprint-1-Plan](Sprint-1-Plan.md) (chunks 1–4) + [Sprint-1-Assessment](Sprint-1-Assessment.md) + +--- + +## 1. Scope + +This testplan covers the **walking-skeleton MVP** for Sparkboard: plate-auth wire-up, single Idea entity CRUD-list, PWA install, and Gitea-Actions deploy to TrueNAS. Tests are organised by type: + +- **UT** — Unit tests (JUnit 5 + Mockito on backend, Vitest on frontend) +- **IT** — Integration tests (`@SpringBootTest` with Testcontainers Postgres on backend) +- **E2E** — End-to-end tests (Playwright against `docker-compose up`) +- **MT** — Manual tests (executed by Patrick on real hardware or on `https://sparkboard.plate-software.de`) + +Tests **not** in scope: plate-auth's own internals (those are covered by the plate-auth library's own test suite). Sparkboard's testplan only verifies the contract surface and Sparkboard-specific code. + +--- + +## 2. Test Overview + +| ID | Title | Type | Acceptance | +|----|-------|------|-----------| +| UT-01 | `IdeaService.create` persists with default status RAW and timestamps | UT | A4 | +| UT-02 | `IdeaService.listForOrg` returns newest first | UT | A4 | +| UT-03 | `IdeaController` rejects empty title (400) | UT | A4 | +| UT-04 | `IdeaController` rejects title > 200 chars (400) | UT | A4 | +| UT-05 | `IdeaController` injects `@CurrentUser` as author | UT | A4 | +| UT-06 | `SparkboardOnboardingHook` writes ADMIN row when user email is in `sparkboard.admins[]` | UT | A3 | +| UT-07 | `SparkboardOnboardingHook` writes MEMBER row when user email is NOT in `sparkboard.admins[]` | UT | A3 | +| UT-08 | `SparkboardOnboardingHook` is idempotent (running twice = one row) | UT | A3 | +| UT-09 | `SparkboardAdminProperties` binds `sparkboard.admins[]` from YAML | UT | A3 | +| UT-10 | Frontend `lib/api.ts` forwards cookies on server-component calls | UT | A4 | +| IT-01 | Sparkboard Flyway migrations run cleanly on empty DB | IT | A3, A4 | +| IT-02 | Sparkboard + plate-auth migrations coexist (two history tables) | IT | A3 | +| IT-03 | `POST /api/ideas` → 200 with authenticated context, idea persisted | IT | A4 | +| IT-04 | `GET /api/ideas` → 200 with `[idea]` returned for the org | IT | A4 | +| IT-05 | `POST /api/ideas` → 401 without auth | IT | A4 | +| IT-06 | `OnboardingHook` triggers on first plate-auth login (integration with real `MembershipService`) | IT | A3 | +| E2E-01 | Allowlisted user → sign-in → `/ideas` happy path | E2E | A1, A4 | +| E2E-02 | Non-allowlisted user → sign-in attempt → rejection screen | E2E | A2 | +| E2E-03 | `/manifest.json` serves valid PWA manifest with `theme_color #ea580c` | E2E | A5 | +| E2E-04 | `/sw.js` registers without console errors | E2E | A5 | +| E2E-05 | Logged-in user creates idea → redirected to `/ideas` → idea visible | E2E | A4 | +| E2E-06 | Two different allowlisted users see each other's ideas (shared org) | E2E | A4 | +| MT-01 | iPhone Safari → "Add to Home Screen" → standalone PWA launches | MT | A5 | +| MT-02 | Android Chrome → install prompt → standalone PWA launches | MT | A5 | +| MT-03 | 5th account (off allowlist) sign-in attempt produces visible error | MT | A2 | +| MT-04 | `git push origin main` triggers `.gitea/workflows/deploy.yml` → green | MT | A6 | +| MT-05 | `smoke-test.sh` against `https://sparkboard.plate-software.de` exits 0 | MT | A6 | +| MT-06 | All 4 humans on allowlist can sign in over the course of one day | MT | A1, A3 | +| MT-07 | README quickstart works on a fresh `git clone` on a clean machine | MT | DoD §10.5 | + +**Total: 27 test cases** (10 UT + 6 IT + 6 E2E + 7 MT). + +--- + +## 3. Unit Tests (Backend) + +### UT-01 — IdeaService.create persists with default status and timestamps + +- **Class:** `de.plate.sparkboard.idea.IdeaServiceTest` +- **Setup:** Mock `IdeaRepository.save` to return its input. +- **Given:** `CreateIdeaRequest("My idea", "Body", null)` and a known `userId`/`orgId`. +- **When:** `ideaService.create(request, userId, orgId)` +- **Then:** + - Returned `Idea` has `status = IdeaStatus.RAW`. + - `createdAt` is set (within last 1 sec). + - `updatedAt == createdAt`. + - `authorId == userId`, `orgId == orgId`. + - `IdeaRepository.save` called exactly once. + +### UT-02 — IdeaService.listForOrg returns newest first + +- **Given:** Repo returns 3 ideas with `createdAt` of T-2h, T-1h, T-0. +- **When:** `ideaService.listForOrg(orgId)` +- **Then:** Result list is `[T-0, T-1h, T-2h]` (newest first). + +### UT-03 — IdeaController rejects empty title (400) + +- **Class:** `IdeaControllerTest` using `@WebMvcTest`. +- **When:** `POST /api/ideas` with body `{"title": "", "body": null}` and valid auth. +- **Then:** HTTP 400, validation error mentions `title`. + +### UT-04 — IdeaController rejects title > 200 chars (400) + +- **When:** `POST /api/ideas` with `title = "x".repeat(201)`. +- **Then:** HTTP 400, validation error mentions `size` or `length`. + +### UT-05 — IdeaController injects @CurrentUser as author + +- **When:** Authenticated POST with `userId = U` and `FAMILY_SPARK_ID`. +- **Then:** Captured `IdeaService.create` argument has `authorId == U`. + +### UT-06 — SparkboardOnboardingHook ADMIN path + +- **Class:** `de.plate.sparkboard.auth.SparkboardOnboardingHookTest` +- **Given:** `sparkboard.admins[] = ["patrick@plate-software.de"]`, login event for `patrick@plate-software.de`. +- **When:** `hook.onFirstLogin(authenticatedUser)` +- **Then:** `MembershipService.upsert(userId, "SPARK_ORG", FAMILY_SPARK_ID, "ADMIN")` called once. + +### UT-07 — SparkboardOnboardingHook MEMBER path + +- **Given:** `sparkboard.admins[] = ["patrick@plate-software.de"]`, login event for `kid@example.com`. +- **Then:** `MembershipService.upsert(userId, "SPARK_ORG", FAMILY_SPARK_ID, "MEMBER")` called once. + +### UT-08 — Hook idempotency + +- **When:** `hook.onFirstLogin(user)` called twice with same user. +- **Then:** `MembershipService.upsert` called twice with identical arguments; no exception. (`upsert` semantics, not duplicate insert.) + +### UT-09 — SparkboardAdminProperties binding + +- **Class:** `SparkboardAdminPropertiesTest` using `ApplicationContextRunner`. +- **Given:** YAML `sparkboard.admins: [a@x.de, b@y.de]`. +- **Then:** `properties.admins() == List.of("a@x.de", "b@y.de")`. + +--- + +## 4. Unit Tests (Frontend) + +### UT-10 — lib/api.ts forwards cookies on server-component calls + +- **Tool:** Vitest + `next/headers` mock. +- **Given:** `cookies()` returns `[{name: "next-auth.session-token", value: "abc"}]`. +- **When:** `listIdeas()` is called from a server component. +- **Then:** The underlying `fetch` is called with `headers.Cookie === "next-auth.session-token=abc"`. + +--- + +## 5. Integration Tests (Backend) + +### IT-01 — Sparkboard Flyway migrations run cleanly + +- **Class:** `SparkboardFlywayMigrationTest` using Testcontainers Postgres. +- **When:** Spring Boot starts with `spring.flyway.locations=classpath:db/migration`. +- **Then:** + - `flyway_schema_history` table exists. + - `spark_org` table exists with 1 seeded row (`'00000000-0000-0000-0000-000000000001'`, name = "Family Spark"). + - `ideas` table exists with all columns from V1. + - No errors in startup log. + +### IT-02 — Sparkboard + plate-auth migrations coexist + +- **Class:** `DualFlywayHistoryTest`. +- **Setup:** Boot with both `plate-auth-starter` (config: `plate.auth.flyway.history-table = flyway_schema_history_auth`) and Sparkboard migrations. +- **Then:** + - Both `flyway_schema_history` AND `flyway_schema_history_auth` tables exist. + - Migrations from both projects applied without collision. + - `auth_identities` (plate-auth) and `ideas` (Sparkboard) both queryable. + +### IT-03 — POST /api/ideas → 200 with authenticated context + +- **Class:** `IdeaApiIntegrationTest` using `@SpringBootTest(WebEnvironment.RANDOM_PORT)` + a stubbed JWT principal. +- **Given:** Mock authenticated user with `userId = U`. +- **When:** `POST /api/ideas { "title": "T", "body": "B" }`. +- **Then:** + - HTTP 200, body is the created `IdeaDto`. + - Postgres now contains one `ideas` row with `author_id = U`, `org_id = FAMILY_SPARK_ID`, `title = "T"`. + +### IT-04 — GET /api/ideas → 200 with `[idea]` + +- **Given:** One `ideas` row seeded with `org_id = FAMILY_SPARK_ID`. +- **When:** `GET /api/ideas` with auth. +- **Then:** HTTP 200, body is a JSON array of length 1 with matching `title`. + +### IT-05 — POST /api/ideas → 401 without auth + +- **When:** `POST /api/ideas` with no `Authorization` header and no session cookie. +- **Then:** HTTP 401, no DB write. + +### IT-06 — OnboardingHook triggers on first plate-auth login (real wiring) + +- **Class:** `OnboardingHookIntegrationTest`. +- **Setup:** Boot full Spring context with `SparkboardOnboardingHook` registered as a bean. +- **When:** Call `AuthService.handleFirstLogin(authenticatedUser)` (the plate-auth integration point). +- **Then:** + - `memberships` table contains a row with `(user_id, 'SPARK_ORG', FAMILY_SPARK_ID, 'MEMBER')`. + - Second invocation does not add a duplicate row. + +--- + +## 6. End-to-End Tests (Playwright) + +> **Target:** `docker-compose up` in CI; smoke-tested against TrueNAS deploy. + +### E2E-01 — Allowlisted user → sign-in → `/ideas` + +- **Steps:** + 1. Navigate to `/`. + 2. Click "Sign in with Google". + 3. Use a mocked OAuth callback OR a real test-google account on allowlist. + 4. Land on `/ideas`. +- **Assert:** URL is `/ideas`; page renders `

Ideas

`; "New idea" link visible. + +### E2E-02 — Non-allowlisted user → rejection + +- **Steps:** Same flow with a Google account whose email is NOT in plate-auth's allowlist. +- **Assert:** Redirected to `/login?error=access_denied` (or equivalent) with a visible message; NO `memberships` row created in DB. + +### E2E-03 — `/manifest.json` is valid + +- **Steps:** `GET /manifest.json`. +- **Assert:** + - HTTP 200, `content-type: application/manifest+json` (or `application/json`). + - JSON parses cleanly. + - Contains `name: "Sparkboard"`, `theme_color: "#ea580c"`, `display: "standalone"`. + +### E2E-04 — `/sw.js` registers cleanly + +- **Steps:** Navigate to `/`; wait for service worker registration via `navigator.serviceWorker.ready`. +- **Assert:** + - No console errors with severity error. + - `navigator.serviceWorker.controller` is non-null OR the registration promise resolved. + +### E2E-05 — Create idea happy path + +- **Steps:** + 1. Auth as allowlisted user. + 2. Click "New idea" → `/ideas/new`. + 3. Fill title `"E2E test"`, body `"E2E body"`, submit. + 4. Redirected to `/ideas`. +- **Assert:** `/ideas` shows `
  • ` (or similar) containing `"E2E test"`. + +### E2E-06 — Two users share an org + +- **Steps:** + 1. User A creates idea `"From A"`. + 2. User B (different allowlisted account) loads `/ideas`. +- **Assert:** User B sees `"From A"` in the list (proves shared org via `FAMILY_SPARK_ID`). + +--- + +## 7. Manual Tests + +### MT-01 — iPhone Safari install + +- **Device:** Patrick's iPhone. +- **Steps:** + 1. Safari → `https://sparkboard.plate-software.de`. + 2. Share menu → "Add to Home Screen". + 3. Confirm icon + name (`Sparkboard`). + 4. Launch from home screen. +- **Assert:** App launches in standalone mode (no Safari chrome); icon is Sparkboard's; title bar reflects the app. + +### MT-02 — Android Chrome install + +- **Device:** Any Android device. +- **Steps:** + 1. Chrome → `https://sparkboard.plate-software.de`. + 2. Accept "Install app" banner OR menu → "Install app". + 3. Launch from launcher. +- **Assert:** Same as MT-01 (standalone, correct icon). + +### MT-03 — 5th account off-allowlist rejection (visible error) + +- **Steps:** Sign in with a Google account NOT on the allowlist. +- **Assert:** Plain-language rejection message; **not** a 500 stack trace; user can navigate back to `/login`. + +### MT-04 — Gitea Actions deploy on `main` + +- **Steps:** + 1. Commit a trivial change to `main` (e.g., README typo fix). + 2. Push. + 3. Watch `.gitea/workflows/deploy.yml` run. +- **Assert:** Workflow run shows ✅ Success; `https://sparkboard.plate-software.de/` serves the latest commit. + +### MT-05 — Smoke test + +- **Steps:** Run `deploy/smoke-test.sh https://sparkboard.plate-software.de` from Patrick's laptop. +- **Assert:** Exit code 0; outputs confirmations for `/api/health`, `/login`, `/manifest.json`, `/sw.js`. + +### MT-06 — All 4 humans sign in within one day + +- **Steps:** All 4 family members sign in over a 24-hour window. +- **Assert:** `memberships` table contains 4 rows for `(SPARK_ORG, FAMILY_SPARK_ID)`. No errors in backend logs. + +### MT-07 — README quickstart on fresh clone + +- **Steps:** + 1. `git clone ` on a clean machine (or after `docker system prune` + cleaned `~/.m2`). + 2. Follow `README.md` step-by-step. + 3. Reach the point where `http://localhost:3000` shows the login page. +- **Assert:** No undocumented steps required. Any deviation = README fix before Sprint 1 closes. + +--- + +## 8. Test Data + +### 8.1 Production seed (V2) + +- One row in `spark_org`: `('00000000-0000-0000-0000-000000000001', 'Family Spark', 'SPARK_ORG', now())`. +- No seed for `ideas` in production. +- `memberships` rows created lazily via `SparkboardOnboardingHook` on first login. + +### 8.2 Dev seed (R__dev_seed_ideas.sql, dev profile only) + +- 5 dev-only ideas with placeholder text. +- Skipped in `prod` profile via Flyway placeholders or profile-conditional location. + +### 8.3 Test fixtures + +- `IdeaTestBuilder` (in `src/test/java`) for `@WebMvcTest` and `@SpringBootTest`. +- Playwright auth state: pre-authenticated session cookie OR mocked OAuth callback. + +--- + +## 9. Environments + +| Env | URL | Purpose | +|-----|-----|---------| +| Local dev | `http://localhost:3000` + `http://localhost:8080` | UT, IT, E2E | +| Gitea Actions CI | ephemeral docker-compose | UT, IT, E2E | +| TrueNAS production | `https://sparkboard.plate-software.de` | MT-01..MT-07 | + +--- + +## 10. Coverage Mapping + +Cross-check that every acceptance criterion has at least 2 covering tests (so a single test failure can't claim acceptance): + +| Acceptance | Covered by | +|-----------|-----------| +| A1 — Allowlisted sign-in → `/ideas` | E2E-01, MT-06 | +| A2 — Non-allowlisted rejected | E2E-02, MT-03 | +| A3 — Membership auto-created | UT-06, UT-07, UT-08, IT-06, MT-06 | +| A4 — Idea CRUD-list | UT-01, UT-02, UT-03, UT-04, UT-05, IT-03, IT-04, IT-05, E2E-05, E2E-06 | +| A5 — PWA installable | E2E-03, E2E-04, MT-01, MT-02 | +| A6 — Gitea Actions deploy | MT-04, MT-05 | + +**Coverage check:** ✅ Every A* has ≥ 2 tests. No orphan acceptances. + +--- + +## 11. Out of Scope (Deferred) + +| Item | Reason | Lands in | +|------|--------|----------| +| Edit / delete idea | Not in Sprint 1 plan | Sprint 2 testplan | +| Idea detail page | Not in Sprint 1 plan | Sprint 2 testplan | +| Status workflow tests | Status field is RAW-default-only in S1 | Sprint 2 testplan | +| Reactions, comments | Sprint 2/3 features | Sprint 2/3 testplan | +| Offline sync tests | sw.js is stub-only | Sprint 4 testplan | +| Push notification tests | Sprint 4 feature | Sprint 4 testplan | +| Native APK tests | Sprint 5 (Capacitor) | Sprint 5 testplan | +| Load / perf tests | 4 users, no SLA | Never (until Sparkboard scales beyond family) | +| Multi-org tests | Sparkboard is single-org by design | Never | + +--- + +## 12. Open Questions (Test-Specific) + +| ID | Question | Impact | +|----|----------|--------| +| QT-01 | Should E2E-01 use a real Google test account or a mocked OAuth callback? | If real, requires a Google test project. If mocked, doesn't validate the full plate-auth chain. **Lean: mocked in CI, real in MT-06.** | +| QT-02 | Do we run E2E in Gitea Actions or only locally? | Gitea Actions adds CI time but catches deploy regressions. **Lean: yes in CI.** | +| QT-03 | Should `R__dev_seed_ideas.sql` be guarded by a Flyway placeholder so it never runs in prod? | Yes — Flyway placeholder `${env}` or separate `db/migration-dev/` location. **Lean: separate dev-only location in `application-dev.yml`.** | + +--- + +## 13. Cross-references + +- [Sprint-1-Plan](Sprint-1-Plan.md) — chunks 1–4 +- [Sprint-1-Assessment](Sprint-1-Assessment.md) — acceptance A1–A6 +- [Architecture](Architecture.md) — system diagrams +- [Open-Questions](Open-Questions.md) — Q01–Q10 +- [Integration-Guide](Integration-Guide.md) — plate-auth wire-up walkthrough + +--- + +_End of Sprint 1 Testplan. Status: **Draft v1, awaiting GO from Patrick.**_