docs(sprint-1-testplan): 27 test cases across UT/IT/E2E/MT mapped to A1-A6
+375
@@ -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 `<h1>Ideas</h1>`; "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 `<li>` (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 <sparkboard repo>` 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.**_
|
||||
Reference in New Issue
Block a user