import { describe, expect, it } from "vitest"; import { Rolle } from "@/generated/prisma/enums"; import { RECHTE, ROLLEN_RANG, ROLLEN_TEXT, darf, staerksteRolle, type Recht } from "@/server/auth/rollen"; const ALLE_ROLLEN = Object.values(Rolle); const ALLE_RECHTE = Object.keys(RECHTE) as Recht[]; /** * Die vollstaendige Rechtematrix, absichtlich ausgeschrieben statt aus `RECHTE` berechnet: * nur so faellt auf, wenn jemand einem `LESER` versehentlich Schreibrechte gibt (#007). Ein * Test, der die Tabelle nur gegen sich selbst haelt, wuerde jede Aenderung mitmachen. */ const MATRIX: Record> = { lesen: { ADMIN: true, REDAKTEUR: true, TECHNIKER: true, LESER: true, MITARBEITER: false }, selbstauskunft: { ADMIN: false, REDAKTEUR: false, TECHNIKER: false, LESER: false, MITARBEITER: true }, laeufeBearbeiten: { ADMIN: true, REDAKTEUR: true, TECHNIKER: true, LESER: false, MITARBEITER: false }, geraeteBearbeiten: { ADMIN: true, REDAKTEUR: true, TECHNIKER: true, LESER: false, MITARBEITER: false }, mitarbeiterBearbeiten: { ADMIN: true, REDAKTEUR: true, TECHNIKER: true, LESER: false, MITARBEITER: false, }, vorlagenBearbeiten: { ADMIN: true, REDAKTEUR: true, TECHNIKER: false, LESER: false, MITARBEITER: false }, /* * Die Vier-Augen-Freigabe (#040) liegt bewusst **nicht** beim TECHNIKER: sonst * haengt sie daran, dass zufaellig ein zweiter greifbar ist. */ freigeben: { ADMIN: true, REDAKTEUR: true, TECHNIKER: false, LESER: false, MITARBEITER: false }, archivVerwalten: { ADMIN: true, REDAKTEUR: false, TECHNIKER: false, LESER: false, MITARBEITER: false }, verwaltung: { ADMIN: true, REDAKTEUR: false, TECHNIKER: false, LESER: false, MITARBEITER: false }, }; describe("Rechtematrix", () => { const faelle = ALLE_RECHTE.flatMap((recht) => ALLE_ROLLEN.map((rolle) => [recht, rolle, MATRIX[recht][rolle]] as const), ); it("prueft jede Rolle gegen jedes Recht", () => { // Haelt fest, dass die Tabelle oben wirklich vollstaendig ist: fuenf Rollen, // neun Rechte. Die Zahlen stehen hier ausgeschrieben, damit eine neue Rolle // oder ein neues Recht **hier** auffaellt und nicht erst im Betrieb (#035). expect(ALLE_ROLLEN).toHaveLength(5); expect(ALLE_RECHTE).toHaveLength(9); expect(faelle).toHaveLength(45); }); it.each(faelle)("%s: %s -> %s", (recht, rolle, erwartet) => { expect(darf(rolle, recht)).toBe(erwartet); }); }); describe("Rollen im Einzelnen", () => { it("laesst LESER nur lesen", () => { expect(darf("LESER", "lesen")).toBe(true); for (const recht of ALLE_RECHTE.filter((r) => r !== "lesen")) { expect(darf("LESER", recht), `LESER darf ${recht} nicht`).toBe(false); } }); it("gibt TECHNIKER Laeufe und Geraete, aber keine Vorlagen und keine Verwaltung", () => { expect(darf("TECHNIKER", "laeufeBearbeiten")).toBe(true); expect(darf("TECHNIKER", "geraeteBearbeiten")).toBe(true); expect(darf("TECHNIKER", "vorlagenBearbeiten")).toBe(false); expect(darf("TECHNIKER", "freigeben")).toBe(false); expect(darf("TECHNIKER", "verwaltung")).toBe(false); expect(darf("TECHNIKER", "archivVerwalten")).toBe(false); }); it("gibt REDAKTEUR alles von TECHNIKER plus Vorlagen, aber keine Verwaltung", () => { for (const recht of ALLE_RECHTE.filter((r) => darf("TECHNIKER", r))) { expect(darf("REDAKTEUR", recht), `REDAKTEUR darf ${recht} wie ein TECHNIKER`).toBe(true); } expect(darf("REDAKTEUR", "vorlagenBearbeiten")).toBe(true); expect(darf("REDAKTEUR", "verwaltung")).toBe(false); expect(darf("REDAKTEUR", "archivVerwalten")).toBe(false); }); it("gibt ADMIN alles – ausser der Selbstauskunft", () => { for (const recht of ALLE_RECHTE.filter((r) => r !== "selbstauskunft")) { expect(darf("ADMIN", recht), `ADMIN darf ${recht}`).toBe(true); } /* * Die eine Ausnahme, und sie ist Absicht (#035): `selbstauskunft` ist kein * zusaetzliches Recht, sondern ein **Ersatz** fuer `lesen`. Ein ADMIN sieht * ueber `lesen` ohnehin alles; ihm die Selbstauskunft zu geben waere ein * zweiter Weg zu denselben Daten — und damit eine zweite Stelle, an der * jemand die Einschraenkung auf die eigene Person vergessen kann. */ expect(darf("ADMIN", "selbstauskunft")).toBe(false); }); it("laesst MITARBEITER genau eines und sonst nichts", () => { expect(darf("MITARBEITER", "selbstauskunft")).toBe(true); for (const recht of ALLE_RECHTE.filter((r) => r !== "selbstauskunft")) { expect(darf("MITARBEITER", recht), `MITARBEITER darf ${recht} nicht`).toBe(false); } // Vor allem nicht `lesen`: das waere die Akteneinsicht, die es nicht sein soll. expect(darf("MITARBEITER", "lesen")).toBe(false); }); it("behaelt das Zurueckholen aus dem Archiv beim ADMIN", () => { // Steht als eigener Test da, weil die Zusage "ein archivierter Lauf aendert sich nicht" // daran haengt. expect(RECHTE.archivVerwalten).toStrictEqual(["ADMIN"]); }); }); describe("Aufbau der Tabelle", () => { it("listet fuer jedes Recht mindestens eine Rolle", () => { for (const recht of ALLE_RECHTE) { expect(RECHTE[recht].length, `${recht} ohne Rolle waere fuer niemanden erreichbar`).toBeGreaterThan(0); } }); it("nennt in jedem Recht nur bekannte Rollen, jede hoechstens einmal", () => { for (const recht of ALLE_RECHTE) { const rollen = RECHTE[recht] as readonly Rolle[]; expect(new Set(rollen).size, `${recht} nennt eine Rolle doppelt`).toBe(rollen.length); for (const rolle of rollen) { expect(ALLE_ROLLEN).toContain(rolle); } } }); it("deckt mit ROLLEN_TEXT genau die Rollen des Enums ab", () => { expect(Object.keys(ROLLEN_TEXT).sort()).toStrictEqual([...ALLE_ROLLEN].sort()); for (const rolle of ALLE_ROLLEN) { expect(ROLLEN_TEXT[rolle].length).toBeGreaterThan(0); } }); it("deckt mit ROLLEN_RANG genau die Rollen des Enums ab", () => { expect(Object.keys(ROLLEN_RANG).sort()).toStrictEqual([...ALLE_ROLLEN].sort()); }); it("gibt jeder Rolle einen eigenen Rang, ADMIN oben und MITARBEITER unten", () => { const raenge = ALLE_ROLLEN.map((rolle) => ROLLEN_RANG[rolle]); expect(new Set(raenge).size).toBe(ALLE_ROLLEN.length); expect(Math.max(...raenge)).toBe(ROLLEN_RANG.ADMIN); expect(Math.min(...raenge)).toBe(ROLLEN_RANG.MITARBEITER); expect(ROLLEN_RANG.ADMIN).toBeGreaterThan(ROLLEN_RANG.REDAKTEUR); expect(ROLLEN_RANG.REDAKTEUR).toBeGreaterThan(ROLLEN_RANG.TECHNIKER); expect(ROLLEN_RANG.TECHNIKER).toBeGreaterThan(ROLLEN_RANG.LESER); /* * **Unter LESER.** Der Rang entscheidet in `staerksteRolle`, wer gewinnt, * wenn jemand in mehreren abgebildeten Gruppen steht. Stuende MITARBEITER * darueber, verdraengte eine Selbstauskunft-Gruppe die echte Rolle — und ein * Techniker verlöre seine Rechte, weil er auch Mitarbeiter des Hauses ist. */ expect(ROLLEN_RANG.LESER).toBeGreaterThan(ROLLEN_RANG.MITARBEITER); }); }); describe("staerksteRolle", () => { it("gibt bei leerer Liste LESER zurueck", () => { // Wer in keiner abgebildeten Gruppe ist, soll hineinsehen duerfen -- nicht scheitern // und erst recht nicht ADMIN werden. expect(staerksteRolle([])).toBe("LESER"); }); it.each(ALLE_ROLLEN)("gibt bei nur %s diese Rolle zurueck", (rolle) => { expect(staerksteRolle([rolle])).toBe(rolle); }); it.each([ ["LESER und TECHNIKER", ["LESER", "TECHNIKER"], "TECHNIKER"], ["TECHNIKER und REDAKTEUR", ["TECHNIKER", "REDAKTEUR"], "REDAKTEUR"], ["REDAKTEUR und ADMIN", ["REDAKTEUR", "ADMIN"], "ADMIN"], ["alle vier", ["LESER", "TECHNIKER", "REDAKTEUR", "ADMIN"], "ADMIN"], ["ADMIN zuerst genannt", ["ADMIN", "LESER"], "ADMIN"], ["LESER zuerst genannt", ["LESER", "ADMIN"], "ADMIN"], ["dieselbe Rolle mehrfach", ["TECHNIKER", "TECHNIKER"], "TECHNIKER"], ] as const)("waehlt aus %s die Rolle %s", (_fall, rollen, erwartet) => { expect(staerksteRolle(rollen)).toBe(erwartet); }); it("ist von der Reihenfolge unabhaengig", () => { const rollen: Rolle[] = ["TECHNIKER", "ADMIN", "LESER", "REDAKTEUR"]; expect(staerksteRolle(rollen)).toBe(staerksteRolle([...rollen].reverse())); }); });