Files
betreuungs-cockpit/e2e/fremdakte.spec.ts
T
Florian Wilde 91a9aca0d8 Verweise auf fremde Akten an einer Stelle pruefen
Wenn ein Formular einen Fremdschluessel mitschickt, kommt der aus einer
Auswahlliste im Browser. Der Server darf sich darauf nicht verlassen -
eine veraenderte Anfrage kann jede beliebige Id enthalten.

Alle 22 Dateien unter src/server/actions/ durchgesehen. Das Ergebnis ist
besser als befuerchtet: Bis auf eine Stelle war jeder Verweis geprueft.

  buchung.vorgangId ging ungeprueft durch. Konto, Beleg und Vertrag
  wurden geprueft, der Vorgang nicht.

Die Pruefung liegt jetzt in lib/auth/verweise.ts statt verstreut in den
Aktionen. Die acht erlaubten Ziele sind eine ausgeschriebene Aufzaehlung
und kein dynamischer Tabellenzugriff: So sieht der Typpruefer jede
Abfrage, und welche Tabellen erreichbar sind, ist eine Entscheidung und
keine Nebenwirkung. Mehrere Verweise laufen nebeneinander - bei vier
Verweisen eine Wartezeit statt vier.

Umgestellt: die vier Einzelpruefungen der Buchung, die drei des
Vermoegenspostens, die eigene Hilfsfunktion fuer den Ansprechpartner.
Nicht angefasst wurden die Stellen, die ihre eigene, passendere Form
haben - etwa das Kontaktprotokoll, das beide Verweise parallel laedt und
feldgenaue Meldungen zurueckgibt.

Zum Test: Der erste Entwurf rief die Server Action per fetch nach. Der
waere auch ohne jede Pruefung gruen gewesen, weil Next einen Aufruf ohne
gueltige Action-Id ohnehin ablehnt - er haette nichts bewiesen. Der
jetzige schiebt dem echten Auswahlfeld einen Eintrag mit fremder Id
unter und schickt das Formular normal ab. Gegenprobe mit ausgebauter
Pruefung: faellt um, wie er soll.

Refs #23
2026-08-23 23:20:26 +02:00

117 lines
3.9 KiB
TypeScript

import { expect, test } from '@playwright/test'
import { PrismaClient } from '@prisma/client'
import { anmelden } from './hilfen'
/**
* Verweise über Aktengrenzen hinweg — gegen die echte Anwendung.
*
* Die Auswahllisten im Browser zeigen nur die eigene Akte. Darauf darf
* sich der Server nicht verlassen: Wer das Formular verändert, schickt
* jede beliebige Id.
*
* Genau das wird hier nachgestellt — nicht als nachgebauter Aufruf,
* sondern indem dem echten Auswahlfeld ein Eintrag mit einer fremden Id
* untergeschoben und das Formular ganz normal abgeschickt wird. Nur so
* beweist der Test etwas: Er liefe rot, wenn die Prüfung fehlte.
*
* Solange nur eine betreute Person angelegt ist, wäre der Fehler
* folgenlos. Er bliebe es nicht — ein fremder Name stünde im Briefkopf.
*/
const KENNUNG = 'Prüfhausen Fremdakte'
async function mitDb<T>(arbeit: (db: PrismaClient) => Promise<T>): Promise<T> {
const db = new PrismaClient()
try {
return await arbeit(db)
} finally {
await db.$disconnect()
}
}
async function aufraeumen(): Promise<void> {
await mitDb(async (db) => {
await db.betreute.deleteMany({ where: { nachname: { contains: 'Fremdakte' } } })
})
}
test.describe('Verweise auf eine fremde Akte', () => {
test.beforeAll(async () => {
await aufraeumen()
})
test.afterAll(async () => {
await aufraeumen()
})
test('weist einen Kontakt aus einer anderen Akte ab', async ({ page }) => {
// Zweite Akte mit einem Kontakt darin — der angemeldete Benutzer hat
// auf sie keinen Zugriff.
const fremderKontakt = await mitDb(async (db) => {
const fremde = await db.betreute.create({
data: {
vorname: 'Otto',
nachname: `${KENNUNG} Fremdakte`,
geburtsdatum: new Date(Date.UTC(1940, 0, 1)),
},
})
const kontakt = await db.kontakt.create({
data: {
betreuteId: fremde.id,
name: `${KENNUNG} Fremder Sachbearbeiter`,
rolle: 'SACHBEARBEITER_BEZIRK',
},
})
return kontakt.id
})
await anmelden(page)
// Irgendeinen Vorgang der eigenen Akte öffnen.
const vorgangId = await mitDb(async (db) => {
const vorgang = await db.vorgang.findFirst({
where: { geloeschtAm: null, betreute: { vorname: 'Erika' } },
select: { id: true },
})
return vorgang?.id ?? ''
})
test.skip(vorgangId === '', 'Kein Vorgang in der eigenen Akte.')
await page.goto(`/vorgaenge/${vorgangId}`)
const auswahl = page.locator('select[name="ansprechpartnerId"]')
await auswahl.waitFor()
// Dem echten Auswahlfeld die fremde Id unterschieben und ganz normal
// abschicken — so, wie es ein verändertes Formular täte.
await auswahl.evaluate((feld, id) => {
const eintrag = document.createElement('option')
eintrag.value = id
eintrag.textContent = 'untergeschoben'
;(feld as HTMLSelectElement).append(eintrag)
;(feld as HTMLSelectElement).value = id
}, fremderKontakt)
await page.getByRole('button', { name: 'Speichern' }).click()
// Der Server lehnt ab, mit einer Meldung, die den Grund nennt.
// Grosszuegige Frist: Im Entwicklungsmodus wird die Server Action beim
// ersten Aufruf uebersetzt, und das dauert laenger als die Vorgabe.
await expect(page.getByText(/gehört nicht zu dieser betreuten Person/).first()).toBeVisible({
timeout: 45_000,
})
// Und vor allem: Die Verknüpfung ist nicht entstanden.
const verknuepft = await mitDb((db) =>
db.vorgang.count({ where: { ansprechpartnerId: fremderKontakt } }),
)
expect(verknuepft).toBe(0)
})
test('die Auswahlliste zeigt den fremden Kontakt gar nicht erst', async ({ page }) => {
await anmelden(page)
await page.goto('/vorgaenge')
const inhalt = await page.locator('body').innerText()
expect(inhalt).not.toContain('Fremder Sachbearbeiter')
})
})