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
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user