diff --git a/docs/scrum4me-test-plan.md b/docs/scrum4me-test-plan.md new file mode 100644 index 0000000..a200ed9 --- /dev/null +++ b/docs/scrum4me-test-plan.md @@ -0,0 +1,454 @@ +# Scrum4Me — API Test Plan + +**Versie:** 1.0 +**Datum:** 25 april 2026 +**Auteur:** Jan Peter Visser +**Status:** Draft + +--- + +## 1. Introduction + +This document describes the test plan and test planning for the Scrum4Me API endpoints — the integration surface that Claude Code and external agents use to interact with the application. It covers strategy, scope, test cases, tooling, exit criteria, and a phased execution schedule. + +The API consists of 7 endpoints that form the Definition of Done checkpoint: all 7 must pass curl-level verification before the project ships. + +--- + +## 2. Objectives + +| # | Objective | +|---|---| +| O-1 | Verify that all 7 API endpoints return correct responses for valid input | +| O-2 | Verify that unauthenticated requests are rejected with 401 | +| O-3 | Verify that demo users cannot perform write operations (403) | +| O-4 | Verify that cross-user access is impossible at every endpoint | +| O-5 | Verify that all Zod validation schemas reject malformed input with 400 | +| O-6 | Verify edge cases: resource-not-found (404), empty result sets, boundary values | +| O-7 | Produce executable curl scripts that satisfy the DoD requirement | + +--- + +## 3. Scope + +### 3.1 In scope + +| Endpoint | Method | Auth type | Write | Demo check | +|---|---|---|---|---| +| `/api/products` | GET | Bearer token | No | No | +| `/api/products/:id/next-story` | GET | Bearer token | No | No | +| `/api/sprints/:id/tasks` | GET | Bearer token | No | No | +| `/api/stories/:id/tasks/reorder` | PATCH | Bearer token | Yes | Yes | +| `/api/stories/:id/log` | POST | Bearer token | Yes | Yes | +| `/api/tasks/:id` | PATCH | Bearer token | Yes | Yes | +| `/api/todos` | POST | Bearer token | Yes | Yes | + +### 3.2 Out of scope + +- `/api/profile/avatar` (GET/POST) — session-cookie auth, separate concern +- Server Actions (`actions/*.ts`) — UI-layer, covered by separate acceptance testing +- Frontend components and pages +- Database migrations and schema changes +- Performance and load testing + +--- + +## 4. Test Strategy + +### 4.1 Layers + +The strategy uses two complementary test layers. Together they satisfy both the automated regression requirement and the DoD curl requirement. + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Layer 1 — Vitest Unit Tests (automated, mocked) │ +│ Fast, deterministic, no external DB required │ +│ Covers: auth, demo block, cross-user isolation, input validation │ +├─────────────────────────────────────────────────────────────────────┤ +│ Layer 2 — Curl Scripts (manual, real DB) │ +│ Executable against localhost:3000 with seeded test data │ +│ Covers: happy paths, response shape, DoD compliance │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +No Playwright, no Jest — Vitest is already configured and the existing `__tests__/api/security.test.ts` establishes the mock pattern to follow. + +### 4.2 Vitest approach + +All unit tests mock two dependencies only: + +```typescript +vi.mock('@/lib/prisma', () => ({ + prisma: { /* model methods as vi.fn() */ } +})) + +vi.mock('@/lib/api-auth', () => ({ + authenticateApiRequest: vi.fn() +})) +``` + +This approach: +- Tests route handler logic in isolation +- Keeps tests fast (no network/DB) +- Follows the pattern already validated in `security.test.ts` +- Does not fall into the mock/prod divergence trap because the mocked boundary (`authenticateApiRequest` + `prisma`) is stable and narrow + +### 4.3 Curl approach + +A single shell script `scripts/test-api.sh` with: +- A `TOKEN` variable set at the top (obtained from a seeded user via the UI) +- One function per endpoint, each printing pass/fail based on HTTP status +- Run order that follows the Lars flow (read → write → verify) + +### 4.4 Test data + +The existing seed (`prisma/seed.ts`) creates: +- `lars` — full-permission user, used as the primary test actor +- `demo` — read-only user, used for demo-block tests + +A second regular user (`tester`) must be created manually (or added to the seed) to test cross-user isolation scenarios. + +--- + +## 5. Test Cases + +The following tables list every test case by endpoint. Each case has an ID, description, input, expected HTTP status, and which layer covers it. + +### TC format + +| Field | Meaning | +|---|---| +| ID | Unique identifier, e.g. `TC-P-01` (P = products) | +| Layer | V = Vitest, C = Curl | +| Input | What is sent | +| Expected | HTTP status + key response fields | + +--- + +### 5.1 GET /api/products + +| ID | Layer | Scenario | Input | Expected | +|---|---|---|---|---| +| TC-P-01 | V | No token | No Authorization header | 401 | +| TC-P-02 | V | Invalid token | `Bearer invalid123` | 401 | +| TC-P-03 | V | Revoked token | Valid hash but `revoked_at` set | 401 | +| TC-P-04 | V | Valid token, owns 2 products | Valid token, 2 products in DB | 200, array of 2 | +| TC-P-05 | V | Valid token, is team member | Valid token, member of product owned by other user | 200, includes that product | +| TC-P-06 | V | Valid token, no products | Valid token, no products in DB | 200, empty array | +| TC-P-07 | V | Archived products excluded | 1 active + 1 archived product | 200, array of 1 | +| TC-P-08 | V | Cross-user: other user's products not returned | Token for user A, products owned by user B | 200, empty array | +| TC-P-09 | C | Happy path (lars) | Lars' token | 200, ≥1 product | + +--- + +### 5.2 GET /api/products/:id/next-story + +| ID | Layer | Scenario | Input | Expected | +|---|---|---|---|---| +| TC-NS-01 | V | No token | No Authorization header | 401 | +| TC-NS-02 | V | Invalid token | `Bearer invalid` | 401 | +| TC-NS-03 | V | Product not found / not accessible | Valid token, unknown product id | 404 | +| TC-NS-04 | V | No active sprint | Valid token, product with no ACTIVE sprint | 404 | +| TC-NS-05 | V | Active sprint, no IN_SPRINT stories | Valid token, sprint exists but 0 stories | 404 | +| TC-NS-06 | V | Returns highest-priority story | Valid token, 3 IN_SPRINT stories with tasks | 200, story with tasks array | +| TC-NS-07 | V | Cross-user: other user's product | Token for user A, product owned by user B | 404 | +| TC-NS-08 | C | Happy path (lars, active sprint) | Lars' token + DevPlanner product id | 200, story object | + +--- + +### 5.3 GET /api/sprints/:id/tasks?limit=10 + +| ID | Layer | Scenario | Input | Expected | +|---|---|---|---|---| +| TC-ST-01 | V | No token | No Authorization header | 401 | +| TC-ST-02 | V | Invalid token | `Bearer invalid` | 401 | +| TC-ST-03 | V | Sprint not found / not accessible | Valid token, unknown sprint id | 404 | +| TC-ST-04 | V | Cross-user: other user's sprint | Token for user A, sprint in user B's product | 404 | +| TC-ST-05 | V | Default limit applied | No `?limit` param | 200, ≤10 tasks | +| TC-ST-06 | V | Custom limit respected | `?limit=3`, sprint has 5 tasks | 200, exactly 3 tasks | +| TC-ST-07 | V | Limit boundary: limit=1 | Sprint has multiple tasks | 200, exactly 1 task | +| TC-ST-08 | V | Sprint with 0 tasks | Valid sprint, no tasks | 200, empty array | +| TC-ST-09 | C | Happy path (lars) | Lars' token + active sprint id + `?limit=10` | 200, tasks array | + +--- + +### 5.4 PATCH /api/stories/:id/tasks/reorder + +| ID | Layer | Scenario | Input | Expected | +|---|---|---|---|---| +| TC-RO-01 | V | No token | No Authorization header | 401 | +| TC-RO-02 | V | Invalid token | `Bearer invalid` | 401 | +| TC-RO-03 | V | Demo user | Valid token, `isDemo: true` | 403 | +| TC-RO-04 | V | Story not found / not accessible | Valid token, unknown story id | 404 | +| TC-RO-05 | V | Cross-user: other user's story | Token for user A, story in user B's product | 404 | +| TC-RO-06 | V | Empty task_ids array | `{ "task_ids": [] }` | 400 | +| TC-RO-07 | V | task_ids not array | `{ "task_ids": "abc" }` | 400 | +| TC-RO-08 | V | task_ids contains IDs from different story | Valid token, mixed-story task IDs | 400 | +| TC-RO-09 | V | Happy path | Valid token + valid task_ids in new order | 200 | +| TC-RO-10 | C | Happy path (lars) | Lars' token + story id + ordered task ids | 200 | + +--- + +### 5.5 POST /api/stories/:id/log + +| ID | Layer | Scenario | Input | Expected | +|---|---|---|---|---| +| TC-L-01 | V | No token | No Authorization header | 401 | +| TC-L-02 | V | Invalid token | `Bearer invalid` | 401 | +| TC-L-03 | V | Demo user | Valid token, `isDemo: true` | 403 | +| TC-L-04 | V | Story not found / not accessible | Valid token, unknown story id | 404 | +| TC-L-05 | V | Cross-user: other user's story | Token for user A, story in user B's product | 404 | +| TC-L-06 | V | Missing `type` field | `{ "content": "..." }` | 400 | +| TC-L-07 | V | Unknown `type` value | `{ "type": "UNKNOWN", "content": "..." }` | 400 | +| TC-L-08 | V | IMPLEMENTATION_PLAN — missing content | `{ "type": "IMPLEMENTATION_PLAN" }` | 400 | +| TC-L-09 | V | IMPLEMENTATION_PLAN — happy path | `{ "type": "IMPLEMENTATION_PLAN", "content": "Approach: ..." }` | 201 | +| TC-L-10 | V | TEST_RESULT — missing status | `{ "type": "TEST_RESULT", "content": "..." }` | 400 | +| TC-L-11 | V | TEST_RESULT — invalid status | `{ "type": "TEST_RESULT", "content": "...", "status": "UNKNOWN" }` | 400 | +| TC-L-12 | V | TEST_RESULT — happy path PASSED | `{ "type": "TEST_RESULT", "content": "...", "status": "PASSED" }` | 201 | +| TC-L-13 | V | TEST_RESULT — happy path FAILED | `{ "type": "TEST_RESULT", "content": "...", "status": "FAILED" }` | 201 | +| TC-L-14 | V | COMMIT — missing commit_hash | `{ "type": "COMMIT", "content": "...", "commit_message": "..." }` | 400 | +| TC-L-15 | V | COMMIT — missing commit_message | `{ "type": "COMMIT", "content": "...", "commit_hash": "abc1234" }` | 400 | +| TC-L-16 | V | COMMIT — happy path | `{ "type": "COMMIT", "content": "...", "commit_hash": "abc1234", "commit_message": "feat: ..." }` | 201 | +| TC-L-17 | C | IMPLEMENTATION_PLAN (lars) | Lars' token + story id + IMPLEMENTATION_PLAN body | 201 | +| TC-L-18 | C | TEST_RESULT PASSED (lars) | Lars' token + story id + TEST_RESULT body | 201 | +| TC-L-19 | C | COMMIT (lars) | Lars' token + story id + COMMIT body | 201 | + +--- + +### 5.6 PATCH /api/tasks/:id + +| ID | Layer | Scenario | Input | Expected | +|---|---|---|---|---| +| TC-T-01 | V | No token | No Authorization header | 401 | +| TC-T-02 | V | Invalid token | `Bearer invalid` | 401 | +| TC-T-03 | V | Demo user | Valid token, `isDemo: true` | 403 | +| TC-T-04 | V | Task not found | Valid token, unknown task id | 404 | +| TC-T-05 | V | Cross-user: task in other user's product | Token for user A, task in user B's product | 404 | +| TC-T-06 | V | Invalid status value | `{ "status": "UNKNOWN" }` | 400 | +| TC-T-07 | V | Empty body (no recognized fields) | `{}` | 400 | +| TC-T-08 | V | Update status only | `{ "status": "IN_PROGRESS" }` | 200 | +| TC-T-09 | V | Update implementation_plan only | `{ "implementation_plan": "Step 1: ..." }` | 200 | +| TC-T-10 | V | Update both fields | `{ "status": "DONE", "implementation_plan": "..." }` | 200 | +| TC-T-11 | V | Team member can update task | Token for team member (not owner), valid task | 200 | +| TC-T-12 | C | Update status to IN_PROGRESS (lars) | Lars' token + task id + `{ "status": "IN_PROGRESS" }` | 200 | +| TC-T-13 | C | Update status to DONE (lars) | Lars' token + task id + `{ "status": "DONE" }` | 200 | + +--- + +### 5.7 POST /api/todos + +| ID | Layer | Scenario | Input | Expected | +|---|---|---|---|---| +| TC-TD-01 | V | No token | No Authorization header | 401 | +| TC-TD-02 | V | Invalid token | `Bearer invalid` | 401 | +| TC-TD-03 | V | Demo user | Valid token, `isDemo: true` | 403 | +| TC-TD-04 | V | Missing title | `{ "product_id": "..." }` | 400 | +| TC-TD-05 | V | Empty title | `{ "title": "" }` | 400 | +| TC-TD-06 | V | Without product_id (global todo) | `{ "title": "My todo" }` | 201 | +| TC-TD-07 | V | With valid product_id | `{ "title": "My todo", "product_id": "..." }` | 201 | +| TC-TD-08 | V | With product_id not accessible to user | `{ "title": "...", "product_id": "" }` | 403 or 404 | +| TC-TD-09 | C | Happy path without product (lars) | Lars' token + `{ "title": "Test todo" }` | 201 | +| TC-TD-10 | C | Happy path with product (lars) | Lars' token + `{ "title": "...", "product_id": "..." }` | 201 | + +--- + +## 6. Test Files + +| File | Endpoints covered | Layer | +|---|---|---| +| `__tests__/api/security.test.ts` | products (GET), tasks/:id (PATCH) — already exists, extend | V | +| `__tests__/api/products.test.ts` | GET /api/products — happy paths + edge cases | V | +| `__tests__/api/next-story.test.ts` | GET /api/products/:id/next-story | V | +| `__tests__/api/sprint-tasks.test.ts` | GET /api/sprints/:id/tasks | V | +| `__tests__/api/story-log.test.ts` | POST /api/stories/:id/log | V | +| `__tests__/api/reorder.test.ts` | PATCH /api/stories/:id/tasks/reorder | V | +| `__tests__/api/tasks.test.ts` | PATCH /api/tasks/:id | V | +| `__tests__/api/todos.test.ts` | POST /api/todos | V | +| `scripts/test-api.sh` | All 7 endpoints | C | + +--- + +## 7. Exit Criteria + +The test phase is complete when all of the following are met: + +| Criterion | Target | +|---|---| +| All Vitest tests pass | `npm test` exits 0 | +| No test is skipped or pending | 0 skipped | +| All curl scripts return expected HTTP codes | 100% pass rate on `scripts/test-api.sh` | +| Demo user blocked on all 4 write endpoints | Verified via TC-RO-03, TC-L-03, TC-T-03, TC-TD-03 | +| Cross-user access impossible | Verified via TC-P-08, TC-NS-07, TC-ST-04, TC-RO-05, TC-L-05, TC-T-05, TC-TD-08 | +| Security review passed | No cross-user data leak found in any endpoint | + +--- + +## 8. Risks & Mitigations + +| Risk | Likelihood | Impact | Mitigation | +|---|---|---|---| +| Mock divergence: mocked Prisma behavior differs from real DB | Medium | High | Keep mocked boundary narrow (only `prisma.*` calls, not business logic); validate happy paths with curl against real DB | +| Seed data insufficient for cross-user tests | Medium | Medium | Add a second test user `tester` to `prisma/seed.ts` | +| API token not available during curl tests | Low | High | Document token creation step clearly in `test-api.sh` header | +| Zod schema changes break test expectations | Low | Low | Vitest tests will catch this immediately on next `npm test` run | +| Reorder scope validation not fully covered | Medium | High | TC-RO-08 explicitly tests mixed-story IDs; add to security review checklist | + +--- + +## 9. Dependencies + +- `prisma/seed.ts` must create (or document creation of) a second user for cross-user tests +- `scripts/` directory must exist before running the curl script +- A valid API token must be obtained from a seeded `lars` user session before running curl tests +- `npm test` must be runnable without a live database (all Vitest tests mock Prisma) + +--- + +--- + +# Test Planning + +## Overview + +| Phase | What | Duration | Start | End | +|---|---|---|---|---| +| P0 | Setup & shared infrastructure | 0.5 day | 2026-04-28 | 2026-04-28 | +| P1 | Security & auth layer (Vitest) | 1 day | 2026-04-29 | 2026-04-29 | +| P2 | Per-endpoint unit tests (Vitest) | 2 days | 2026-04-30 | 2026-05-01 | +| P3 | Curl scripts | 0.5 day | 2026-05-02 | 2026-05-02 | +| P4 | Review, edge cases, DoD verification | 0.5 day | 2026-05-05 | 2026-05-05 | + +Total: **4.5 days** + +--- + +## Phase 0 — Setup & Infrastructure (2026-04-28) + +**Goal:** Everything required to write and run tests is in place. + +### Tasks + +| ID | Task | Deliverable | +|---|---|---| +| P0-1 | Add `tester` user to `prisma/seed.ts` with no shared products | Updated seed file | +| P0-2 | Create `scripts/` directory with `test-api.sh` skeleton (TOKEN var, helper functions, empty cases) | `scripts/test-api.sh` | +| P0-3 | Verify `npm test` runs cleanly on `security.test.ts` | Green CI | +| P0-4 | Create `__tests__/api/` test file skeletons (empty describe blocks) for each new file | 7 new `.test.ts` files | + +**Done when:** `npm test` passes, all skeleton files exist, seed creates 3 users (demo, lars, tester). + +--- + +## Phase 1 — Security & Auth Layer (2026-04-29) + +**Goal:** All auth (401), demo-block (403), and cross-user isolation test cases pass for all endpoints. + +### Tasks + +| ID | Task | Test cases | File | +|---|---|---|---| +| P1-1 | Extend `security.test.ts` — add missing endpoints to auth/demo/cross-user coverage | TC-NS-01–03,07 / TC-ST-01–04 / TC-RO-01–05 / TC-L-01–05 / TC-TD-01–03,08 | `security.test.ts` | +| P1-2 | Verify all 401 cases return `{ error: 'Unauthorized' }` | TC-P-01–03 etc. | `security.test.ts` | +| P1-3 | Verify all 403 demo cases return `{ error: 'Niet beschikbaar in demo-modus' }` | TC-RO-03, TC-L-03, TC-T-03, TC-TD-03 | `security.test.ts` | +| P1-4 | Verify cross-user returns empty array or 404 (not 403) for read endpoints | TC-P-08, TC-NS-07, TC-ST-04 | `security.test.ts` | + +**Done when:** All P1 test cases green, 0 skipped. Security test file covers all 7 endpoints. + +--- + +## Phase 2 — Per-Endpoint Unit Tests (2026-04-30 – 2026-05-01) + +**Goal:** Happy paths, input validation, and edge cases covered per endpoint. + +### Day 1 (2026-04-30) — Read endpoints + +| ID | Task | Test cases | File | +|---|---|---|---| +| P2-1 | `products.test.ts` — happy paths, empty result, archived filter | TC-P-04–07,09 | `products.test.ts` | +| P2-2 | `next-story.test.ts` — happy path, no sprint, no stories | TC-NS-04–06,08 | `next-story.test.ts` | +| P2-3 | `sprint-tasks.test.ts` — happy path, limit param, empty sprint | TC-ST-05–09 | `sprint-tasks.test.ts` | + +### Day 2 (2026-05-01) — Write endpoints + +| ID | Task | Test cases | File | +|---|---|---|---| +| P2-4 | `story-log.test.ts` — all 3 log types, each field validation | TC-L-06–16 | `story-log.test.ts` | +| P2-5 | `reorder.test.ts` — happy path, empty array, mixed-story IDs | TC-RO-06–09 | `reorder.test.ts` | +| P2-6 | `tasks.test.ts` — status update, plan update, both, invalid status, team member access | TC-T-06–11 | `tasks.test.ts` | +| P2-7 | `todos.test.ts` — with/without product_id, empty title | TC-TD-04–07 | `todos.test.ts` | + +**Done when:** All Vitest files pass, `npm test` exits 0 with ≥60 test cases across all files. + +--- + +## Phase 3 — Curl Scripts (2026-05-02) + +**Goal:** `scripts/test-api.sh` covers all 7 endpoints and all curl test cases pass against localhost. + +### Tasks + +| ID | Task | Test cases | | +|---|---|---|---| +| P3-1 | Implement curl script for GET endpoints (products, next-story, sprint-tasks) | TC-P-09, TC-NS-08, TC-ST-09 | | +| P3-2 | Implement curl script for write endpoints (reorder, log ×3, tasks ×2, todos ×2) | TC-RO-10, TC-L-17–19, TC-T-12–13, TC-TD-09–10 | | +| P3-3 | Add negative cases to curl script: no token (401), demo token (403) | TC-P-01, TC-TD-03 | | +| P3-4 | Run full script against seeded local DB, fix any failures | All C cases | | +| P3-5 | Document token acquisition steps in `scripts/README.md` or script header | | | + +**Done when:** `bash scripts/test-api.sh` prints all PASS, no failures. + +--- + +## Phase 4 — Review & DoD Verification (2026-05-05) + +**Goal:** Test suite is complete, DoD exit criteria are all met. + +### Tasks + +| ID | Task | +|---|---| +| P4-1 | Run full `npm test` — verify 0 failures, 0 skipped | +| P4-2 | Run `scripts/test-api.sh` against staging/Neon DB — verify all PASS | +| P4-3 | Walk through security review checklist (cross-user access per endpoint) | +| P4-4 | Verify demo user is blocked on all 4 write endpoints via curl | +| P4-5 | Update `__tests__/lars-flow-checklist.md` to reference the new curl script | +| P4-6 | Add test instructions to README (`npm test`, `bash scripts/test-api.sh`) | +| P4-7 | Commit test files per commit strategy (separate commits per layer) | + +**Done when:** All exit criteria from section 7 are met. Test plan status → `Approved`. + +--- + +## Commit Plan + +Following the strict commit strategy, test work is committed in these layers: + +``` +chore(tests): add tester user to prisma seed +test(security): extend security.test.ts to cover all 7 endpoints +test(products): add unit tests for GET /api/products +test(next-story): add unit tests for GET /api/products/:id/next-story +test(sprint-tasks): add unit tests for GET /api/sprints/:id/tasks +test(story-log): add unit tests for POST /api/stories/:id/log +test(reorder): add unit tests for PATCH /api/stories/:id/tasks/reorder +test(tasks): add unit tests for PATCH /api/tasks/:id +test(todos): add unit tests for POST /api/todos +chore(scripts): add test-api.sh curl test script +docs(tests): update README with test instructions +``` + +--- + +## Summary Timeline + +``` +Week of 2026-04-28 + Mon 28 Apr │ P0 — Setup & infrastructure + Tue 29 Apr │ P1 — Security & auth layer (Vitest) + Wed 30 Apr │ P2 Day 1 — Read endpoint unit tests + Thu 01 May │ P2 Day 2 — Write endpoint unit tests + Fri 02 May │ P3 — Curl scripts + +Week of 2026-05-05 + Mon 05 May │ P4 — Review, DoD verification, commit +```