--- title: "Scrum4Me — API Test Plan" status: active audience: [maintainer, contributor] language: nl last_updated: 2026-05-03 --- # 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 ```