776149e7d3
Sprint 12 Phase 2: Real integration tests with seed DB - R__seed_test_data.sql (Flyway repeatable, 7 members, strains, batches, docs, board, events) - TestResetController (profile-gated per-test DB reset) - docker-compose.test.yml (self-contained, tmpfs Postgres) - Dockerfile.playwright (v1.60.0, pre-installed deps) - 13 integration spec files, 70+ test cases (@smoke + @full) - seed-constants.ts, selectors.ts, api-client.ts test helpers
296 lines
9.7 KiB
TypeScript
296 lines
9.7 KiB
TypeScript
import { expect, test } from "@playwright/test"
|
|
|
|
import { ApiClient } from "../api-client"
|
|
import { SEED } from "../seed-constants"
|
|
|
|
const apiClient = new ApiClient()
|
|
|
|
test.describe("KCanG Regulatory Edge Cases @full", () => {
|
|
test.beforeEach(async () => {
|
|
await apiClient.login(SEED.admin.email, SEED.admin.password)
|
|
await apiClient.resetDb()
|
|
})
|
|
|
|
// Requires: backend quota enforcement
|
|
test("rejects adult distribution exceeding 25g/day", async ({ page }) => {
|
|
await page.goto("/distributions/new")
|
|
|
|
// Select adult member (Max Mustermann)
|
|
const memberSelect = page
|
|
.getByLabel(/mitglied|member/i)
|
|
.or(page.locator("[data-testid='distribution-member-select']"))
|
|
await memberSelect.click()
|
|
await page.getByText(SEED.members.max.name).click()
|
|
|
|
// Select strain
|
|
const strainSelect = page
|
|
.getByLabel(/sorte|strain|charge|batch/i)
|
|
.or(page.locator("[data-testid='distribution-strain-select']"))
|
|
await strainSelect.click()
|
|
await page.getByText(SEED.strains.northernLights.name).click()
|
|
|
|
// Enter 26g (exceeds 25g daily limit)
|
|
const amountInput = page
|
|
.getByLabel(/menge|amount|gramm/i)
|
|
.or(page.locator("input[name*='amount']"))
|
|
await amountInput.fill("26")
|
|
|
|
// Submit
|
|
const submitBtn = page.getByRole("button", {
|
|
name: /ausgeben|submit|speichern/i,
|
|
})
|
|
await submitBtn.click()
|
|
|
|
// Should show rejection/error
|
|
await expect(
|
|
page.getByText(/überschr|exceeded|limit|abgelehnt|rejected/i)
|
|
).toBeVisible({ timeout: 5000 })
|
|
})
|
|
|
|
// Requires: backend quota enforcement
|
|
test("accepts adult distribution of exactly 25g", async ({ page }) => {
|
|
await page.goto("/distributions/new")
|
|
|
|
const memberSelect = page
|
|
.getByLabel(/mitglied|member/i)
|
|
.or(page.locator("[data-testid='distribution-member-select']"))
|
|
await memberSelect.click()
|
|
await page.getByText(SEED.members.max.name).click()
|
|
|
|
const strainSelect = page
|
|
.getByLabel(/sorte|strain|charge|batch/i)
|
|
.or(page.locator("[data-testid='distribution-strain-select']"))
|
|
await strainSelect.click()
|
|
await page.getByText(SEED.strains.northernLights.name).click()
|
|
|
|
const amountInput = page
|
|
.getByLabel(/menge|amount|gramm/i)
|
|
.or(page.locator("input[name*='amount']"))
|
|
await amountInput.fill("25")
|
|
|
|
const submitBtn = page.getByRole("button", {
|
|
name: /ausgeben|submit|speichern/i,
|
|
})
|
|
await submitBtn.click()
|
|
|
|
// Should succeed
|
|
await expect(
|
|
page.getByText(/erfolg|success|gespeichert/i)
|
|
).toBeVisible({ timeout: 5000 })
|
|
})
|
|
|
|
// Requires: backend quota enforcement
|
|
test("rejects under-21 member with strain exceeding 10% THC", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/distributions/new")
|
|
|
|
// Select under-21 member (Jonas Weber)
|
|
const memberSelect = page
|
|
.getByLabel(/mitglied|member/i)
|
|
.or(page.locator("[data-testid='distribution-member-select']"))
|
|
await memberSelect.click()
|
|
await page.getByText(SEED.members.jonas.name).click()
|
|
|
|
// Select Amnesia Haze (22% THC — exceeds 10% limit for under-21)
|
|
const strainSelect = page
|
|
.getByLabel(/sorte|strain|charge|batch/i)
|
|
.or(page.locator("[data-testid='distribution-strain-select']"))
|
|
await strainSelect.click()
|
|
await page.getByText(SEED.strains.amnesiaHaze.name).click()
|
|
|
|
const amountInput = page
|
|
.getByLabel(/menge|amount|gramm/i)
|
|
.or(page.locator("input[name*='amount']"))
|
|
await amountInput.fill("5")
|
|
|
|
const submitBtn = page.getByRole("button", {
|
|
name: /ausgeben|submit|speichern/i,
|
|
})
|
|
await submitBtn.click()
|
|
|
|
// Should show THC rejection
|
|
await expect(
|
|
page.getByText(/thc|überschr|exceeded|limit|abgelehnt|rejected/i)
|
|
).toBeVisible({ timeout: 5000 })
|
|
})
|
|
|
|
// Requires: backend quota enforcement
|
|
test("accepts under-21 member with strain within THC limit", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/distributions/new")
|
|
|
|
// Select under-21 member (Jonas)
|
|
const memberSelect = page
|
|
.getByLabel(/mitglied|member/i)
|
|
.or(page.locator("[data-testid='distribution-member-select']"))
|
|
await memberSelect.click()
|
|
await page.getByText(SEED.members.jonas.name).click()
|
|
|
|
// Select CBD Critical Mass (5% THC — within 10% limit)
|
|
const strainSelect = page
|
|
.getByLabel(/sorte|strain|charge|batch/i)
|
|
.or(page.locator("[data-testid='distribution-strain-select']"))
|
|
await strainSelect.click()
|
|
await page.getByText(SEED.strains.cbdCriticalMass.name).click()
|
|
|
|
const amountInput = page
|
|
.getByLabel(/menge|amount|gramm/i)
|
|
.or(page.locator("input[name*='amount']"))
|
|
await amountInput.fill("5")
|
|
|
|
const submitBtn = page.getByRole("button", {
|
|
name: /ausgeben|submit|speichern/i,
|
|
})
|
|
await submitBtn.click()
|
|
|
|
// Should succeed
|
|
await expect(
|
|
page.getByText(/erfolg|success|gespeichert/i)
|
|
).toBeVisible({ timeout: 5000 })
|
|
})
|
|
|
|
// Requires: backend quota enforcement
|
|
test("rejects under-21 member exceeding 30g/month", async ({ page }) => {
|
|
// This test assumes Jonas has already received close to 30g this month
|
|
// The seed data should set up 31g attempted distribution
|
|
await page.goto("/distributions/new")
|
|
|
|
const memberSelect = page
|
|
.getByLabel(/mitglied|member/i)
|
|
.or(page.locator("[data-testid='distribution-member-select']"))
|
|
await memberSelect.click()
|
|
await page.getByText(SEED.members.jonas.name).click()
|
|
|
|
const strainSelect = page
|
|
.getByLabel(/sorte|strain|charge|batch/i)
|
|
.or(page.locator("[data-testid='distribution-strain-select']"))
|
|
await strainSelect.click()
|
|
await page.getByText(SEED.strains.cbdCriticalMass.name).click()
|
|
|
|
// 31g exceeds the 30g/month limit for under-21
|
|
const amountInput = page
|
|
.getByLabel(/menge|amount|gramm/i)
|
|
.or(page.locator("input[name*='amount']"))
|
|
await amountInput.fill("31")
|
|
|
|
const submitBtn = page.getByRole("button", {
|
|
name: /ausgeben|submit|speichern/i,
|
|
})
|
|
await submitBtn.click()
|
|
|
|
// Should show monthly quota rejection
|
|
await expect(
|
|
page.getByText(/überschr|exceeded|limit|monat|monthly|abgelehnt/i)
|
|
).toBeVisible({ timeout: 5000 })
|
|
})
|
|
|
|
// Requires: backend quota enforcement
|
|
test("accepts near-quota member within daily limit", async ({ page }) => {
|
|
// Thomas has 23g already this day — 2g more should be fine (25g total)
|
|
await page.goto("/distributions/new")
|
|
|
|
const memberSelect = page
|
|
.getByLabel(/mitglied|member/i)
|
|
.or(page.locator("[data-testid='distribution-member-select']"))
|
|
await memberSelect.click()
|
|
await page.getByText(SEED.members.thomas.name).click()
|
|
|
|
const strainSelect = page
|
|
.getByLabel(/sorte|strain|charge|batch/i)
|
|
.or(page.locator("[data-testid='distribution-strain-select']"))
|
|
await strainSelect.click()
|
|
await page.getByText(SEED.strains.northernLights.name).click()
|
|
|
|
const amountInput = page
|
|
.getByLabel(/menge|amount|gramm/i)
|
|
.or(page.locator("input[name*='amount']"))
|
|
await amountInput.fill("2")
|
|
|
|
const submitBtn = page.getByRole("button", {
|
|
name: /ausgeben|submit|speichern/i,
|
|
})
|
|
await submitBtn.click()
|
|
|
|
// Should succeed (23g + 2g = 25g, exactly at limit)
|
|
await expect(
|
|
page.getByText(/erfolg|success|gespeichert/i)
|
|
).toBeVisible({ timeout: 5000 })
|
|
})
|
|
|
|
// Requires: backend quota enforcement
|
|
test("rejects near-quota member exceeding daily cumulative", async ({
|
|
page,
|
|
}) => {
|
|
// Thomas has 23g already — 3g more would be 26g (exceeds 25g/day)
|
|
await page.goto("/distributions/new")
|
|
|
|
const memberSelect = page
|
|
.getByLabel(/mitglied|member/i)
|
|
.or(page.locator("[data-testid='distribution-member-select']"))
|
|
await memberSelect.click()
|
|
await page.getByText(SEED.members.thomas.name).click()
|
|
|
|
const strainSelect = page
|
|
.getByLabel(/sorte|strain|charge|batch/i)
|
|
.or(page.locator("[data-testid='distribution-strain-select']"))
|
|
await strainSelect.click()
|
|
await page.getByText(SEED.strains.northernLights.name).click()
|
|
|
|
const amountInput = page
|
|
.getByLabel(/menge|amount|gramm/i)
|
|
.or(page.locator("input[name*='amount']"))
|
|
await amountInput.fill("3")
|
|
|
|
const submitBtn = page.getByRole("button", {
|
|
name: /ausgeben|submit|speichern/i,
|
|
})
|
|
await submitBtn.click()
|
|
|
|
// Should show daily cumulative rejection
|
|
await expect(
|
|
page.getByText(/überschr|exceeded|limit|abgelehnt|rejected/i)
|
|
).toBeVisible({ timeout: 5000 })
|
|
})
|
|
|
|
// Requires: backend quota enforcement
|
|
test("shows THC warning for under-21 members on distribution page", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/distributions/new")
|
|
|
|
// Select under-21 member (Jonas)
|
|
const memberSelect = page
|
|
.getByLabel(/mitglied|member/i)
|
|
.or(page.locator("[data-testid='distribution-member-select']"))
|
|
await memberSelect.click()
|
|
await page.getByText(SEED.members.jonas.name).click()
|
|
|
|
// Should show THC% warning/info for under-21
|
|
await expect(
|
|
page.getByText(/thc.*10|unter.*21|u21|jugendschutz/i).first()
|
|
).toBeVisible({ timeout: 3000 })
|
|
})
|
|
|
|
// Requires: backend quota enforcement
|
|
test("quota display shows correct remaining amount", async ({ page }) => {
|
|
await page.goto("/distributions/new")
|
|
|
|
// Select Thomas (near-quota member, 23g already used today)
|
|
const memberSelect = page
|
|
.getByLabel(/mitglied|member/i)
|
|
.or(page.locator("[data-testid='distribution-member-select']"))
|
|
await memberSelect.click()
|
|
await page.getByText(SEED.members.thomas.name).click()
|
|
|
|
// Should display remaining quota info
|
|
await expect(
|
|
page
|
|
.getByText(/verbleibend|remaining|rest|kontingent|quota/i)
|
|
.first()
|
|
.or(page.locator("[data-testid*='quota']").first())
|
|
).toBeVisible({ timeout: 3000 })
|
|
})
|
|
})
|