From 6c32ba1a6a075ca67129ba8eb732ab22b8c5098f Mon Sep 17 00:00:00 2001 From: Harlekin Date: Tue, 1 Sep 2026 21:11:40 +0200 Subject: [PATCH] Tests schreiben nicht mehr in die Entwicklungsdatenbank MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Testläufe hatten dort 70 erfundene Rollen, 112 Nutzer und 40 Akten hinterlassen. Die Rollenverwaltung zeigte deshalb "Gast" siebzehnmal, echte Rollen doppelt und in willkürlicher Reihenfolge. Aufräumen allein reicht nicht: CaseEntry, CaseEntryVersion und AuditLog lassen sich nach harter Regel 2 nicht löschen. Was ein Test dort anlegt, bleibt für immer stehen — in Phase 2 hätte das die Fallliste unbrauchbar gemacht. Tests laufen deshalb gegen _test auf demselben Server. pnpm test legt sie an, migriert und seedet sie vorher. Über TEST_DATABASE_URL lässt sich ein anderes Ziel setzen. Dazu die Abschnitte der Rollenverwaltung in der Reihenfolge, in der die Abteilung ihre Rollen aufzählt: Ränge, Zugehörigkeit, Zugang. Innerhalb eines Abschnitts entscheidet die Position auf dem Discord-Server, oben die höchste. Damit steht die Hierarchie an einer Stelle, die ohnehin gepflegt wird. --- .github/workflows/ci.yml | 3 +- README.md | 3 ++ package.json | 5 ++- packages/db/package.json | 1 + packages/db/scripts/prepare-test-db.mjs | 54 +++++++++++++++++++++++++ packages/db/src/index.ts | 1 + packages/db/src/testDatabase.ts | 44 ++++++++++++++++++++ packages/db/tests/setup.ts | 5 +-- web/app/(app)/admin/rollen/page.tsx | 9 ++++- web/tests/setup.ts | 5 +++ worker/tests/setup.ts | 5 +-- 11 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 packages/db/scripts/prepare-test-db.mjs create mode 100644 packages/db/src/testDatabase.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 987584b..a208d9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,8 @@ jobs: - run: pnpm typecheck - # Die Regeltests in packages/db laufen gegen die echte Postgres-Instanz. + # Die Regeltests laufen gegen eine echte Postgres-Instanz, aber gegen + # eine eigene Datenbank: siehe docs/13-entscheidungen.md. - run: pnpm test - name: Playwright-Browser diff --git a/README.md b/README.md index a6f6c7d..9cb5673 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,9 @@ pnpm lint && pnpm typecheck && pnpm test pnpm test:e2e ``` +`pnpm test` legt sich eine eigene Datenbank `afis_test` an und lässt die +Entwicklungsdatenbank in Ruhe. Grund in `docs/13-entscheidungen.md`. + ## Betrieb kurz gefasst | | | diff --git a/package.json b/package.json index 8fdaad8..edc405e 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,12 @@ "db:deploy": "pnpm --filter @afis/db run migrate:deploy", "db:seed": "pnpm --filter @afis/db run seed", "db:studio": "pnpm --filter @afis/db run studio", - "test": "pnpm -r --sequential test", + "test": "pnpm db:test:prepare && pnpm -r --sequential test", "test:e2e": "pnpm --filter @afis/web run test:e2e", "lint": "pnpm -r --sequential lint", "typecheck": "pnpm -r --sequential typecheck", - "analyze": "pnpm --filter @afis/web run analyze" + "analyze": "pnpm --filter @afis/web run analyze", + "db:test:prepare": "pnpm --filter @afis/db run build && pnpm --filter @afis/db run test:prepare" }, "devDependencies": { "typescript": "5.9.3" diff --git a/packages/db/package.json b/packages/db/package.json index fe45c9c..fe7c9ef 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -22,6 +22,7 @@ "seed": "tsx prisma/seed.ts", "studio": "prisma studio", "test": "vitest run --passWithNoTests", + "test:prepare": "node scripts/prepare-test-db.mjs", "typecheck": "tsc --noEmit" }, "prisma": { diff --git a/packages/db/scripts/prepare-test-db.mjs b/packages/db/scripts/prepare-test-db.mjs new file mode 100644 index 0000000..635cab1 --- /dev/null +++ b/packages/db/scripts/prepare-test-db.mjs @@ -0,0 +1,54 @@ +import { execFileSync } from 'node:child_process' +import { createRequire } from 'node:module' +import { fileURLToPath } from 'node:url' +import pg from 'pg' +import { loadEnvFromRepoRoot } from '../dist/loadEnv.js' +import { resolveTestDatabaseUrl } from '../dist/testDatabase.js' + +/** + * Legt die Testdatenbank an, falls sie fehlt, und bringt sie auf den aktuellen + * Migrationsstand. Wird von `pnpm test` vorgeschaltet. + * + * Läuft nur in der Entwicklung und in der CI, nie im Betrieb. + */ +loadEnvFromRepoRoot() + +const testUrl = resolveTestDatabaseUrl() +const dbName = new URL(testUrl).pathname.replace(/^\//, '') + +const maintenance = new URL(testUrl) +maintenance.pathname = '/postgres' + +const client = new pg.Client({ connectionString: maintenance.toString() }) +await client.connect() +try { + const { rowCount } = await client.query('SELECT 1 FROM pg_database WHERE datname = $1', [dbName]) + if (rowCount === 0) { + // Bezeichner lassen sich nicht als Parameter binden, deshalb quotiert. + await client.query(`CREATE DATABASE "${dbName.replace(/"/g, '""')}"`) + console.log(`[test-db] ${dbName} angelegt`) + } +} finally { + await client.end() +} + +const require = createRequire(import.meta.url) +const prismaCli = require.resolve('prisma/build/index.js') + +const testEnv = { ...process.env, DATABASE_URL: testUrl, DIRECT_DATABASE_URL: testUrl } + +execFileSync(process.execPath, [prismaCli, 'migrate', 'deploy'], { + stdio: 'inherit', + env: testEnv, +}) + +// Der Auslieferungszustand gehört auch in die Testdatenbank: Freigabestufen und +// Änderungsrichtlinie werden von Tests vorausgesetzt. Der Seed ist idempotent. +const tsxCli = require.resolve('tsx/cli') +execFileSync(process.execPath, [tsxCli, 'prisma/seed.ts'], { + stdio: 'inherit', + cwd: fileURLToPath(new URL('..', import.meta.url)), + env: testEnv, +}) + +console.log(`[test-db] ${dbName} ist auf Stand`) diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 6a0c2e2..3b44649 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -10,6 +10,7 @@ import { loadEnvFromRepoRoot } from './loadEnv.js' */ export * from './generated/client.js' export { loadEnvFromRepoRoot } +export { resolveTestDatabaseUrl, switchToTestDatabase } from './testDatabase.js' /** * Erzeugt einen Prisma-Client. diff --git a/packages/db/src/testDatabase.ts b/packages/db/src/testDatabase.ts new file mode 100644 index 0000000..b4b08c2 --- /dev/null +++ b/packages/db/src/testDatabase.ts @@ -0,0 +1,44 @@ +/** + * Tests laufen gegen eine **eigene** Datenbank, nicht gegen die der + * Entwicklung. + * + * Der Grund ist nicht Ordnungsliebe: `CaseEntry`, `CaseEntryVersion` und + * `AuditLog` lassen sich nach Regel 2 aus CLAUDE.md nicht löschen. Was ein Test + * dort anlegt, bleibt für immer stehen. In einer gemeinsamen Datenbank + * verstopfen die Testakten binnen weniger Läufe die Fallliste, und aufräumen + * lässt sich das nicht mehr. + * + * Ohne `TEST_DATABASE_URL` wird der Name der Entwicklungsdatenbank um `_test` + * ergänzt. Sie liegt also auf demselben Server, aber getrennt. + */ +export function resolveTestDatabaseUrl(source = process.env.DATABASE_URL): string { + const explicit = process.env.TEST_DATABASE_URL + if (explicit) return explicit + + if (!source) { + throw new Error('Konfiguration unvollständig: DATABASE_URL oder TEST_DATABASE_URL fehlt') + } + + const url = new URL(source) + const name = url.pathname.replace(/^\//, '') + if (!name) throw new Error(`DATABASE_URL nennt keine Datenbank: ${source}`) + if (name.endsWith('_test')) return url.toString() + + url.pathname = `/${name}_test` + // pgbouncer im Transaktionsmodus taugt nicht zum Anlegen von Datenbanken + // und nicht für LISTEN. Für Tests wird direkt verbunden. + url.searchParams.delete('pgbouncer') + return url.toString() +} + +/** + * Setzt die Umgebung eines Testlaufs auf die Testdatenbank um. Muss laufen, + * bevor irgendein Modul einen Prisma-Client baut — deshalb gehört der Aufruf + * in die `setupFiles` von Vitest, nicht in eine einzelne Testdatei. + */ +export function switchToTestDatabase(): string { + const url = resolveTestDatabaseUrl() + process.env.DATABASE_URL = url + process.env.DIRECT_DATABASE_URL = url + return url +} diff --git a/packages/db/tests/setup.ts b/packages/db/tests/setup.ts index 295d090..1753f82 100644 --- a/packages/db/tests/setup.ts +++ b/packages/db/tests/setup.ts @@ -1,5 +1,4 @@ -import { loadEnvFromRepoRoot } from '../src/loadEnv.js' +import { loadEnvFromRepoRoot, switchToTestDatabase } from '../src/index.js' -// Damit die Regeltests die lokale Entwicklungsdatenbank finden. In der CI -// stehen die Werte bereits in der Umgebung. loadEnvFromRepoRoot() +switchToTestDatabase() diff --git a/web/app/(app)/admin/rollen/page.tsx b/web/app/(app)/admin/rollen/page.tsx index 624c78d..531410a 100644 --- a/web/app/(app)/admin/rollen/page.tsx +++ b/web/app/(app)/admin/rollen/page.tsx @@ -8,7 +8,14 @@ import { ClearancePreview } from './ClearancePreview' export const metadata: Metadata = { title: de.roleAdmin.heading } export const dynamic = 'force-dynamic' -const ORDER = ['MEMBERSHIP', 'CLEARANCE', 'RANK', 'UNSORTED'] as const +/** + * Reihenfolge der Abschnitte: erst die Ränge, dann die Zugehörigkeit, dann die + * Zugangsrollen — so, wie die Abteilung sie selbst aufzählt. Innerhalb eines + * Abschnitts entscheidet die Position der Rolle auf dem Discord-Server, oben + * die höchste. Damit steht die Hierarchie an einer Stelle, die ohnehin gepflegt + * wird, statt in einer zweiten Sortierung in dieser Anwendung. + */ +const ORDER = ['RANK', 'MEMBERSHIP', 'CLEARANCE', 'UNSORTED'] as const export default async function RoleAdminPage() { // Ohne roles.manage gibt es 404, nicht 403. diff --git a/web/tests/setup.ts b/web/tests/setup.ts index 92fc0e5..d6bcd01 100644 --- a/web/tests/setup.ts +++ b/web/tests/setup.ts @@ -1,3 +1,8 @@ import '@testing-library/dom' +import { loadEnvFromRepoRoot, switchToTestDatabase } from '@afis/db' process.env.TZ = 'Europe/Berlin' + +// Umschalten auf die Testdatenbank, bevor lib/db.ts einen Client baut. +loadEnvFromRepoRoot() +switchToTestDatabase() diff --git a/worker/tests/setup.ts b/worker/tests/setup.ts index c7fd661..8af6bf7 100644 --- a/worker/tests/setup.ts +++ b/worker/tests/setup.ts @@ -1,5 +1,4 @@ -import { loadEnvFromRepoRoot } from '@afis/db' +import { loadEnvFromRepoRoot, switchToTestDatabase } from '@afis/db' -// Damit die Tests die lokale Entwicklungsdatenbank finden. In der CI stehen -// die Werte bereits in der Umgebung. loadEnvFromRepoRoot() +switchToTestDatabase()