test: Spiegel-Eingang und Integrationstabellen absichern

Der Eingang des Spiegels ist die einzige Tuer, die ohne Anmeldung
aufgeht. Vier Faelle: ohne Signatur, falsch signiert, zu alt, und der
gute Fall — bei dem eine nicht mehr geschickte Belegung auch hier
verschwinden muss.

Der Testserver bekommt dafuer dasselbe Geheimnis wie der Test. Eine
Pruefung, die immer uebersprungen wird, prueft nichts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-11 23:58:43 +02:00
co-authored by Claude Opus 5
parent 0d0f8503cc
commit 4da2f3d9c6
4 changed files with 234 additions and 0 deletions
+10
View File
@@ -6,6 +6,16 @@ export const EMAIL = process.env.ADMIN_EMAIL ?? ''
export const PASSWORT = process.env.ADMIN_PASSWORT ?? ''
export const VERBINDUNG = process.env.DATABASE_URL ?? ''
/**
* Geheimnis fuer den Spiegel im Test.
*
* Der Testserver bekommt denselben Wert ueber playwright.config.ts, sonst
* liesse sich die Tuer gar nicht pruefen — und eine Pruefung, die immer
* uebersprungen wird, prueft nichts.
*/
export const SPIEGEL_GEHEIMNIS =
process.env.SPIEGEL_GEHEIMNIS ?? 'e2e-spiegel-geheimnis-nur-lokal'
/**
* Meldet das Testkonto mit Passwort an.
*
+138
View File
@@ -0,0 +1,138 @@
import { createHmac } from 'node:crypto'
import { PrismaPg } from '@prisma/adapter-pg'
import { expect, test } from '@playwright/test'
import { PrismaClient } from '../src/generated/prisma/client.js'
import { SPIEGEL_GEHEIMNIS, VERBINDUNG } from './hilfe'
/**
* Der Eingang des Spiegels.
*
* Hier meldet sich kein Mensch an — die Gegenstelle weist sich allein über
* eine Signatur aus. Deshalb wird hier nicht die Oberfläche geprüft, sondern
* genau diese Tür: Sie muss zu sein, wenn irgendetwas nicht stimmt.
*/
const GEHEIMNIS = SPIEGEL_GEHEIMNIS
const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: VERBINDUNG }) })
const PRAEFIX = `E2E-S-${Date.now()}`
function signieren(koerper: string, geheimnis = GEHEIMNIS): string {
return createHmac('sha256', geheimnis).update(koerper).digest('hex')
}
function sendung(teile: Record<string, unknown> = {}): string {
return JSON.stringify({
gesendetAm: new Date().toISOString(),
belegungen: [],
aufgaben: [],
...teile,
})
}
test.describe('Spiegel', () => {
test.afterAll(async () => {
await db.spiegelbelegung.deleteMany({ where: { quelleRef: { startsWith: PRAEFIX } } })
await db.aufgabe.deleteMany({ where: { quelleRef: { startsWith: PRAEFIX } } })
await db.auditEintrag.deleteMany({ where: { aktion: 'spiegel.empfangen' } })
await db.$disconnect()
})
test('weist eine Sendung ohne Signatur ab', async ({ request }) => {
const antwort = await request.post('/api/spiegel', {
headers: { 'Content-Type': 'application/json' },
data: sendung(),
})
expect(antwort.status()).toBe(401)
})
test('weist eine falsch signierte Sendung ab', async ({ request }) => {
const koerper = sendung()
const antwort = await request.post('/api/spiegel', {
headers: {
'Content-Type': 'application/json',
'X-Planer-Signatur': signieren(koerper, 'das-ist-nicht-das-geheimnis'),
},
data: koerper,
})
expect(antwort.status()).toBe(401)
})
test('weist eine Sendung ab, die zu lange unterwegs war', async ({ request }) => {
test.skip(!GEHEIMNIS, 'Ohne SPIEGEL_GEHEIMNIS ist der Eingang ohnehin zu.')
// Eine Stunde alt: richtig signiert, trotzdem wertlos. Sonst ließe sich
// ein abgefangenes Paket beliebig oft erneut einspielen.
const koerper = sendung({
gesendetAm: new Date(Date.now() - 3_600_000).toISOString(),
})
const antwort = await request.post('/api/spiegel', {
headers: {
'Content-Type': 'application/json',
'X-Planer-Signatur': signieren(koerper),
},
data: koerper,
})
expect(antwort.status()).toBe(401)
})
test('nimmt eine gültige Sendung an und räumt Veraltetes weg', async ({ request }) => {
test.skip(
!GEHEIMNIS,
'Ohne SPIEGEL_GEHEIMNIS auf dem Testserver lässt sich das nicht prüfen.',
)
const beginn = new Date(Date.now() + 86_400_000)
const ende = new Date(beginn.getTime() + 3_600_000)
const erste = sendung({
belegungen: [
{
ref: `${PRAEFIX}-a`,
beginn: beginn.toISOString(),
ende: ende.toISOString(),
},
],
aufgaben: [{ ref: `${PRAEFIX}-1`, titel: `${PRAEFIX} Angebot schreiben` }],
})
const antwort = await request.post('/api/spiegel', {
headers: {
'Content-Type': 'application/json',
'X-Planer-Signatur': signieren(erste),
},
data: erste,
})
expect(antwort.ok()).toBe(true)
expect(await db.spiegelbelegung.count({ where: { quelleRef: `${PRAEFIX}-a` } })).toBe(
1,
)
const aufgabe = await db.aufgabe.findFirst({
where: { quelle: 'SPIEGEL', quelleRef: `${PRAEFIX}-1` },
select: { titel: true },
})
expect(aufgabe?.titel).toBe(`${PRAEFIX} Angebot schreiben`)
// Zweite Runde ohne die Belegung: Sie wurde drüben ausgeplant und muss
// auch hier verschwinden — sonst stünde für immer eine Sperre, die es
// längst nicht mehr gibt.
const zweite = sendung({ aufgaben: [] })
const nachher = await request.post('/api/spiegel', {
headers: {
'Content-Type': 'application/json',
'X-Planer-Signatur': signieren(zweite),
},
data: zweite,
})
expect(nachher.ok()).toBe(true)
expect(await db.spiegelbelegung.count({ where: { quelleRef: `${PRAEFIX}-a` } })).toBe(
0,
)
// Die Aufgabe bleibt: Sie ist Planung, keine Sperre.
expect(
await db.aufgabe.count({ where: { quelle: 'SPIEGEL', quelleRef: `${PRAEFIX}-1` } }),
).toBe(1)
})
})