Files
cannamanage/docs/sprint-12/cannamanage-sprint12-phase2-integration-tests.md
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

46 KiB
Raw Permalink Blame History

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:

# 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:

// 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:

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:

// 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.

// 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:

// 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:

{
  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):

// 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:

// 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:

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:

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

# 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:

// 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:

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)

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.

# 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:

# 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.