2
Sprint 1 Testplan
Patrick Plate edited this page 2026-06-24 15:28:48 +02:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Sprint 1 Testplan — "Spark"

Status: Draft v1 Sprint: 1 — "Spark" Date: 2026-06-24 Owner: Patrick + Roo-Planner Basis: Sprint-1-Plan (chunks 14) + Sprint-1-Assessment


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.onboarding.SparkboardOnboardingHookTest
  • Given: sparkboard.admins[] = ["patrick@plate-software.de"], login event for patrick@plate-software.de.
  • When: hook.onFirstSignIn(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.onFirstSignIn(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


End of Sprint 1 Testplan. Status: Draft v1, awaiting GO from Patrick.