Phase 10: Mandantenfähigkeit (#15)

This commit was merged in pull request #15.
This commit is contained in:
2026-09-05 13:47:46 +00:00
parent 2b53556175
commit 91742a0dd7
98 changed files with 3810 additions and 201 deletions
+91 -1
View File
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { RolleSchluessel } from "@prisma/client";
import {
ALLE_RECHTE,
KeinZugriffFehler,
@@ -24,7 +25,10 @@ import {
* Terminal nur das Nötigste kann.
*/
function sitzung(rollen: Sitzung["rollen"], organisationId = "org-1"): Sitzung {
function sitzung(
rollen: Sitzung["rollen"],
organisationId = "org-1",
): Sitzung & { personId: string } {
return {
personId: "person-1",
organisationId,
@@ -34,6 +38,23 @@ function sitzung(rollen: Sitzung["rollen"], organisationId = "org-1"): Sitzung {
};
}
/**
* Eine Sitzung der Kreisaufsicht: kein Konto in dieser Wehr, keine Akte.
*
* Seit Phase 10 gibt es diesen Zustand, und er ist der einzige, in dem
* `personId` fehlt.
*/
function aufsicht(organisationId = "org-2"): Sitzung {
return {
personId: null,
organisationId,
name: "Kreisbrandinspektion",
rollen: ["AUFSICHT"],
anmeldeArt: "LOKAL",
nurLesend: true,
};
}
describe("Rollenkatalog", () => {
it("hat für jede Rolle eine deutsche Bezeichnung", () => {
for (const rolle of ROLLEN) {
@@ -195,3 +216,72 @@ describe("alsRolle", () => {
expect(alsRolle(null)).toBeNull();
});
});
describe("Kreisaufsicht", () => {
/**
* Der wichtigste Test dieser Datei.
*
* Die Kreisebene sieht Wehren, denen sie nicht angehört. Bekäme sie dort
* versehentlich ein Schreibrecht, wäre das kein Bedienfehler, sondern ein
* Loch in der Mandantentrennung — und zwar eines, das niemandem auffällt,
* weil in der Oberfläche alles normal aussieht.
*
* Die Zusicherung ist deshalb bewusst allgemein formuliert: nicht "diese
* zwölf Rechte", sondern "kein einziges, das nicht lesen heißt". So gilt
* sie auch für Module, die es noch gar nicht gibt.
*/
it("hat ausschließlich Leserechte — auch für künftige Module", () => {
const rechte = [...rechteVon(["AUFSICHT"])];
expect(rechte.length).toBeGreaterThan(0);
expect(rechte.filter((recht) => !recht.endsWith(".lesen"))).toEqual([]);
});
it("sieht die Kasse nicht", () => {
// Sie gehört dem Verein oder der Kommune, nicht der Kreisaufsicht — und
// wäre nur deshalb dabei, weil das Recht zufällig auf ".lesen" endet.
expect(hatRecht(aufsicht(), "kasse.lesen")).toBe(false);
});
it("sieht weder Audit-Log noch KI-Protokoll", () => {
// Wer wann was getan hat, bleibt innerhalb der Wehr.
expect(hatRecht(aufsicht(), "system.auditlog")).toBe(false);
expect(hatRecht(aufsicht(), "ki.protokoll")).toBe(false);
});
it("kann keine Personendaten exportieren", () => {
// Das Auskunftsrecht nach DSGVO erfüllt die Wehr, die die Daten führt.
expect(hatRecht(aufsicht(), "person.export")).toBe(false);
});
it("sieht den Atemschutz — dafür gibt es sie", () => {
expect(hatRecht(aufsicht(), "atemschutz.lesen")).toBe(true);
expect(hatRecht(aufsicht(), "atemschutz.bearbeiten")).toBe(false);
expect(hatRecht(aufsicht(), "atemschutz.ueberwachung")).toBe(false);
});
it("hat in dieser Wehr keine eigene Akte", () => {
// Daran hängt, dass sie in keiner Mitgliederliste auftaucht und als
// Urheber keiner Änderung eingetragen werden kann.
expect(aufsicht().personId).toBeNull();
expect(aufsicht().nurLesend).toBe(true);
});
it("sieht fremde Personen nur über person.lesen, nie über die eigene Akte", () => {
// `darfPersonSehen` lässt jeden die eigenen Daten sehen. Ohne Akte darf
// dieser Weg nicht versehentlich greifen: sonst entschiede ein Vergleich
// von null mit null darüber, wer wen sieht.
const kreis = aufsicht();
expect(darfPersonSehen(kreis, "irgendeine-person")).toBe(true);
expect(hatRecht(kreis, "person.lesen")).toBe(true);
});
});
describe("Rollenkatalog und Mandantentrennung", () => {
it("führt AUFSICHT nicht als vergebbare Rolle in der Datenbank", () => {
// Sie ergibt sich aus der Mitgliedschaftsart, nicht aus einer Zuweisung.
// Wäre sie vergebbar, könnte jemand innerhalb einer Wehr Rechte an einer
// anderen verteilen.
expect(RolleSchluessel).not.toHaveProperty("AUFSICHT");
});
});