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()