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
117 lines
3.9 KiB
TypeScript
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')
|
|
})
|
|
})
|