docs: Sprint 12 planning, analysis, reviews, and code review

- sprint12-analysis.md (full page audit)
- sprint12-plan.md (button fix plan)
- sprint12-testplan.md (button fix test plan)
- sprint12-phase2-integration-tests.md (v3, expert-approved)
- sprint12-phase2-panel-review.md (3 review cycles, 95% confidence)
- sprint12-code-review.md (approved with comments, blockers fixed)
This commit is contained in:
Patrick Plate
2026-06-18 14:43:25 +02:00
parent 776149e7d3
commit be932c1930
6 changed files with 2436 additions and 0 deletions
@@ -0,0 +1,999 @@
# Sprint 12 Phase 2: Real Integration Tests with Seed DB
**Date:** 2026-06-18
**Author:** Patrick Plate / Lumen (Planner)
**Status:** v3 — final revision per panel re-review
**Goal:** Replace demo/mock-mode E2E tests with full-stack integration tests backed by a real PostgreSQL database
---
## 1. Seed Data Strategy
### 1.1 Current State
The existing `scripts/seed/init.sql` already provides a baseline:
| Entity | Count | Notes |
|--------|-------|-------|
| Club | 1 | "Grüner Daumen e.V." |
| Admin User | 1 | admin@test.de / test123 |
| Members | 6 | Various statuses, 1 under-21 (Jonas Weber, DOB 2007-03-15), 1 near-quota (23g/25g used this month) |
| Strains | 3 | Northern Lights (18.5% THC), Amnesia Haze (22% THC), CBD Critical Mass (0.5% THC / 12% CBD) |
| Batches | 3 | All AVAILABLE |
| Distributions | 3 | From December 2024 |
| Monthly Quotas | 3 | December 2024 |
| Stock Movements | 4 | HARVEST_IN + DISTRIBUTION |
| Fee Schedules | 2 | Regulär + Ermäßigt |
| Fee Assignments | 4 | Members → fee schedules |
| Payments | 3 | PAID, various methods |
| Board Positions | 2 | 1. Vorsitzender + Kassenwart |
| Board Members | 2 | Max + Lisa elected |
### 1.2 Missing Seed Data (to add)
| Entity | Table | Count | Purpose |
|--------|-------|-------|---------|
| Staff User | `users` | 1 | staff@test.de / test123 (ROLE_STAFF) — for role-based tests |
| Member User | `users` | 1 | max@test.de / test123 (ROLE_MEMBER) — portal tests |
| Near-quota Member | `members` + `monthly_quotas` | 1 | "Thomas Müller" — 23g of 25g used this month (edge case D-3) |
| Documents | `documents` | 4 | One per category (SATZUNG, PROTOKOLL, VERTRAG, SONSTIGES) |
| Events | `club_events` | 3 | 1 past, 1 today, 1 future |
| Event RSVPs | `event_rsvps` | 3 | Various statuses |
| Info Board Posts | `info_board_posts` | 3 | 1 pinned, 2 normal |
| Forum Topics | `forum_topics` | 2 | 1 pinned, 1 regular |
| Forum Replies | `forum_replies` | 3 | 2 on topic 1, 1 on topic 2 |
| Grow Entries | `grow_calendar` (V9) | 3 | SEEDLING, VEGETATIVE, FLOWERING stages |
| Compliance Deadlines | `compliance_deadlines` (V28) | 3 | PENDING, OVERDUE, COMPLETED |
| Destruction Records | `destruction_records` (V23) | 1 | Compliance audit trail (v3: `recorded_by` = admin UUID `b1000000-...001`, not member UUID) |
### 1.3 Seed File Architecture
**Decision (v2): Flyway-only seeding — NO Docker `docker-entrypoint-initdb.d` mount.**
The seed data is loaded exclusively via a Flyway repeatable migration (`R__seed_test_data.sql`). This eliminates the timing contradiction where `docker-entrypoint-initdb.d` runs BEFORE Flyway creates the schema.
```
cannamanage-api/src/main/resources/db/
├── migration/ ← versioned migrations (V1..V35+)
│ ├── V1__initial_schema.sql
│ ├── ...
│ └── V35__xxx.sql
└── testdata/ ← test-only seed (Flyway repeatable)
└── R__seed_test_data.sql ← single file, includes all seed data
```
**Activation:** Only when `test` Spring profile is active:
```properties
# application-test.properties
spring.flyway.locations=classpath:db/migration,classpath:db/testdata
```
**Production/default profiles** only load `classpath:db/migration` — seed data is never deployed to production.
For local development reference, modular fragments remain under `scripts/seed/fragments/` for documentation and manual use:
```
scripts/seed/fragments/ ← reference fragments (not loaded by Flyway)
├── 00-club.sql
├── 01-users.sql
├── 02-members.sql
├── 03-strains-batches.sql
├── 04-distributions.sql
├── 05-finance.sql
├── 06-board.sql
├── 07-documents.sql
├── 08-events.sql
├── 09-forum.sql
├── 10-info-board.sql
├── 11-grow.sql
└── 12-compliance.sql
```
These fragments are concatenated into `R__seed_test_data.sql` during development. The single-file approach ensures Flyway checksum tracking works correctly.
### 1.4 Seed Data Design Principles
1. **Deterministic UUIDs** — All IDs follow the pattern `{prefix}000000-0000-0000-0000-00000000000{N}` for predictability in assertions
2. **ON CONFLICT DO NOTHING** — Idempotent inserts, safe to re-run (Flyway repeatable migration re-executes on checksum change)
3. **Realistic dates** — Use relative dates where possible (`NOW() - INTERVAL '7 days'`) for time-sensitive tests
4. **KCanG edge cases built-in** — Under-21 member (THC/quota limits), near-quota member (23g/25g), high-THC strain (22%), recalled batch, overdue compliance deadline
5. **All FKs satisfied** — Every row references valid parents (tenant_id, club_id, user_id)
### 1.5 Test Accounts
| Email | Password | Role | Purpose |
|-------|----------|------|---------|
| admin@test.de | test123 | ROLE_ADMIN | Full admin dashboard access |
| staff@test.de | test123 | ROLE_STAFF | Staff-level access (limited) |
| max@test.de | test123 | ROLE_MEMBER | Member portal access |
---
## 2. Test Architecture
### 2.1 DB Reset Strategy
**Decision (v2): Per-test reset via backend API endpoint + `beforeEach` hook.**
The backend exposes a test-only endpoint (gated by `test` Spring profile):
```
POST /api/v1/test/reset-db
Authorization: Bearer <admin-token>
Profile: test (only available when SPRING_PROFILES_ACTIVE includes "test")
```
This endpoint will:
1. `TRUNCATE ... CASCADE` all application tables (preserving Flyway schema_history)
2. Re-execute `R__seed_test_data.sql` content via JdbcTemplate
3. Return 200 OK when ready
**Why per-test (not per-suite)?**
- Panel finding R-1: If test 3 (create) fails mid-way, test 4 (delete) will also fail due to dirty state
- TRUNCATE + INSERT is <500ms — acceptable per-test overhead for 60+ tests
- Each test starts from identical seed state — no ordering dependencies
**Integration in Playwright:**
```typescript
// e2e/integration/helpers/db-reset.ts
import { ApiClient } from './api-client';
export async function resetDatabase(apiClient: ApiClient) {
const response = await apiClient.resetDb();
if (response.status !== 200) {
throw new Error(`DB reset failed: ${response.status}`);
}
}
```
Each spec file uses `beforeEach`:
```typescript
test.describe.serial('Members', () => {
test.beforeEach(async () => {
await apiClient.resetDb();
});
test('Members table shows all 6 seed members', async ({ page }) => { ... });
test('Create new member', async ({ page }) => { ... });
});
```
**Why not pg_restore or fresh container?**
- Container restart is too slow (15-30s for Spring Boot + Flyway)
- TRUNCATE + INSERT is <500ms
- Keeps the Docker orchestration simple
### 2.2 Selector Strategy — `data-testid` Attributes
**Decision (v2): Commit to `data-testid` attributes for all testable UI elements.**
This is NOT an open question — it is a requirement for Phase 2 implementation. Every interactive element and data-display element that integration tests assert on MUST have a `data-testid` attribute.
**Naming convention:**
```
data-testid="<page>-<component>-<identifier>"
```
Examples:
- `data-testid="members-table"` — the members list table
- `data-testid="members-row-{id}"` — individual member row
- `data-testid="members-create-btn"` — create button
- `data-testid="members-form-email"` — email input in create/edit form
- `data-testid="distributions-quota-display"` — quota usage display
- `data-testid="dashboard-member-count"` — member count card
- `data-testid="nav-item-{slug}"` — navigation items
**Shared selectors file:**
```typescript
// e2e/integration/helpers/selectors.ts
export const SELECTORS = {
members: {
table: '[data-testid="members-table"]',
row: (id: string) => `[data-testid="members-row-${id}"]`,
createBtn: '[data-testid="members-create-btn"]',
formEmail: '[data-testid="members-form-email"]',
formName: '[data-testid="members-form-name"]',
formSubmit: '[data-testid="members-form-submit"]',
},
distributions: {
table: '[data-testid="distributions-table"]',
quotaDisplay: '[data-testid="distributions-quota-display"]',
newBtn: '[data-testid="distributions-create-btn"]',
},
dashboard: {
memberCount: '[data-testid="dashboard-member-count"]',
stockSummary: '[data-testid="dashboard-stock-summary"]',
},
// ... more selectors per page
} as const;
```
**Implementation impact:** Phase 2 implementation MUST add `data-testid` attributes to frontend components being tested. This is tracked as a sub-task of Phase 2C.
### 2.8 Seed Constants — Single Source of Truth for Test Assertions (v3: R-4)
**New file: `cannamanage-frontend/e2e/seed-constants.ts`**
All test assertions referencing seed data MUST import expected values from this file. When the seed SQL changes, update this one file — not 13 spec files.
```typescript
// cannamanage-frontend/e2e/seed-constants.ts
// Single source of truth for values derived from R__seed_test_data.sql
// ─── Deterministic UUIDs ───────────────────────────────────────────
export const CLUB_ID = 'a1000000-0000-0000-0000-000000000001';
export const ADMIN_USER_ID = 'b1000000-0000-0000-0000-000000000001';
export const STAFF_USER_ID = 'b1000000-0000-0000-0000-000000000002';
export const MEMBER_USER_ID = 'b1000000-0000-0000-0000-000000000003';
export const MEMBERS = {
MAX_MUSTERMANN: { id: 'c1000000-0000-0000-0000-000000000001', name: 'Max Mustermann', email: 'max@test.de' },
LISA_MEYER: { id: 'c1000000-0000-0000-0000-000000000002', name: 'Lisa Meyer' },
JONAS_WEBER: { id: 'c1000000-0000-0000-0000-000000000003', name: 'Jonas Weber', isUnder21: true },
THOMAS_MUELLER: { id: 'c1000000-0000-0000-0000-000000000004', name: 'Thomas Müller', quotaUsedG: 23 },
SARAH_SCHMIDT: { id: 'c1000000-0000-0000-0000-000000000005', name: 'Sarah Schmidt' },
ANNA_BRAUN: { id: 'c1000000-0000-0000-0000-000000000006', name: 'Anna Braun' },
} as const;
export const MEMBER_COUNT = 6;
// ─── Strains & Batches ────────────────────────────────────────────
export const STRAINS = {
NORTHERN_LIGHTS: { id: 'd1000000-0000-0000-0000-000000000001', name: 'Northern Lights', thcPct: 18.5 },
AMNESIA_HAZE: { id: 'd1000000-0000-0000-0000-000000000002', name: 'Amnesia Haze', thcPct: 22.0 },
CBD_CRITICAL_MASS: { id: 'd1000000-0000-0000-0000-000000000003', name: 'CBD Critical Mass', thcPct: 0.5, cbdPct: 12.0 },
} as const;
export const BATCH_COUNT = 3;
// ─── Distributions ────────────────────────────────────────────────
export const DISTRIBUTION_COUNT = 3;
export const DISTRIBUTION_QUANTITIES_G = [5, 3, 2];
// ─── Finance ──────────────────────────────────────────────────────
export const PAYMENT_COUNT = 3;
export const PAYMENT_AMOUNT_EUR = 30;
export const FEE_REGULAR_EUR = 30;
export const FEE_REDUCED_EUR = 15;
// ─── KCanG Quota Limits ───────────────────────────────────────────
export const KCANG = {
ADULT_DAILY_LIMIT_G: 25,
ADULT_MONTHLY_LIMIT_G: 50,
UNDER21_MONTHLY_LIMIT_G: 30,
UNDER21_MAX_THC_PCT: 10,
} as const;
// ─── Board ────────────────────────────────────────────────────────
export const BOARD_POSITIONS = ['1. Vorsitzender', 'Kassenwart'];
// ─── Documents ────────────────────────────────────────────────────
export const DOCUMENT_COUNT = 4;
export const DOCUMENT_CATEGORIES = ['SATZUNG', 'PROTOKOLL', 'VERTRAG', 'SONSTIGES'];
// ─── Events ───────────────────────────────────────────────────────
export const EVENT_COUNT = 3;
// ─── Forum ────────────────────────────────────────────────────────
export const FORUM_TOPIC_COUNT = 2;
export const FORUM_REPLY_COUNT = 3;
// ─── Grow ─────────────────────────────────────────────────────────
export const GROW_ENTRY_COUNT = 3;
export const GROW_STAGES = ['SEEDLING', 'VEGETATIVE', 'FLOWERING'];
// ─── Compliance ───────────────────────────────────────────────────
export const COMPLIANCE_DEADLINE_COUNT = 3;
export const COMPLIANCE_STATUSES = ['PENDING', 'OVERDUE', 'COMPLETED'];
```
**Usage in tests:**
```typescript
// e2e/integration/02-members.spec.ts
import { MEMBERS, MEMBER_COUNT } from '../seed-constants';
test('Members table shows all seed members', async ({ page }) => {
await page.goto('/members');
const rows = page.locator(SELECTORS.members.table + ' tbody tr');
await expect(rows).toHaveCount(MEMBER_COUNT);
await expect(page.locator(SELECTORS.members.row(MEMBERS.MAX_MUSTERMANN.id)))
.toContainText(MEMBERS.MAX_MUSTERMANN.name);
});
```
**Rule:** Never hardcode seed-derived values in spec files. Always import from `seed-constants.ts`. When `R__seed_test_data.sql` changes, update `seed-constants.ts` — all tests automatically adapt.
### 2.3 Playwright Config Changes
Add a new project `integration` to `playwright.config.ts`:
```typescript
{
name: "integration",
testMatch: /integration\/.+\.spec\.ts/,
dependencies: ["setup"],
timeout: 90_000,
expect: {
timeout: 15_000, // v2: extended for API-dependent assertions (panel R-3)
},
use: {
storageState: authFile,
browserName: "chromium",
navigationTimeout: 60_000,
},
}
```
**Timeout rationale (v2, panel R-3):**
- `timeout: 90_000` — overall test timeout, appropriate for real backend with Docker networking
- `navigationTimeout: 60_000` — page loads through Docker proxy
- `expect.timeout: 15_000` — assertions may wait for API responses; default 5s is too short for DB-backed assertions
- **First-test warmup:** The first test in a suite may be 5-10s slower due to connection pool warmup, JIT compilation, and first-request overhead. This is expected — `expect.timeout: 15_000` accommodates it.
### 2.4 Test File Organization
```
cannamanage-frontend/e2e/
├── global-setup.ts ← auth + health check wait (v2: A-5)
├── integration/ ← NEW: integration test specs
│ ├── helpers/
│ │ ├── api-client.ts ← direct API calls for setup/teardown/reset
│ │ ├── db-reset.ts ← DB reset helper (v2: R-1)
│ │ ├── selectors.ts ← shared data-testid selectors (v2: R-2)
│ │ └── assertions.ts ← reusable assertion helpers
│ ├── 01-dashboard.spec.ts
│ ├── 02-members.spec.ts
│ ├── 03-distributions.spec.ts
│ ├── 04-stock.spec.ts
│ ├── 05-documents.spec.ts
│ ├── 06-board.spec.ts
│ ├── 07-calendar.spec.ts
│ ├── 08-forum.spec.ts
│ ├── 09-info-board.spec.ts
│ ├── 10-finance.spec.ts
│ ├── 11-grow.spec.ts
│ ├── 12-compliance.spec.ts
│ └── 13-kcang-regulatory.spec.ts ← NEW (v2: D-1/D-2 KCanG edge cases)
├── system-test.spec.ts ← existing (keep as smoke test)
└── ... ← existing specs (keep)
```
### 2.5 Helper: API Client
A thin wrapper for direct backend API calls (for verification, setup, and DB reset):
```typescript
// e2e/integration/helpers/api-client.ts
export class ApiClient {
constructor(private baseUrl: string, private token: string) {}
async resetDb() { /* POST /api/v1/test/reset-db */ }
async getMembers() { /* GET /api/v1/members */ }
async getDistributions() { /* GET /api/v1/distributions */ }
async getBatches() { /* GET /api/v1/stock/batches */ }
async getMemberQuota(memberId: string) { /* GET /api/v1/members/{id}/quota */ }
// ... other endpoints for DB state verification
}
```
This allows tests to verify DB state after UI actions without relying solely on UI assertions.
### 2.6 Auth Flow
The existing `global-setup.ts` already handles admin login and saves `storageState`. Integration tests will:
1. Reuse the saved admin auth state (no per-test login overhead)
2. For member portal tests: create a separate auth state file (`member.json`)
### 2.7 Global Setup — Health Check Wait (v2: A-5)
Before any test runs, `global-setup.ts` waits for both backend and frontend to be healthy:
```typescript
// e2e/global-setup.ts
async function globalSetup() {
// Wait for backend health
await waitForUrl('http://backend:8080/actuator/health', { timeout: 120_000, interval: 3_000 });
// Wait for frontend
await waitForUrl('http://frontend:3000', { timeout: 60_000, interval: 2_000 });
// Perform initial DB reset to ensure clean state
const apiClient = new ApiClient('http://backend:8080', adminToken);
await apiClient.resetDb();
// Authenticate and save state
// ...
}
```
This prevents flaky first-test failures due to services not being ready.
---
## 3. Integration Test Specs
### 3.1 Dashboard (`01-dashboard.spec.ts`)
**Seed data needed:** All (dashboard aggregates from multiple tables)
| # | Test Case | Type | Assertion |
|---|-----------|------|-----------|
| 1 | Dashboard shows member count | Read | `[data-testid="dashboard-member-count"]` contains "6" |
| 2 | Dashboard shows stock summary | Read | Batch quantities match seed |
| 3 | Dashboard shows recent distributions | Read | Latest distribution visible |
| 4 | Dashboard shows compliance status | Read | Status indicator present |
| 5 | All dashboard cards load without error | Read | No loading spinners after `expect.timeout` |
### 3.2 Members (`02-members.spec.ts`)
**Seed data needed:** 6 members with various statuses (incl. under-21 + near-quota)
| # | Test Case | Type | Assertion |
|---|-----------|------|-----------|
| 1 | Members table shows all 6 seed members | Read | Table rows = 6, names match |
| 2 | Member search/filter works | Read | Filter by "Max" → 1 result |
| 3 | Create new member | CRUD | Fill form → submit → toast → table has 7 rows |
| 4 | Edit existing member | CRUD | Click edit → change name → save → verify new name |
| 5 | Member detail shows correct data | Read | Click row → detail matches seed (DOB, email, status) |
| 6 | Under-21 member shows quota indicator | Read | Jonas Weber row shows age-restriction indicator |
| 7 | Near-quota member shows warning | Read | Thomas Müller row shows "23g/25g" usage warning |
**DB verification after test 3:**
```typescript
const members = await apiClient.getMembers();
expect(members.length).toBe(7);
expect(members.find(m => m.email === 'new@test.de')).toBeTruthy();
```
### 3.3 Distributions (`03-distributions.spec.ts`)
**Seed data needed:** 3 distributions, 6 members, 3 batches
| # | Test Case | Type | Assertion |
|---|-----------|------|-----------|
| 1 | Distributions table shows seed data | Read | 3 rows, quantities match (5g, 3g, 2g) |
| 2 | Record new distribution | CRUD | Select member + batch → enter grams → submit |
| 3 | New distribution updates quota display | CRUD | Member's used quota increases |
| 4 | Under-21 member cannot exceed 30g/month quota | CRUD | Attempt 31g → error toast |
| 5 | Distribution shows batch strain name | Read | "Northern Lights" visible in row |
| 6 | THC/CBD values display correctly | Read | Verify THC% / CBD% columns |
### 3.4 Stock (`04-stock.spec.ts`)
**Seed data needed:** 3 batches (all AVAILABLE), 3 strains
| # | Test Case | Type | Assertion |
|---|-----------|------|-----------|
| 1 | Stock table shows 3 batches | Read | Rows = 3, batch codes match |
| 2 | Batch details show strain info | Read | "Northern Lights 18.5% THC" |
| 3 | Add new batch (receive stock) | CRUD | Fill form → submit → 4 rows |
| 4 | Stock movement logged | CRUD | After receive, movement audit visible |
| 5 | Recall batch | CRUD | Click recall → status changes to RECALLED |
| 6 | Recalled batch not available for distribution | Read | Not in distribution dropdown |
### 3.5 Documents (`05-documents.spec.ts`)
**Seed data needed:** 4 documents across categories
| # | Test Case | Type | Assertion |
|---|-----------|------|-----------|
| 1 | Documents page shows 4 seed documents | Read | Table rows = 4, titles visible |
| 2 | Filter by category works | Read | Filter "SATZUNG" → 1 result |
| 3 | Upload new document | CRUD | Select file → fill title/category → upload → toast |
| 4 | New document appears in list | Read | After upload, table has 5 rows |
| 5 | Download document | CRUD | Click download → verify response (non-empty) |
| 6 | Delete document | CRUD | Click delete → confirm → table has 4 rows again |
| 7 | Category badges display correctly | Read | Color-coded badges match category |
**Note:** For upload testing, Playwright can use `page.setInputFiles()` with a small test PDF.
### 3.6 Board (`06-board.spec.ts`)
**Seed data needed:** 2 positions, 2 board members
| # | Test Case | Type | Assertion |
|---|-----------|------|-----------|
| 1 | Board page shows 2 positions | Read | "1. Vorsitzender", "Kassenwart" visible |
| 2 | Positions show elected members | Read | "Max Mustermann", "Lisa Meyer" visible |
| 3 | Create new position | CRUD | Click "Position hinzufügen" → fill title → save |
| 4 | Elect member to new position | CRUD | Click "Mitglied zuweisen" → select → confirm |
| 5 | Remove board member | CRUD | Click remove → confirm dialog → position shows vacant |
| 6 | Term dates display correctly | Read | "15.01.2024 15.01.2026" visible |
### 3.7 Calendar / Events (`07-calendar.spec.ts`)
**Seed data needed:** 3 events (past, today, future)
| # | Test Case | Type | Assertion |
|---|-----------|------|-----------|
| 1 | Calendar shows events | Read | At least 1 event dot/indicator visible |
| 2 | Event detail shows correct info | Read | Click event → title, time, location visible |
| 3 | Create new event | CRUD | Fill form (title, date, type) → save |
| 4 | RSVP to event | CRUD | Click RSVP → status changes to "Zugesagt" |
| 5 | Cancel event | CRUD | Click cancel → event marked as cancelled |
### 3.8 Forum (`08-forum.spec.ts`)
**Seed data needed:** 2 topics, 3 replies
| # | Test Case | Type | Assertion |
|---|-----------|------|-----------|
| 1 | Forum shows 2 topics | Read | Topic titles visible, reply counts shown |
| 2 | Pinned topic appears first | Read | First topic has pin indicator |
| 3 | Open topic shows replies | Read | Click topic → 2 replies visible |
| 4 | Create new topic | CRUD | Fill title + content → submit → 3 topics |
| 5 | Reply to topic | CRUD | Open topic → type reply → submit → reply count +1 |
| 6 | Topic search/filter works | Read | Search term → filtered results |
### 3.9 Info Board (`09-info-board.spec.ts`)
**Seed data needed:** 3 posts (1 pinned, 2 normal)
| # | Test Case | Type | Assertion |
|---|-----------|------|-----------|
| 1 | Info board shows 3 posts | Read | Post cards/rows = 3 |
| 2 | Pinned post appears first | Read | First post has pin indicator |
| 3 | Create new announcement | CRUD | Fill title + content + category → publish |
| 4 | Archive post | CRUD | Click archive → post disappears from main view |
| 5 | Category filter works | Read | Select category → filtered results |
### 3.10 Finance (`10-finance.spec.ts`)
**Seed data needed:** 3 payments, 2 fee schedules
| # | Test Case | Type | Assertion |
|---|-----------|------|-----------|
| 1 | Finance overview shows payment summary | Read | Total amounts visible |
| 2 | Payments table shows 3 entries | Read | Rows = 3, amounts match (30€, 30€, 30€) |
| 3 | Record new payment | CRUD | Select member → amount → method → save |
| 4 | Payment status badge shows correctly | Read | "PAID" badges on all seed entries |
| 5 | Fee schedule overview shows tiers | Read | "Regulär 30€" and "Ermäßigt 15€" visible |
### 3.11 Grow (`11-grow.spec.ts`)
**Seed data needed:** 3 grow entries at different stages
| # | Test Case | Type | Assertion |
|---|-----------|------|-----------|
| 1 | Grow page shows entries | Read | 3 grow cards/rows visible |
| 2 | Stage indicators display correctly | Read | SEEDLING / VEGETATIVE / FLOWERING labels |
| 3 | Create new grow entry | CRUD | Fill strain + planted date + stage → save |
| 4 | Update grow stage | CRUD | Change stage SEEDLING → VEGETATIVE → verify |
| 5 | Grow timeline/history visible | Read | Stage transitions logged |
### 3.12 Compliance (`12-compliance.spec.ts`)
**Seed data needed:** 3 compliance deadlines (PENDING, OVERDUE, COMPLETED)
| # | Test Case | Type | Assertion |
|---|-----------|------|-----------|
| 1 | Compliance dashboard shows status | Read | Overall compliance indicator |
| 2 | Deadlines list shows 3 entries | Read | Status badges match (PENDING, OVERDUE, COMPLETED) |
| 3 | Overdue items highlighted | Read | Red/warning indicator on overdue item |
| 4 | Mark deadline as completed | CRUD | Click complete → status changes |
| 5 | Reports section accessible | Read | Navigation to reports works |
### 3.13 KCanG Regulatory Edge Cases (`13-kcang-regulatory.spec.ts`) — NEW v2
**Seed data needed:** Under-21 member (Jonas Weber), near-quota member (Thomas Müller, 23g/25g), high-THC strain (Amnesia Haze 22%)
This spec specifically tests KCanG (Konsumcannabisgesetz) regulatory enforcement:
| # | Test Case | Type | Assertion |
|---|-----------|------|-----------|
| 1 | Daily 25g limit: distribution exceeding 25g/day rejected | CRUD | Select adult member → enter 26g → submit → quota error toast |
| 2 | Monthly 50g limit: distribution exceeding 50g/month rejected | CRUD | Select member with 45g used → enter 6g → error toast |
| 3 | Under-21 THC% limit: distribution of >10% THC to under-21 rejected | CRUD | Select Jonas Weber (U21) → select Amnesia Haze (22% THC) → enter 1g → THC limit error |
| 4 | Under-21 THC% limit: distribution of ≤10% THC to under-21 allowed | CRUD | Select Jonas Weber → select CBD Critical Mass (0.5% THC) → enter 5g → success |
| 5 | Under-21 monthly limit: 30g/month (not 50g) | CRUD | Select Jonas Weber → enter 31g of low-THC strain → quota error |
| 6 | Near-quota member: 23g used + 3g = 26g exceeds daily 25g | CRUD | Select Thomas Müller → enter 3g → error (daily limit) |
| 7 | Near-quota member: 23g used + 2g = 25g exactly at daily limit | CRUD | Select Thomas Müller → enter 2g → success (exactly at limit) |
| 8 | Quota display shows correct remaining for near-quota member | Read | Thomas Müller shows "23g / 25g" with warning indicator |
| 9 | Under-21 member shows THC restriction notice in UI | Read | Jonas Weber's distribution form shows "max. 10% THC" notice |
**DB verification after test 7:**
```typescript
const quota = await apiClient.getMemberQuota(thomasMuellerId);
expect(quota.usedToday).toBe(25); // 23 + 2
expect(quota.remainingToday).toBe(0);
```
---
## 4. CI Integration
### 4.1 Execution Strategy
```yaml
# In GitHub Actions / Gitea Actions:
jobs:
integration-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Start full stack
run: docker compose -f docker-compose.test.yml up -d --build
- name: Wait for health
run: |
timeout 120 bash -c 'until curl -sf http://localhost:8080/actuator/health; do sleep 3; done'
timeout 60 bash -c 'until curl -sf http://localhost:3000; do sleep 2; done'
- name: Run integration tests
run: |
docker compose -f docker-compose.test.yml exec playwright \
npx playwright test e2e/integration/ --reporter=list,html
- name: Upload HTML report (v2: R-5)
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-html-report
path: cannamanage-frontend/playwright-report/
- name: Upload test artifacts (traces + screenshots)
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-artifacts
path: cannamanage-frontend/test-results/
```
### 4.2 Test Tagging Strategy (v2: R-6)
Tests are tagged for CI tiering using Playwright's `test.describe` annotations:
```typescript
// Smoke tests — run on every PR push (<2 min)
test.describe('@smoke', () => {
test('Dashboard loads', ...);
test('Login works', ...);
test('Members table renders', ...);
});
// Full suite — run on main merge + nightly (~8 min)
test.describe('@full', () => {
test('Complete CRUD flow', ...);
test('KCanG regulatory edge cases', ...);
});
```
**CI trigger mapping:**
| Trigger | Tag filter | Timeout |
|---------|-----------|---------|
| PR (push) | `--grep @smoke` | 3 min |
| main merge | (all tests) | 10 min |
| Nightly | (all tests) + visual regression | 15 min |
| Manual dispatch | Selectable via `--grep` | Configurable |
### 4.3 Flaky Test Handling
1. **Retries:** `retries: 1` in CI mode only (via `process.env.CI`)
2. **Timeouts:** Liberal timeouts for Docker networking (90s test, 60s navigation, 15s expect)
3. **Wait strategies:** Never use `waitForTimeout` — always wait for specific `data-testid` selectors or network events
4. **Trace collection:** `trace: 'on-first-retry'` for debugging CI failures
5. **Screenshot on failure:** `screenshot: 'only-on-failure'` in CI
### 4.4 Screenshot Comparison Strategy (v2: R-4)
**Approach: Structural comparison, NOT pixel-diff.**
Pixel-diff is fragile across environments (font rendering, anti-aliasing, Docker vs local). Instead:
- Use Playwright's `toHaveScreenshot()` with `maxDiffPixelRatio: 0.01` tolerance
- Store baseline screenshots in `e2e/integration/__screenshots__/` (committed to git)
- Update baselines via `npx playwright test --update-snapshots` when UI intentionally changes
- In CI: compare against committed baselines; fail on structural regressions
For the nightly visual regression run:
```typescript
test('Dashboard visual regression @nightly', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard.png', { maxDiffPixelRatio: 0.01 });
});
```
### 4.5 Artifact Collection
On failure, collect:
- Screenshots (per-test on failure)
- Playwright trace files (`.zip` — viewable via trace.playwright.dev)
- Backend logs: `docker compose logs backend > backend.log`
- **HTML report:** `--reporter=html` generates a browsable report (uploaded as CI artifact, viewable directly)
---
## 5. Docker Compose Improvements
### 5.1 Updated `docker-compose.test.yml` (v3)
```yaml
services:
db:
image: postgres:16-alpine
container_name: cannamanage-test-db
environment:
POSTGRES_DB: cannamanage_test
POSTGRES_USER: cannamanage
POSTGRES_PASSWORD: testpass
# v3: tmpfs for CI speed (ephemeral data). For macOS local dev, use docker-compose.test.local.yml override.
tmpfs:
- /var/lib/postgresql/data
# v2: NO volume mount for init.sql — seed handled by Flyway R__seed_test_data.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U cannamanage"]
interval: 3s
timeout: 3s
retries: 10
backend:
build:
context: .
dockerfile: Dockerfile.backend
container_name: cannamanage-test-backend
environment:
SPRING_PROFILES_ACTIVE: docker,test
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/cannamanage_test
SPRING_DATASOURCE_USERNAME: cannamanage
SPRING_DATASOURCE_PASSWORD: testpass
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8080/actuator/health || exit 1"]
interval: 5s
timeout: 5s
retries: 20
frontend:
build:
context: ./cannamanage-frontend
dockerfile: Dockerfile
container_name: cannamanage-test-frontend
environment:
NEXT_PUBLIC_API_URL: http://backend:8080
depends_on:
backend:
condition: service_healthy
playwright:
build:
context: ./cannamanage-frontend
dockerfile: Dockerfile.playwright
container_name: cannamanage-test-playwright
environment:
BASE_URL: http://frontend:3000
API_URL: http://backend:8080
CI: "true"
depends_on:
frontend:
condition: service_started
backend:
condition: service_healthy
# v3 (v2-1): Volume mounts OVERRIDE the COPY'd files from Dockerfile.playwright at runtime.
# This is intentional — the Dockerfile pre-installs deps (node_modules), while
# the volume mounts allow iterating on test code without rebuilding the image.
# The e2e/ and config mounts are :ro (read-only); results/report are writable for output.
volumes:
- ./cannamanage-frontend/e2e:/app/e2e:ro
- ./cannamanage-frontend/playwright.config.ts:/app/playwright.config.ts:ro
- ./cannamanage-frontend/test-results:/app/test-results
- ./cannamanage-frontend/playwright-report:/app/playwright-report
command: >
sh -c "
echo 'Waiting for frontend...' &&
timeout 90 sh -c 'until wget -q -O /dev/null http://frontend:3000 2>/dev/null; do sleep 2; done' &&
echo 'Frontend ready — running integration tests...' &&
npx playwright test e2e/integration/ --reporter=list,html
"
```
### 5.2 Playwright Dockerfile (v3: A-4 + v2-3)
**New file: `cannamanage-frontend/Dockerfile.playwright`**
The Playwright container needs its own Dockerfile to pre-install dependencies (panel A-4).
> **⚠️ Version Pinning Rule (v3: v2-3):** The Playwright Docker image version (`v1.49.0` below) MUST always match the `@playwright/test` version in `package.json`. A mismatch between the Docker image (which bundles browser binaries) and the npm package (which provides the API) causes cryptic browser launch failures. When upgrading Playwright, update BOTH `package.json` AND this Dockerfile in the same commit.
```dockerfile
# IMPORTANT: Keep this version in sync with @playwright/test in package.json (v3: v2-3)
FROM mcr.microsoft.com/playwright:v1.49.0-jammy
WORKDIR /app
# Copy package files for dependency install
COPY package.json pnpm-lock.yaml .npmrc ./
# Install pnpm and dependencies (v2: A-4 — pre-install deps)
RUN npm install -g pnpm && \
pnpm install --frozen-lockfile
# Copy Playwright config and test sources
COPY playwright.config.ts ./
COPY e2e/ ./e2e/
# Playwright browsers are pre-installed in the base image
```
This ensures `pnpm install --frozen-lockfile` runs at build time, not at test runtime.
### 5.5 Local Development Override (v3: A-2 — tmpfs conditional)
**New file: `docker-compose.test.local.yml`**
On macOS with Docker Desktop, `tmpfs` may cause Postgres startup failures due to the Linux VM's handling of tmpfs syscalls. For local development, use a named volume instead:
```yaml
# docker-compose.test.local.yml — override for macOS local development
# Usage: docker compose -f docker-compose.test.yml -f docker-compose.test.local.yml up
services:
db:
tmpfs: [] # disable tmpfs from base compose
volumes:
- test-db-data:/var/lib/postgresql/data
volumes:
test-db-data:
driver: local
```
**When to use which:**
| Environment | Command | DB Storage |
|-------------|---------|-----------|
| CI (GitHub Actions, `ubuntu-latest`) | `docker compose -f docker-compose.test.yml up` | `tmpfs` (fast, ephemeral) |
| Local macOS (Docker Desktop) | `docker compose -f docker-compose.test.yml -f docker-compose.test.local.yml up` | Named volume (compatible) |
Alternatively, developers who don't experience tmpfs issues on their Docker Desktop version can use the base compose directly. The override is opt-in for those who hit Postgres startup failures.
**CI detection:** The CI workflow uses only `docker-compose.test.yml` (tmpfs enabled by default). The `CI=true` environment variable is already set in the playwright service. No conditional logic needed in the compose file itself — the override file approach keeps it simple and declarative.
### 5.3 Key Improvements (v2 summary)
| Change | Reason | Panel Finding |
|--------|--------|---------------|
| Remove `init.sql` volume mount from db | Seed timing contradiction — Flyway handles it | A-1 (BLOCKER) |
| `tmpfs` for Postgres | 3-5x faster writes, data is ephemeral | — |
| `test` Spring profile | Gates `/api/v1/test/reset-db` + Flyway testdata location | A-3 |
| Explicit `container_name` per service | Clear network addressing (A-2) | A-2 |
| `Dockerfile.playwright` with `pnpm install` | Pre-install deps at build time | A-4 |
| Health check on backend before playwright starts | Prevents flaky first-test failures | A-5 |
| `--reporter=html` + artifact upload | Browsable CI reports | R-5 |
| Frontend accessible as `http://frontend:3000` | Docker service name = hostname for Playwright | A-2 |
### 5.4 Network Addressing (v2: A-2)
Docker Compose service names serve as DNS hostnames within the compose network:
- **Frontend** is reachable at `http://frontend:3000` from the `playwright` container
- **Backend** is reachable at `http://backend:8080` from both `frontend` and `playwright`
- **Database** is reachable at `db:5432` from `backend`
The `BASE_URL` environment variable for Playwright is set to `http://frontend:3000`. No custom network aliases or extra configuration needed — Docker Compose default network handles it.
---
## 6. Implementation Phases
### Phase 2A: Seed Data Expansion + Flyway Integration (Day 1)
1. Create `cannamanage-api/src/main/resources/db/testdata/R__seed_test_data.sql`
2. Expand with all missing entities (staff/member users, documents, events, forum, grow, compliance)
3. Add KCanG edge case seed data: under-21 member (Jonas Weber), near-quota member (Thomas Müller, 23g/25g)
4. Add `spring.flyway.locations` to `application-test.properties`
5. Verify: start backend with `--spring.profiles.active=test` → seed data present in DB
### Phase 2B: Backend Test Profile + Reset Endpoint (Day 1)
1. Implement `TestResetController` with `POST /api/v1/test/reset-db`
2. Gate with `@Profile("test")` annotation
3. Controller executes TRUNCATE CASCADE + re-runs seed SQL via JdbcTemplate
4. Add integration test for the reset endpoint itself
5. Verify: call endpoint → DB returns to seed state
### Phase 2C: Test Infrastructure + data-testid Attributes (Day 2)
1. Create `e2e/integration/helpers/api-client.ts` (with `resetDb()` method)
2. Create `e2e/integration/helpers/db-reset.ts`
3. Create `e2e/integration/helpers/selectors.ts` (data-testid constants)
4. Update `playwright.config.ts` with `integration` project (incl. `expect.timeout: 15_000`)
5. Update `global-setup.ts` with health check wait loop
6. **Add `data-testid` attributes to all frontend components** being tested (critical sub-task)
7. Create `cannamanage-frontend/Dockerfile.playwright`
8. Update `docker-compose.test.yml` (remove init.sql mount, add Dockerfile.playwright, add health checks)
### Phase 2D: Integration Test Specs (Day 2-4)
1. Priority 1: Members, Distributions, Stock (core business logic)
2. Priority 2: Documents, Board (recently fixed in Sprint 12 Phase 1)
3. Priority 3: Calendar, Forum, Info Board (communication)
4. Priority 4: Finance, Grow, Compliance (supporting modules)
5. Priority 5: **KCanG Regulatory (`13-kcang-regulatory.spec.ts`)** — daily 25g limit, under-21 THC%, monthly quotas
### Phase 2E: CI Pipeline (Day 4)
1. Create GitHub/Gitea Actions workflow with `@smoke` / `@full` tagging
2. Configure artifact upload (HTML report + traces + screenshots)
3. Add visual regression baseline screenshots
4. Test full cycle: push → build → test → report
---
## 7. Success Criteria
| Criterion | Metric |
|-----------|--------|
| All seed data loads without errors | Backend starts with `test` profile, no Flyway errors |
| Integration tests pass against real DB | 70+ test cases (incl. 9 KCanG regulatory), 100% pass rate |
| Per-test DB reset works | Each test starts from identical seed state |
| Test execution time (v3: R-1) | `@full` suite < 8 minutes; `@smoke` suite < 2 minutes |
| No flaky tests on 3 consecutive runs | 3/3 green runs |
| CI pipeline works end-to-end | PR triggers → results posted with HTML report |
| DB state verification works | API assertions confirm CRUD effects |
| data-testid selectors stable | No selector-based failures across runs |
| KCanG regulatory tests pass | All 9 edge cases correctly enforced |
| seed-constants.ts consistency (v3: R-4) | All assertions import from `seed-constants.ts`, no hardcoded seed values in specs |
---
## 8. Risks & Mitigations
| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|-----------|
| Flyway repeatable migration not re-running on unchanged checksum | Low | High | Document: change seed content → checksum changes → Flyway re-runs automatically |
| Per-test reset too slow (>500ms × 70 tests = 35s overhead) | Low | Low | Acceptable — reset is <500ms, total overhead ~35s for full isolation |
| Test data coupling (tests depend on specific IDs) | Medium | Medium | Use deterministic UUIDs, document in selectors.ts |
| Docker build time in CI | Low | Medium | Cache Docker layers, use pre-built Playwright image |
| Frontend hydration issues with real data | Low | High | Use `waitForLoadState('networkidle')` + `data-testid` selectors |
| Backend API response format changes break tests | Medium | Medium | Use API client abstraction, update in one place |
| data-testid attributes missing in new components | Medium | Medium | Enforce via PR review checklist + ESLint rule (future) |
| First-test warmup causing timeout | Low | Low | `expect.timeout: 15_000` accommodates warmup (panel R-3) |
| Connection pool warmup on first request | Low | Low | globalSetup calls resetDb() which warms the pool before tests run |
---
## 9. Resolved Questions (v2)
These were open questions in v1, now resolved per panel review:
| Question | Resolution | Panel Finding |
|----------|-----------|---------------|
| Should we add `data-testid` attributes? | **YES — mandatory.** All testable elements get `data-testid`. Naming: `<page>-<component>-<identifier>` | R-2 |
| Member portal integration test suite? | Yes — separate auth state file (`member.json`), used by portal-specific tests | — |
| CRUD tests cleanup vs dirty state? | **Per-test reset via API endpoint.** Each test starts clean. | R-1 |
| Preferred CI platform? | GitHub Actions (primary), Gitea Actions (mirror) | — |
| Seed data loading timing? | **Flyway-only via `R__seed_test_data.sql`** in `classpath:db/testdata`. No Docker init.sql mount. | A-1, A-3 |
| Screenshot comparison strategy? | Structural comparison with `maxDiffPixelRatio: 0.01` — NOT pixel-diff | R-4 |
| CI reporter artifact strategy? | `--reporter=html` uploaded as CI artifact | R-5 |
| Test tiering for CI? | `@smoke` (PR) / `@full` (merge + nightly) via `--grep` | R-6 |
---
## Appendix A: Panel Review Changes Summary (v1 → v2)
| Finding | Severity | Resolution in v2 |
|---------|----------|-----------------|
| A-1: Seed timing contradiction | ❌ BLOCKER | Removed Docker init.sql mount; Flyway-only seeding |
| D-1: Missing KCanG 25g/day limit tests | ⚠️ | Added `13-kcang-regulatory.spec.ts` with 9 test cases |
| D-2: Missing under-21 THC% test | ⚠️ | Included in KCanG spec (tests 3, 4, 5, 9) |
| D-3: Near-quota member in seed | ️ | Added Thomas Müller (23g/25g) to seed data |
| A-2: Network alias clarity | ⚠️ | Explicit container_name + documented service-name networking |
| A-3: Flyway location specification | ⚠️ | `db/testdata/R__seed_test_data.sql` + `application-test.properties` |
| A-4: Playwright pnpm install | ⚠️ | Custom `Dockerfile.playwright` with pre-installed deps |
| A-5: Health check wait in globalSetup | ️ | Added `waitForUrl` in globalSetup before any tests |
| R-1: Per-test DB reset | ⚠️ | `beforeEach` calls `apiClient.resetDb()` |
| R-2: data-testid commitment | ⚠️ | Mandatory — naming convention + selectors.ts defined |
| R-3: expect.timeout + warmup | ⚠️ | `expect.timeout: 15_000` + documented warmup behavior |
| R-4: Screenshot comparison | ️ | Structural (maxDiffPixelRatio), not pixel-diff |
| R-5: HTML reporter artifacts | ️ | `--reporter=html` + CI artifact upload |
| R-6: Test tagging | ️ | `@smoke` / `@full` with `--grep` in CI |
---
## Appendix B: v2 Re-Review Changes Summary (v2 → v3)
Changes made to address partially-resolved and info findings from the v2 re-review:
| Finding | Severity | Resolution in v3 |
|---------|----------|-----------------|
| A-2: tmpfs unconditional — no CI-only gating | ⚠️ Partial | Added `docker-compose.test.local.yml` override with named volume for macOS. Documented CI vs local usage in Section 5.5. |
| R-1: "< 5 minutes" success criterion optimistic | ⚠️ Partial | Changed to "< 8 minutes for `@full` suite, < 2 minutes for `@smoke` suite" in Section 7. |
| R-4: No explicit seed-constants.ts | ⚠️ Partial | Added complete `seed-constants.ts` file with all deterministic UUIDs, member data, KCanG limits, and counts (Section 2.8). Rule: never hardcode seed values in specs. |
| D-4: `recorded_by` should reference admin UUID | ️ Info | Updated seed data design — destruction records and distributions use admin UUID `b1000000-...001` as `recorded_by`. |
| v2-1: Volume mount + Dockerfile build overlap | ️ Info | Added explanatory comment in docker-compose.test.yml Section 5.1 documenting the intentional pattern (Dockerfile pre-installs deps, volume mounts allow test iteration). |
| v2-3: Playwright Docker image version pinning | ️ Info | Added version pinning rule in Section 5.2: Playwright image version MUST match `@playwright/test` in package.json. Comment added to Dockerfile. |