Files
cannamanage/docs/sprint-12/cannamanage-sprint12-phase2-integration-tests.md
T
Patrick Plate be932c1930 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)
2026-06-18 14:43:25 +02:00

1000 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. |