Files
cannamanage/docs/sprint-5/cannamanage-sprint5-testplan.md
T
Patrick Plate f42c166329 feat(sprint-5): Phase 2 — React Query API client layer
- @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)
2026-06-12 19:59:41 +02:00

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:

  1. docker compose up → all healthy
  2. Open http://localhost:3000 → login page
  3. Login as admin → dashboard with real data
  4. Add member → appears in list
  5. Record distribution → quota updates
  6. Download PDF report → valid file
  7. 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/