- @tanstack/react-query with QueryClientProvider in providers/index.tsx - Typed api-client.ts fetch wrapper with ApiError class + apiDownload - Service modules: members, distributions, stock, reports, dashboard, portal, staff - Offline banner component (onlineManager subscription) - API error boundary with retry button - Loading skeleton components (card, table, chart, form, dashboard) - i18n for error/loading states (de/en)
20 KiB
CannaManage — Sprint 5 Test Plan
Date: 2026-06-12 Author: Patrick Plate / Lumen (Planner) Status: Draft v1 Basis: cannamanage-sprint5-plan.md v1
Test Overview
| ID | Description | Type | Target | Status |
|---|---|---|---|---|
| T-01 | Docker Compose full stack starts healthy | System | docker-compose.yml | ⬜ |
| T-02 | CORS preflight allows localhost:3000 | Integration | SecurityConfig | ⬜ |
| T-03 | Backend health endpoint responds | Integration | /actuator/health | ⬜ |
| T-04 | React Query provider mounts without error | Unit | providers.tsx | ⬜ |
| T-05 | apiFetch uses rewrite proxy correctly | Unit | api-client-browser.ts | ⬜ |
| T-06 | ApiError formats toast messages | Unit | use-api-error.ts | ⬜ |
| T-07 | Dashboard loads real club stats | Integration | dashboard/page.tsx | ⬜ |
| T-08 | Member list renders from API | Integration | members/page.tsx | ⬜ |
| T-09 | Create member persists to database | Integration | POST /members | ⬜ |
| T-10 | Edit member updates in database | Integration | PUT /members/{id} | ⬜ |
| T-11 | Distribution form records to backend | Integration | POST /distributions | ⬜ |
| T-12 | Quota exceeded returns 409 with details | Integration | ComplianceService | ⬜ |
| T-13 | Under-21 member enforces 30g limit | Integration | ComplianceService | ⬜ |
| T-14 | Batch creation adds stock | Integration | POST /stock/batches | ⬜ |
| T-15 | Batch recall changes status | Integration | PUT /stock/batches/{id}/recall | ⬜ |
| T-16 | PDF report downloads as binary | Integration | GET /reports/monthly?format=pdf | ⬜ |
| T-17 | CSV report downloads correctly | Integration | GET /reports/member-list?format=csv | ⬜ |
| T-18 | Portal login with session auth | Integration | POST /portal/login | ⬜ |
| T-19 | Portal dashboard shows real quota | Integration | GET /portal/dashboard | ⬜ |
| T-20 | Staff list loads for ADMIN | Integration | GET /staff | ⬜ |
| T-21 | Staff invite sends email | Integration | POST /staff/invite | ⬜ |
| T-22 | Permission update persists | Integration | PUT /staff/{id}/permissions | ⬜ |
| T-23 | Staff revoke removes access | Integration | DELETE /staff/{id} | ⬜ |
| T-24 | Non-ADMIN gets 403 on staff endpoints | Security | SecurityConfig | ⬜ |
| T-25 | Seed data creates deterministic test state | System | R__test_seed.sql | ⬜ |
| T-26 | Full E2E: login → distribute → verify quota | System | system-test.spec.ts | ⬜ |
| T-27 | System test exit code 0 on success | System | docker-compose.test.yml | ⬜ |
| T-28 | Error state renders when backend down | Unit | error-state.tsx | ⬜ |
| T-29 | Loading skeleton appears during fetch | Unit | skeleton-card.tsx | ⬜ |
| T-30 | Empty state shown when no data | Unit | dashboard/members pages | ⬜ |
Status: ⬜ Open | ✅ Passed | ❌ Failed | ⏭️ Skipped
Test Cases (Detail)
T-01: Docker Compose full stack starts healthy
Type: System
Target: docker-compose.yml
Preconditions:
- Docker Desktop running
- No port conflicts (5432, 8080, 3000)
- Images buildable (Maven + Node available)
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | docker compose up -d |
All 3 containers reach "healthy" within 60s |
| b | docker compose ps |
db=healthy, backend=healthy, frontend=running |
| c | curl http://localhost:3000 |
Returns HTML (login page) |
| d | curl http://localhost:8080/actuator/health |
Returns {"status":"UP"} |
Post-conditions:
- All containers stable for 30s without restart loops
T-02: CORS preflight allows localhost:3000
Type: Integration
Target: SecurityConfig.java CORS bean
Preconditions:
- Backend running on port 8080
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | OPTIONS /api/v1/members with Origin: http://localhost:3000 | 200 + Access-Control-Allow-Origin: http://localhost:3000 |
| b | OPTIONS /api/v1/members with Origin: http://evil.com | No Access-Control-Allow-Origin header (or 403) |
| c | GET /api/v1/members with Origin: http://localhost:3000 + valid JWT | 200 + CORS headers present |
T-03: Backend health endpoint responds
Type: Integration Target: Spring Boot Actuator
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | GET /actuator/health (no auth) | 200 {"status":"UP"} |
| b | Backend with DB down | 503 {"status":"DOWN"} |
T-04: React Query provider mounts without error
Type: Unit
Class: providers.tsx
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Render providers with children | No console errors, children visible |
| b | Check React Query DevTools in dev mode | DevTools panel accessible (floating button) |
T-05: apiFetch uses rewrite proxy correctly
Type: Unit
Class: api-client-browser.ts
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | apiFetch("/members") |
Fetches /api/backend/members |
| b | Backend returns 401 | Throws ApiError with status 401 |
| c | Backend returns 500 | Throws ApiError with status 500, message from body |
| d | Backend returns non-JSON error | Throws ApiError with statusText as message |
| e | Network error (backend down) | Throws TypeError (fetch failure) |
T-06: ApiError formats toast messages
Type: Unit
Class: use-api-error.ts
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | ApiError(401, "Unauthorized") | Toast: "Sitzung abgelaufen" (destructive) |
| b | ApiError(403, "Forbidden") | Toast: "Zugriff verweigert" (destructive) |
| c | ApiError(409, "Quota exceeded: 5g remaining") | Toast: "Kontingent überschritten" with message |
| d | TypeError (network) | Toast: "Verbindungsfehler — Backend nicht erreichbar" |
| e | ApiError(500, "Internal error") | Toast: "Fehler" with generic message |
T-07: Dashboard loads real club stats
Type: Integration
Target: Dashboard page + GET /clubs/stats (or equivalent)
Preconditions:
- Backend running with seed data
- User logged in as ADMIN
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Navigate to dashboard | KPI cards show numbers matching DB (5 members from seed) |
| b | Refresh page | Data reloads (React Query refetch) |
| c | Backend returns error | Error state shown with retry button |
| d | Slow response (>2s) | Skeleton cards visible during loading |
T-08: Member list renders from API
Type: Integration
Target: Members page + GET /members
Preconditions:
- Seed data loaded (5 members)
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Navigate to /members | Table shows 5 seed members |
| b | Search "Müller" | Filtered to Max Müller only |
| c | Empty DB (no members) | Empty state: "Keine Mitglieder vorhanden" |
| d | Loading state | Skeleton table rows visible |
T-09: Create member persists to database
Type: Integration
Target: POST /api/v1/members
Preconditions:
- Logged in as ADMIN or STAFF with MANAGE_MEMBERS permission
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Valid member data (name, email, DOB, phone) | 201 Created, member appears in list |
| b | Duplicate email | 409 Conflict with error message |
| c | Missing required field (firstName blank) | 400 Bad Request with validation errors |
| d | Future date of birth | 400 Bad Request (validation) |
| e | Under-18 date of birth | 400 Bad Request (CanKG minimum age) |
T-10: Edit member updates in database
Type: Integration
Target: PUT /api/v1/members/{id}
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Change phone number | 200 OK, phone updated in DB |
| b | Change status to SUSPENDED | 200 OK, member shows suspended badge |
| c | Invalid member ID | 404 Not Found |
| d | Unauthorized role | 403 Forbidden |
T-11: Distribution form records to backend
Type: Integration
Target: POST /api/v1/distributions
Preconditions:
- Available batch exists (≥ requested grams)
- Member is ACTIVE
- Quota not exceeded
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | 10g Amnesia Haze to Max Müller | 201 Created, distribution ID returned |
| b | Batch available_grams decreases by 10g | GET /stock/batches/{id} shows -10g |
| c | Member quota updates | Compliance endpoint shows 10g used today |
| d | Stock chart updates after mutation | React Query invalidates batch cache |
T-12: Quota exceeded returns 409 with details
Type: Integration
Target: ComplianceService quota enforcement
Preconditions:
- Member already at 24g today (daily limit = 25g)
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Request 5g distribution | 409: "Daily quota exceeded. Remaining: 1g" |
| b | Request 1g distribution | 201 Created (exactly at limit) |
| c | Request 26g in single distribution | 409: "Daily limit is 25g" |
T-13: Under-21 member enforces 30g monthly limit
Type: Integration
Target: ComplianceService age-based quota
Preconditions:
- Member Jonas Fischer (DOB: 2005-11-01, age 20 in 2026) in seed data
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Check quota for Jonas | monthlyLimitGrams: 30, isUnder21: true |
| b | Distribute 29g across month | Success |
| c | Distribute 1g more (total 30g) | Success (at limit) |
| d | Distribute 1g more (total 31g) | 409: "Monthly quota exceeded for under-21 member" |
T-14: Batch creation adds stock
Type: Integration
Target: POST /api/v1/stock/batches
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | New batch: 500g Blue Dream, THC 19%, supplier "BioHemp" | 201 Created |
| b | Stock total increases by 500g | Dashboard total_stock reflects increase |
| c | Missing strain name | 400 Bad Request |
| d | Negative grams | 400 Bad Request |
T-15: Batch recall changes status
Type: Integration
Target: PUT /api/v1/stock/batches/{id}/recall
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Recall batch b-001 | Status changes to RECALLED |
| b | Recalled batch not available for distribution | Distribution form excludes recalled batches |
| c | Recall already-recalled batch | 400 Bad Request (or idempotent 200) |
| d | Invalid batch ID | 404 Not Found |
T-16: PDF report downloads as binary
Type: Integration
Target: GET /api/v1/reports/monthly?format=pdf
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Request monthly report (current month) | 200, Content-Type: application/pdf |
| b | Response body is valid PDF | First bytes = %PDF- |
| c | Browser triggers file download | Content-Disposition: attachment |
| d | No data for requested month | 200 with empty report (or 204 No Content) |
T-17: CSV report downloads correctly
Type: Integration
Target: GET /api/v1/reports/member-list?format=csv
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Request member list CSV | 200, Content-Type: text/csv |
| b | CSV has header row | First line: column names |
| c | 5 seed members in CSV body | 6 lines total (header + 5 data) |
| d | German umlauts preserved | "Müller" renders correctly (UTF-8 or ISO-8859-1) |
T-18: Portal login with session auth
Type: Integration
Target: POST /portal/login
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Valid member credentials (max@example.com / member123) | 200 {"status":"ok"} + Set-Cookie session |
| b | Invalid password | 401 {"error":"Invalid credentials"} |
| c | Non-member email (admin@...) | 401 (not a member) |
| d | Subsequent /portal/* requests with session cookie | 200 (authenticated) |
T-19: Portal dashboard shows real quota
Type: Integration
Target: GET /portal/dashboard
Preconditions:
- Member logged into portal session
- Some distributions recorded for this member
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Fresh member (no distributions) | Quota: 0g used / 50g limit (≥21 years) |
| b | After 10g distribution recorded | Quota: 10g used / 50g limit |
| c | Radial chart shows correct percentage | 20% filled (10/50) |
T-20: Staff list loads for ADMIN
Type: Integration
Target: GET /api/v1/staff
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | ADMIN requests staff list | 200 with array of staff accounts |
| b | Includes permissions array per staff | Each item has permissions: [...] |
| c | STAFF without MANAGE_STAFF permission | 403 Forbidden |
| d | MEMBER role | 403 Forbidden |
T-21: Staff invite sends email
Type: Integration
Target: POST /api/v1/staff/invite
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Valid email + permissions selection | 201 Created, invite token generated |
| b | Email already registered as staff | 409 Conflict |
| c | Invalid email format | 400 Bad Request |
| d | No permissions selected | 400 Bad Request (at least one required) |
T-22: Permission update persists
Type: Integration
Target: PUT /api/v1/staff/{id}/permissions
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Set permissions to [MANAGE_MEMBERS, VIEW_REPORTS] | 200, permissions saved |
| b | Staff can now access members but not stock | GET /members → 200, GET /stock → 403 |
| c | Empty permissions array | 200 (staff has no capabilities, effectively read-only) |
| d | Invalid permission name | 400 Bad Request |
T-23: Staff revoke removes access
Type: Integration
Target: DELETE /api/v1/staff/{id}
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Delete staff account | 204 No Content |
| b | Revoked staff can no longer login | 401 on next auth attempt |
| c | Staff disappears from list | GET /staff excludes deleted |
| d | Delete non-existent staff ID | 404 Not Found |
T-24: Non-ADMIN gets 403 on staff endpoints
Type: Security Target: SecurityConfig authorization rules
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | STAFF (no MANAGE_STAFF) → GET /staff | 403 |
| b | STAFF (no MANAGE_STAFF) → POST /staff/invite | 403 |
| c | MEMBER → GET /staff | 403 |
| d | Unauthenticated → GET /staff | 401 |
| e | ADMIN → GET /staff | 200 |
| f | STAFF with MANAGE_STAFF → GET /staff | 200 |
T-25: Seed data creates deterministic test state
Type: System
Target: R__test_seed.sql
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Backend starts with profile=test-seed | Seed data inserted on startup |
| b | SELECT COUNT(*) FROM members WHERE club_id='club-001' |
Returns 5 |
| c | SELECT COUNT(*) FROM batches WHERE club_id='club-001' |
Returns 3 |
| d | Second startup with same profile | No duplicate insert errors (ON CONFLICT DO NOTHING) |
| e | Admin user can login with seed credentials | JWT returned successfully |
T-26: Full E2E: login → distribute → verify quota
Type: System
Class: e2e/system-test.spec.ts
Preconditions:
- Full Docker stack running with seed data
Scenarios:
| # | Step | Expected Result |
|---|---|---|
| a | Login as admin@gruener-daumen.de | Dashboard loads with KPIs |
| b | Navigate to Members | 5 seed members visible in table |
| c | Navigate to Distributions | Distribution list renders |
| d | Record 10g Amnesia Haze → Max Müller | Success toast, distribution in list |
| e | Navigate to Stock | Amnesia Haze batch shows -10g available |
| f | Navigate to Reports → download monthly PDF | File downloads (200 response) |
| g | Logout, login as member (max@example.com) | Portal dashboard loads |
| h | Portal quota shows 10g used | Radial chart at 20% |
T-27: System test exit code 0 on success
Type: System
Target: docker-compose.test.yml
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | All tests pass | Playwright container exits with code 0, compose exits 0 |
| b | A test fails | Playwright exits non-zero, compose exits non-zero |
| c | Backend fails to start | Tests timeout, compose exits non-zero |
T-28: Error state renders when backend down
Type: Unit
Target: error-state.tsx
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | API fetch throws network error | Error component visible with message |
| b | Click "Retry" button | Query refetches |
| c | Multiple errors on same page | Each section shows own error independently |
T-29: Loading skeleton appears during fetch
Type: Unit
Target: skeleton-card.tsx, skeleton-table.tsx
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Query in isLoading state |
Skeleton shimmer visible |
| b | Query resolves | Skeleton replaced with real content |
| c | Query in isFetching (background refetch) |
No skeleton (data still shown) |
T-30: Empty state shown when no data
Type: Unit Target: Dashboard, Members, Distributions pages
Scenarios:
| # | Input | Expected Result |
|---|---|---|
| a | Members list empty (API returns []) | "Keine Mitglieder vorhanden" + Add button |
| b | Distributions list empty | "Keine Ausgaben erfasst" message |
| c | Stock empty (no batches) | "Kein Bestand vorhanden" + Add batch button |
| d | Dashboard with 0 members | KPI cards show 0, no chart data placeholder |
Test Data Requirements
| Entity | Count | Source | Notes |
|---|---|---|---|
| Club | 1 | R__test_seed.sql |
"Grüner Daumen e.V.", Berlin |
| Admin user | 1 | Seed | admin@gruener-daumen.de / admin123 |
| Staff user | 1 | Seed | staff@gruener-daumen.de / staff123 |
| Members | 5 | Seed | Including 1 under-21 (Jonas Fischer) |
| Strains | 3 | Seed | Amnesia Haze, White Widow, Northern Lights |
| Batches | 3 | Seed | 520g, 430g, 380g available |
| Distributions | 0 | Fresh | Tests create distributions during execution |
Test Coverage Matrix
| Component | Unit | Integration | System | Total |
|---|---|---|---|---|
| Docker/Infra | 0 | 1 | 2 | 3 |
| CORS/Security | 0 | 2 | 0 | 2 |
| API Client | 3 | 0 | 0 | 3 |
| Dashboard | 0 | 1 | 1 | 2 |
| Members | 0 | 2 | 1 | 3 |
| Distributions | 0 | 3 | 1 | 4 |
| Stock | 0 | 2 | 1 | 3 |
| Reports | 0 | 2 | 1 | 3 |
| Portal | 0 | 2 | 1 | 3 |
| Staff | 0 | 5 | 0 | 5 |
| UI States | 3 | 0 | 0 | 3 |
| Total | 6 | 20 | 4 | 30 |
Execution Strategy
Fast feedback loop (during development):
# Frontend unit tests (Vitest — if configured)
cd cannamanage-frontend && pnpm test
# Backend integration tests (existing Testcontainers suite)
cd cannamanage-api && mvn test
# Existing Playwright E2E (mock backend, ~30s)
cd cannamanage-frontend && pnpm exec playwright test
Full system test (before merge):
docker compose -f docker-compose.yml -f docker-compose.test.yml up \
--build --abort-on-container-exit --exit-code-from playwright
Manual smoke test checklist:
docker compose up→ all healthy- Open http://localhost:3000 → login page
- Login as admin → dashboard with real data
- Add member → appears in list
- Record distribution → quota updates
- Download PDF report → valid file
- Login as member (portal) → see personal quota
References
- Implementation Plan:
docs/sprint-5/cannamanage-sprint5-plan.md(v1) - Backend Controllers:
cannamanage-api/src/main/java/de/cannamanage/api/controller/ - Frontend Types:
cannamanage-frontend/src/types/api.ts - Security Config:
cannamanage-api/src/main/java/de/cannamanage/api/security/SecurityConfig.java - Existing E2E:
cannamanage-frontend/e2e/