Die Stammdaten, an denen die Historie haengt. Personen werden deaktiviert, Geraete und Abteilungen archiviert — geloescht wird nichts, woran ein Lauf haengen kann. - Mitarbeiterliste mit Suche ueber Anzeigename, Mail, UPN und sAMAccountName, Filter nach Abteilung, standardmaessig nur aktive; die Suche ist ein eigenes Bauteil, weil #013 und #014 sie wieder brauchen - Personenseite mit Stammdaten, Geraeten, Vorgesetztem; was aus dem Verzeichnis kommt, ist gesperrt — der naechste Abgleich wuerde es ohnehin zurueckdrehen - Abteilungen unter Admin: Ziel-OU, Standardgruppen, Mail-Domain, dazu die Namen, die bei Personen vorkommen, aber nicht gepflegt sind - Geraete mit Suche ueber Hostname, Bezeichnung, Seriennummer und Tag-Code; CSV-Import mit einem Ergebnis je Zeile - Neues Recht mitarbeiterBearbeiten; die Seitenleiste fuehrt Personen und Geraete unter einem Eintrag, im Modus Dienstleistung anders beschriftet Der unabhaengige Review fand zwei Blocker. Der schwerere meldete Erfolg: ein nicht geschlossenes Anfuehrungszeichen verschluckte im CSV-Import alle Folgezeilen bis zum naechsten `"`, und der Import sagte „3 von 3 Zeilen angelegt". Ein Wert reicht jetzt nicht mehr ueber die Zeile hinaus — fuer Hostnamen und Seriennummern ist ein Umbruch praktisch immer ein vergessenes Anfuehrungszeichen, und so endet der Schaden an seiner Zeile. Der zweite: ungepflegte Abteilungen wurden ueber alle Kunden hinweg gruppiert. Dazu sieben weitere Befunde, alle behoben — darunter eine Besitzerauswahl, die Personen fremder Kunden anbot, und ein Techniker, der ein Geraet endgueltig loeschen konnte. 610 Tests gruen, 18 E2E-Ablaeufe gegen den gebauten Stand. Schliesst #011 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
145 lines
6.0 KiB
TypeScript
145 lines
6.0 KiB
TypeScript
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<Recht, Record<Rolle, boolean>> = {
|
|
lesen: { ADMIN: true, REDAKTEUR: true, TECHNIKER: true, LESER: true },
|
|
laeufeBearbeiten: { ADMIN: true, REDAKTEUR: true, TECHNIKER: true, LESER: false },
|
|
geraeteBearbeiten: { ADMIN: true, REDAKTEUR: true, TECHNIKER: true, LESER: false },
|
|
mitarbeiterBearbeiten: { ADMIN: true, REDAKTEUR: true, TECHNIKER: true, LESER: false },
|
|
vorlagenBearbeiten: { ADMIN: true, REDAKTEUR: true, TECHNIKER: false, LESER: false },
|
|
archivVerwalten: { ADMIN: true, REDAKTEUR: false, TECHNIKER: false, LESER: false },
|
|
verwaltung: { ADMIN: true, REDAKTEUR: false, TECHNIKER: false, LESER: 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 unten wirklich vollstaendig ist: vier Rollen, sieben Rechte.
|
|
expect(ALLE_ROLLEN).toHaveLength(4);
|
|
expect(ALLE_RECHTE).toHaveLength(7);
|
|
expect(faelle).toHaveLength(28);
|
|
});
|
|
|
|
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", "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", () => {
|
|
for (const recht of ALLE_RECHTE) {
|
|
expect(darf("ADMIN", recht), `ADMIN darf ${recht}`).toBe(true);
|
|
}
|
|
});
|
|
|
|
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 LESER 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.LESER);
|
|
expect(ROLLEN_RANG.ADMIN).toBeGreaterThan(ROLLEN_RANG.REDAKTEUR);
|
|
expect(ROLLEN_RANG.REDAKTEUR).toBeGreaterThan(ROLLEN_RANG.TECHNIKER);
|
|
expect(ROLLEN_RANG.TECHNIKER).toBeGreaterThan(ROLLEN_RANG.LESER);
|
|
});
|
|
});
|
|
|
|
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()));
|
|
});
|
|
});
|