# 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 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="--" ``` 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: `--` | 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. |