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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
24a799d6ba
commit
126ad93301
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Integrationstest gegen eine echte Datenbank.
|
||||
*
|
||||
* Prüft, was Vitest nicht abdeckt: Eindeutigkeit, Fremdschlüssel, Kaskaden
|
||||
* und dass das Audit-Protokoll den Benutzer überlebt.
|
||||
*
|
||||
* Bewusst ohne umschließende Transaktion: Der Test 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 — auch wenn der Test unterwegs abbricht.
|
||||
*
|
||||
* Aufruf: npm run test:integration
|
||||
*/
|
||||
import 'dotenv/config'
|
||||
import { PrismaPg } from '@prisma/adapter-pg'
|
||||
import { PrismaClient } from '../src/generated/prisma/client.js'
|
||||
|
||||
const verbindung = process.env.DATABASE_URL
|
||||
if (!verbindung) {
|
||||
console.error('DATABASE_URL ist nicht gesetzt.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: verbindung }) })
|
||||
|
||||
type Pruefung = { name: string; ok: boolean; hinweis?: string }
|
||||
const ergebnisse: Pruefung[] = []
|
||||
|
||||
function pruefe(name: string, bedingung: boolean, hinweis?: string): void {
|
||||
ergebnisse.push(hinweis ? { name, ok: bedingung, hinweis } : { name, ok: bedingung })
|
||||
}
|
||||
|
||||
/** Erwartet, dass die Aktion scheitert. */
|
||||
async function scheitert(name: string, tun: () => Promise<unknown>): Promise<void> {
|
||||
try {
|
||||
await tun()
|
||||
pruefe(name, false, 'Die Aktion war erlaubt, obwohl sie scheitern sollte.')
|
||||
} catch {
|
||||
pruefe(name, true)
|
||||
}
|
||||
}
|
||||
|
||||
const kennung = `zzz-test-${Date.now()}`
|
||||
const testEmail = `${kennung}@example.invalid`
|
||||
|
||||
async function aufraeumen(): Promise<void> {
|
||||
// Reihenfolge egal, die Kaskaden erledigen den Rest; der Audit-Eintrag
|
||||
// hängt bewusst an keinem Benutzer mehr.
|
||||
await db.auditEintrag.deleteMany({ where: { benutzerEmail: testEmail } })
|
||||
await db.benutzer.deleteMany({ where: { email: testEmail } })
|
||||
await db.bereich.deleteMany({ where: { name: { startsWith: kennung } } })
|
||||
await db.webauthnChallenge.deleteMany({ where: { kennung } })
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
// --- Benutzer und Eindeutigkeit der Adresse ------------------------------
|
||||
const benutzer = await db.benutzer.create({
|
||||
data: { email: testEmail, name: 'Testkonto' },
|
||||
})
|
||||
pruefe('Benutzer anlegen', benutzer.id.length > 0)
|
||||
pruefe('Rolle ist im Zweifel BENUTZER', benutzer.rolle === 'BENUTZER')
|
||||
pruefe('Konto ist im Zweifel aktiv', benutzer.aktiv === true)
|
||||
|
||||
await scheitert('E-Mail-Adresse ist eindeutig', () =>
|
||||
db.benutzer.create({ data: { email: testEmail } }),
|
||||
)
|
||||
|
||||
// --- Bereich und Eindeutigkeit des Namens --------------------------------
|
||||
const bereich = await db.bereich.create({
|
||||
data: { name: `${kennung}-bereich`, farbe: '#2563eb', kapazitaetMo: 120 },
|
||||
})
|
||||
pruefe('Bereich anlegen', bereich.name.endsWith('-bereich'))
|
||||
pruefe(
|
||||
'Arbeitsfenster hat brauchbare Standardwerte',
|
||||
bereich.arbeitsfensterVon === 480 && bereich.arbeitsfensterBis === 1020,
|
||||
)
|
||||
|
||||
await scheitert('Bereichsname ist eindeutig', () =>
|
||||
db.bereich.create({ data: { name: bereich.name, farbe: '#000000' } }),
|
||||
)
|
||||
|
||||
// --- Passkey, Eindeutigkeit und Fremdschlüssel ---------------------------
|
||||
const passkey = await db.passkey.create({
|
||||
data: {
|
||||
benutzerId: benutzer.id,
|
||||
credentialId: `${kennung}-cred`,
|
||||
publicKey: Buffer.from([1, 2, 3]),
|
||||
bezeichnung: 'Testgerät',
|
||||
},
|
||||
})
|
||||
pruefe('Passkey anlegen', passkey.counter === 0n)
|
||||
pruefe(
|
||||
'Zähler ist ein BigInt und beginnt bei null',
|
||||
typeof passkey.counter === 'bigint',
|
||||
)
|
||||
|
||||
await scheitert('Credential-ID ist eindeutig', () =>
|
||||
db.passkey.create({
|
||||
data: {
|
||||
benutzerId: benutzer.id,
|
||||
credentialId: passkey.credentialId,
|
||||
publicKey: Buffer.from([4]),
|
||||
bezeichnung: 'Zweitgerät',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await scheitert('Passkey ohne gültigen Benutzer ist nicht möglich', () =>
|
||||
db.passkey.create({
|
||||
data: {
|
||||
benutzerId: 'gibt-es-nicht',
|
||||
credentialId: `${kennung}-waise`,
|
||||
publicKey: Buffer.from([9]),
|
||||
bezeichnung: 'Waise',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// --- Öffentlicher Schlüssel kommt unverändert zurück ---------------------
|
||||
const gelesen = await db.passkey.findUnique({ where: { id: passkey.id } })
|
||||
pruefe(
|
||||
'Der öffentliche Schlüssel kommt Byte für Byte zurück',
|
||||
Buffer.from(gelesen?.publicKey ?? []).equals(Buffer.from([1, 2, 3])),
|
||||
)
|
||||
|
||||
// --- Konto und Eindeutigkeit je Anbieter ---------------------------------
|
||||
await db.konto.create({
|
||||
data: {
|
||||
benutzerId: benutzer.id,
|
||||
type: 'oidc',
|
||||
provider: 'oidc',
|
||||
providerAccountId: kennung,
|
||||
},
|
||||
})
|
||||
await scheitert('Ein Anbieterkonto ist nur einmal verknüpfbar', () =>
|
||||
db.konto.create({
|
||||
data: {
|
||||
benutzerId: benutzer.id,
|
||||
type: 'oidc',
|
||||
provider: 'oidc',
|
||||
providerAccountId: kennung,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// --- Audit überlebt den Benutzer ----------------------------------------
|
||||
const audit = await db.auditEintrag.create({
|
||||
data: {
|
||||
benutzerId: benutzer.id,
|
||||
benutzerEmail: benutzer.email,
|
||||
aktion: 'test.durchgefuehrt',
|
||||
objektTyp: 'Bereich',
|
||||
objektId: bereich.id,
|
||||
details: { name: bereich.name },
|
||||
instanz: 'daheim',
|
||||
},
|
||||
})
|
||||
pruefe('Audit-Eintrag anlegen', audit.id.length > 0)
|
||||
|
||||
await db.benutzer.delete({ where: { id: benutzer.id } })
|
||||
|
||||
const nachher = await db.auditEintrag.findUnique({ where: { id: audit.id } })
|
||||
pruefe('Audit-Eintrag überlebt das Löschen des Benutzers', nachher !== null)
|
||||
pruefe(
|
||||
'Die Kennung wird gelöst, die Adresse bleibt als Nachweis stehen',
|
||||
nachher?.benutzerId === null && nachher?.benutzerEmail === testEmail,
|
||||
)
|
||||
|
||||
pruefe(
|
||||
'Passkeys verschwinden mit dem Benutzer',
|
||||
(await db.passkey.count({ where: { credentialId: passkey.credentialId } })) === 0,
|
||||
)
|
||||
pruefe(
|
||||
'Verknüpfte Konten verschwinden mit dem Benutzer',
|
||||
(await db.konto.count({ where: { providerAccountId: kennung } })) === 0,
|
||||
)
|
||||
}
|
||||
|
||||
let abbruch: unknown = null
|
||||
|
||||
main()
|
||||
.catch((fehler: unknown) => {
|
||||
abbruch = fehler
|
||||
})
|
||||
.finally(async () => {
|
||||
await aufraeumen()
|
||||
|
||||
const uebrig =
|
||||
(await db.benutzer.count({ where: { email: testEmail } })) +
|
||||
(await db.bereich.count({ where: { name: { startsWith: kennung } } }))
|
||||
pruefe('Aufräumen hat nichts übrig gelassen', uebrig === 0)
|
||||
|
||||
await db.$disconnect()
|
||||
|
||||
if (abbruch) {
|
||||
console.error('Der Integrationstest ist abgebrochen:', abbruch)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const gefallen = ergebnisse.filter((e) => !e.ok)
|
||||
for (const e of ergebnisse) {
|
||||
console.log(`${e.ok ? '✓' : '✗'} ${e.name}${e.hinweis ? ` – ${e.hinweis}` : ''}`)
|
||||
}
|
||||
console.log(
|
||||
`\n${ergebnisse.length - gefallen.length} von ${ergebnisse.length} Prüfungen bestanden.`,
|
||||
)
|
||||
process.exit(gefallen.length === 0 ? 0 : 1)
|
||||
})
|
||||
Reference in New Issue
Block a user