Files
planer/scripts/migrationstest.ts
Florian WildeandClaude Opus 5 126ad93301 test: Integrations-, Migrations- und E2E-Tests ergänzen
Der Integrationstest prüft gegen eine echte Datenbank, was Vitest nicht
sieht: Eindeutigkeit, Fremdschlüssel, Kaskaden und dass das Protokoll den
gelöschten Benutzer überlebt. Er läuft bewusst ohne umschließende
Transaktion — er prüft absichtlich Fälle, die scheitern müssen, und ein
gescheiterter Befehl bricht in PostgreSQL die ganze Transaktion ab.
Stattdessen tragen alle Testdaten dieselbe Kennung und werden am Ende
wieder entfernt.

Der Migrationstest legt eine Wegwerf-Datenbank an, spielt alle
Migrationen ein und vergleicht das Ergebnis mit dem Schema. Dass er
Abweichungen wirklich bemerkt, wurde mit einem probeweise ergänzten Feld
nachgewiesen.

Der E2E-Rauchtest deckt den Weg ab, der in M1 trägt: Umleitung ohne
Anmeldung samt gemerktem Ziel, Abweisung falscher Zugangsdaten,
erfolgreiche Anmeldung und die Verwaltungsseite.

Dafür neu: eine Middleware, die nur den angefragten Pfad als Kopfzeile
durchreicht. Ein Layout kennt seinen Pfad nicht, braucht ihn aber für die
Rückkehr nach der Anmeldung. Anmeldelogik steht dort bewusst nicht — die
Middleware läuft im Edge-Umfeld ohne Prisma.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 13:10:54 +02:00

122 lines
3.6 KiB
TypeScript

/**
* Migrationstest.
*
* Legt eine Wegwerf-Datenbank an, spielt alle Migrationen der Reihe nach ein
* und prüft anschließend, ob das Ergebnis genau dem Schema entspricht. Damit
* fällt auf, wenn jemand das Schema ändert, ohne eine Migration zu erzeugen —
* beim Aufspielen im Betrieb wäre das sonst erst spät zu merken.
*
* Aufruf: npm run test:migration
*/
import 'dotenv/config'
import { spawnSync } from 'node:child_process'
import { join } from 'node:path'
import { Client } from 'pg'
const WURZEL = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
const verbindung = process.env.DATABASE_URL
if (!verbindung) {
console.error('DATABASE_URL ist nicht gesetzt.')
process.exit(1)
}
const testDatenbank = `planer_migrationstest_${Date.now()}`
/** Dieselbe Verbindung, aber auf eine andere Datenbank gerichtet. */
function mitDatenbank(url: string, name: string): string {
const zerlegt = new URL(url)
zerlegt.pathname = `/${name}`
return zerlegt.toString()
}
function prisma(argumente: string[], zusatz: Record<string, string>): number {
const ergebnis = spawnSync(
process.execPath,
[join(WURZEL, 'node_modules', 'prisma', 'build', 'index.js'), ...argumente],
{
cwd: WURZEL,
stdio: 'inherit',
env: { ...process.env, ...zusatz },
},
)
return ergebnis.status ?? 1
}
async function aufVerwaltungsdatenbank<T>(
tun: (klient: Client) => Promise<T>,
): Promise<T> {
// "postgres" ist auf jedem Server vorhanden und wird hier nur benutzt, um
// die Wegwerf-Datenbank anzulegen und wieder zu entfernen.
const klient = new Client({ connectionString: mitDatenbank(verbindung!, 'postgres') })
await klient.connect()
try {
return await tun(klient)
} finally {
await klient.end()
}
}
async function main(): Promise<number> {
console.log(`▸ Wegwerf-Datenbank ${testDatenbank} anlegen`)
await aufVerwaltungsdatenbank((k) => k.query(`CREATE DATABASE "${testDatenbank}"`))
const testUrl = mitDatenbank(verbindung!, testDatenbank)
console.log('▸ Alle Migrationen einspielen')
if (prisma(['migrate', 'deploy'], { DATABASE_URL: testUrl }) !== 0) {
console.error('✗ Die Migrationen ließen sich nicht einspielen.')
return 1
}
console.log('▸ Ergebnis mit dem Schema vergleichen')
// --exit-code liefert 2, wenn ein Unterschied bleibt.
const abweichung = prisma(
[
'migrate',
'diff',
// Die Quelle ist die eben eingespielte Wegwerf-Datenbank, das Ziel das
// Schema. So braucht der Vergleich keine Schattendatenbank.
'--from-config-datasource',
'--to-schema',
'prisma/schema.prisma',
'--exit-code',
],
{ DATABASE_URL: testUrl },
)
if (abweichung === 2) {
console.error(
'\n✗ Nach dem Einspielen weicht die Datenbank vom Schema ab.\n' +
' Vermutlich fehlt eine Migration: "npx prisma migrate dev --name <name>".',
)
return 1
}
if (abweichung !== 0) {
console.error('✗ Der Vergleich ließ sich nicht durchführen.')
return 1
}
console.log('\n✓ Die Migrationen ergeben genau das Schema.')
return 0
}
let code = 1
try {
code = await main()
} catch (fehler) {
console.error('Der Migrationstest ist abgebrochen:', fehler)
} finally {
await aufVerwaltungsdatenbank(async (k) => {
// Offene Verbindungen trennen, sonst lässt sich die Datenbank nicht löschen.
await k.query(
'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1',
[testDatenbank],
)
await k.query(`DROP DATABASE IF EXISTS "${testDatenbank}"`)
})
console.log(`▸ Wegwerf-Datenbank ${testDatenbank} entfernt`)
}
process.exit(code)