feat(laeufe): Vier-Augen-Freigabe (#040)
Verlangt eine Vorlagen-Version eine Freigabe, laesst sich ein Lauf erst abschliessen, wenn ein Zweiter ihn freigegeben hat. Das braucht zweierlei, und beides zusammen: das neue Recht `freigeben` (ADMIN und REDAKTEUR, nicht TECHNIKER) und dass man nicht an dem Lauf gearbeitet hat. Als gearbeitet zaehlt: angelegt, einen Schritt gesetzt oder abgeschlossen. Eine Zuweisung allein nicht — waere die Regel hier strenger, blockierte sie kleine Teams ohne Gewinn. Auch ein Administrator kann sich nicht selbst freigeben, und das ist der ganze Sinn. Die Pruefung sitzt in `hatDaranGearbeitet` an einer Stelle, wie `laufMussVeraenderbarSein`; die wichtigste Haelfte — der Ersteller — haelt zusaetzlich ein CHECK in der Datenbank. Zurueckweisen verlangt eine Begruendung, setzt den Lauf auf IN_ARBEIT und nimmt eine erteilte Freigabe zurueck. Kein fuenfter LaufStatus: ein neuer Enum-Wert haette jeden Codepfad betroffen, der heute vier Faelle kennt. Sobald jemand abzuschliessen versucht und die Freigabe fehlt, bekommen die Freigabeberechtigten eine Mail — ein anderer Empfaengerkreis als bei jeder anderen Meldung, und ohne die, die gearbeitet haben. Die Aufforderung entsteht ausserhalb der Transaktion: innerhalb wuerde sie mit dem Fehler zurueckgerollt, und dann wartet ein Lauf auf etwas, von dem niemand weiss. Ein partieller Unique-Index haelt sie bei mehreren Versuchen auf einmal. Die Pflicht haengt an der Version, nicht an der Vorlage: eine veroeffentlichte Version ist unveraenderlich, ein laufender Lauf behaelt also seine Regel. Beim Bearbeiten und Duplizieren wandert sie mit — das war die unmittelbare Lehre aus #039, wo die tiefe Kopie zwei Felder stillschweigend verworfen hatte. 1526 Vitest-Tests (vorher 1496), 115 E2E-Ablaeufe gruen. Klickprobe am gebauten Stand mit elf Pruefungen; drei rote Punkte darin waren meine eigenen Testfehler, einer davon lehrreich: ein reload() direkt nach einer Server Action trifft den Router-Cache von Next und zeigt kurz den alten Stand. Closes #40 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a2330ef682
commit
d30bb6d235
@@ -120,6 +120,13 @@ describe.runIf(datenbankVerfuegbar)("Handgeschriebene Datenbankobjekte", () => {
|
||||
* Wochenende ueberfaellig steht, 2880 Mails — und zwar ohne dass ein
|
||||
* einziger Test rot waere, denn jede davon ist fuer sich korrekt (#034).
|
||||
*/
|
||||
/*
|
||||
* Die Freigabe-Aufforderung geht je Lauf und Empfaenger einmal hinaus
|
||||
* (#040). Sie entsteht bei einem gescheiterten Abschlussversuch, und
|
||||
* jemand versucht es mehrmals — ohne den Index eine Mail je Versuch, und
|
||||
* jede davon fuer sich korrekt.
|
||||
*/
|
||||
"Zustellung_freigabe_einmal",
|
||||
"Zustellung_ueberfaellig_einmal",
|
||||
]);
|
||||
});
|
||||
@@ -149,6 +156,14 @@ describe.runIf(datenbankVerfuegbar)("Handgeschriebene Datenbankobjekte", () => {
|
||||
"Lauf_abgeschlossen_mit_zeitpunkt",
|
||||
"Lauf_archiviert_mit_zeitpunkt",
|
||||
"Lauf_begonnen_mit_zeitpunkt",
|
||||
/*
|
||||
* Die Vier-Augen-Regel in der Datenbank (#040): wer den Lauf angelegt hat,
|
||||
* gibt ihn nicht frei. Der Dienst prueft strenger — er sieht auch nach, wer
|
||||
* an den Schrittergebnissen gearbeitet hat —, aber diese Haelfte gilt auch
|
||||
* dann, wenn jemand an der Dienstschicht vorbei schreibt.
|
||||
*/
|
||||
"Lauf_freigabe_mit_zeitpunkt",
|
||||
"Lauf_freigabe_nicht_selbst",
|
||||
"Lauf_mitarbeiter_eindeutig",
|
||||
"Mitarbeiter_vorgesetzter_nicht_selbst",
|
||||
"Schrittergebnis_begruendung_noetig",
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { LaufStatus, SchrittZustand, VersionStatus } from "@/generated/prisma/enums";
|
||||
import {
|
||||
LaufFehler,
|
||||
laufAbschliessen,
|
||||
laufAnlegen,
|
||||
laufFreigeben,
|
||||
laufZurueckweisen,
|
||||
} from "@/server/services/laeufe";
|
||||
import { schrittergebnisSetzen } from "@/server/services/schrittergebnisse";
|
||||
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
||||
|
||||
/**
|
||||
* Vier-Augen-Freigabe (#040).
|
||||
*
|
||||
* **Eine Zusage trägt diesen Meilenstein:** wer an einem Lauf gearbeitet hat,
|
||||
* gibt ihn nicht frei. Alles andere — das Recht, die Pflicht an der Version, die
|
||||
* Zurückweisung — hängt daran.
|
||||
*
|
||||
* Geprüft wird sie von drei Seiten, weil sie auf drei Wegen umgangen werden
|
||||
* könnte: über das Anlegen, über ein Schrittergebnis und über den Abschluss. Und
|
||||
* ein CHECK in der Datenbank hält die wichtigste Hälfte selbst dann, wenn jemand
|
||||
* an der Dienstschicht vorbei schreibt.
|
||||
*/
|
||||
|
||||
async function aufbau(freigabePflicht: boolean) {
|
||||
const kunde = await testPrisma.kunde.create({
|
||||
data: { name: "Eigene Organisation", kuerzel: "EIGEN", istEigeneOrganisation: true },
|
||||
});
|
||||
await testPrisma.einstellung.create({ data: { id: "global" } });
|
||||
|
||||
const techniker = await testPrisma.benutzer.create({
|
||||
data: { email: "tina@firma.example", name: "Tina", rolle: "TECHNIKER" },
|
||||
});
|
||||
const redakteur = await testPrisma.benutzer.create({
|
||||
data: { email: "red@firma.example", name: "Red", rolle: "REDAKTEUR" },
|
||||
});
|
||||
const admin = await testPrisma.benutzer.create({
|
||||
data: { email: "admin@firma.example", name: "Admin", rolle: "ADMIN" },
|
||||
});
|
||||
|
||||
const vorlage = await testPrisma.vorlage.create({ data: { name: "Restore-Test" } });
|
||||
const version = await testPrisma.vorlagenVersion.create({
|
||||
data: { vorlageId: vorlage.id, version: 1, status: VersionStatus.ENTWURF, freigabePflicht },
|
||||
});
|
||||
const abschnitt = await testPrisma.vorlagenAbschnitt.create({
|
||||
data: { versionId: version.id, titel: "Prüfung", position: 0 },
|
||||
});
|
||||
const schritt = await testPrisma.vorlagenSchritt.create({
|
||||
data: { abschnittId: abschnitt.id, titel: "Sicherung zurückspielen", pflicht: true, position: 0 },
|
||||
});
|
||||
await testPrisma.vorlagenVersion.update({
|
||||
where: { id: version.id },
|
||||
data: { status: VersionStatus.VEROEFFENTLICHT, veroeffentlichtAm: new Date() },
|
||||
});
|
||||
|
||||
return {
|
||||
kunde,
|
||||
versionId: version.id,
|
||||
schrittId: schritt.id,
|
||||
techniker: { id: techniker.id, rolle: techniker.rolle },
|
||||
redakteur: { id: redakteur.id, rolle: redakteur.rolle },
|
||||
admin: { id: admin.id, rolle: admin.rolle },
|
||||
};
|
||||
}
|
||||
|
||||
/** Ein Lauf, den der Techniker angelegt und abgearbeitet hat. */
|
||||
async function abgearbeitet(freigabePflicht: boolean) {
|
||||
const stand = await aufbau(freigabePflicht);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: stand.versionId }, stand.techniker);
|
||||
await schrittergebnisSetzen(
|
||||
{ laufId: lauf.id, vorlagenSchrittId: stand.schrittId, zustand: SchrittZustand.ERLEDIGT },
|
||||
stand.techniker,
|
||||
);
|
||||
return { ...stand, laufId: lauf.id };
|
||||
}
|
||||
|
||||
describe.runIf(datenbankVerfuegbar)("Ein Lauf mit Freigabepflicht", () => {
|
||||
beforeEach(async () => {
|
||||
await datenbankLeeren();
|
||||
});
|
||||
|
||||
it("lässt sich nicht abschließen, solange keine Freigabe steht", async () => {
|
||||
const { laufId, techniker } = await abgearbeitet(true);
|
||||
await expect(laufAbschliessen(laufId, techniker)).rejects.toThrow(LaufFehler);
|
||||
await expect(laufAbschliessen(laufId, techniker)).rejects.toThrow(/Freigabe durch einen Zweiten/u);
|
||||
});
|
||||
|
||||
it("lässt sich nach der Freigabe abschließen", async () => {
|
||||
const { laufId, techniker, redakteur } = await abgearbeitet(true);
|
||||
await laufFreigeben(laufId, redakteur);
|
||||
const fertig = await laufAbschliessen(laufId, techniker);
|
||||
expect(fertig.status).toBe(LaufStatus.ABGESCHLOSSEN);
|
||||
});
|
||||
|
||||
it("hält die Freigabe mit Freigeber und Zeitpunkt fest", async () => {
|
||||
const { laufId, redakteur } = await abgearbeitet(true);
|
||||
const nachher = await laufFreigeben(laufId, redakteur, "Sichtprüfung am 24.10. durch Red");
|
||||
|
||||
expect(nachher.freigegebenVonId).toBe(redakteur.id);
|
||||
expect(nachher.freigegebenAm).toBeInstanceOf(Date);
|
||||
expect(nachher.freigabeNotiz).toBe("Sichtprüfung am 24.10. durch Red");
|
||||
|
||||
const eintrag = await testPrisma.protokoll.findFirstOrThrow({
|
||||
where: { laufId, aktion: "Lauf freigegeben" },
|
||||
});
|
||||
expect(eintrag.benutzerId).toBe(redakteur.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe.runIf(datenbankVerfuegbar)("Wer gearbeitet hat, gibt nicht frei", () => {
|
||||
beforeEach(async () => {
|
||||
await datenbankLeeren();
|
||||
});
|
||||
|
||||
it("weist den Techniker ab, der den Lauf angelegt und abgearbeitet hat", async () => {
|
||||
// Er hätte das Recht ohnehin nicht — der nächste Test zeigt den Fall mit Recht.
|
||||
const { laufId, techniker } = await abgearbeitet(true);
|
||||
await expect(laufFreigeben(laufId, techniker)).rejects.toThrow(/Recht "freigeben"/u);
|
||||
});
|
||||
|
||||
it("weist auch einen ADMIN ab, der den Lauf angelegt hat", async () => {
|
||||
/*
|
||||
* **Der wichtigste Fall.** Das Recht allein genügt nicht; sonst wäre die
|
||||
* Vier-Augen-Regel für jeden Administrator eine Formsache. Ein Einzelkämpfer
|
||||
* kommt damit nicht weiter — das ist eine bewusste Härte, und die Anleitung
|
||||
* sagt es.
|
||||
*/
|
||||
const { versionId, admin } = await aufbau(true);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: versionId }, admin);
|
||||
|
||||
await expect(laufFreigeben(lauf.id, admin)).rejects.toThrow(/an diesem Lauf gearbeitet/u);
|
||||
});
|
||||
|
||||
it("weist ab, wer nur ein Schrittergebnis gesetzt hat", async () => {
|
||||
/*
|
||||
* Angelegt hat ihn der Techniker, abgearbeitet der Redakteur. Der Redakteur
|
||||
* hat das Recht — aber er hat gearbeitet, also gibt er nicht frei.
|
||||
*/
|
||||
const { versionId, schrittId, techniker, redakteur } = await aufbau(true);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: versionId }, techniker);
|
||||
await schrittergebnisSetzen(
|
||||
{ laufId: lauf.id, vorlagenSchrittId: schrittId, zustand: SchrittZustand.ERLEDIGT },
|
||||
redakteur,
|
||||
);
|
||||
|
||||
await expect(laufFreigeben(lauf.id, redakteur)).rejects.toThrow(/an diesem Lauf gearbeitet/u);
|
||||
});
|
||||
|
||||
it("lässt den frei, der nur zugewiesen war", async () => {
|
||||
/*
|
||||
* Die Gegenrichtung, und sie ist Absicht: eine **Zuweisung** ist noch keine
|
||||
* Arbeit. Wer zugewiesen wurde und nichts getan hat, soll freigeben können,
|
||||
* wenn jemand anderes abgearbeitet hat — sonst wäre die Regel strenger als
|
||||
* gemeint und blockierte kleine Teams unnötig.
|
||||
*/
|
||||
const { versionId, schrittId, techniker, redakteur } = await aufbau(true);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: versionId, zugewiesenAnId: redakteur.id }, techniker);
|
||||
await schrittergebnisSetzen(
|
||||
{ laufId: lauf.id, vorlagenSchrittId: schrittId, zustand: SchrittZustand.ERLEDIGT },
|
||||
techniker,
|
||||
);
|
||||
|
||||
await expect(laufFreigeben(lauf.id, redakteur)).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("weist ab, wer ihn abgeschlossen hat", async () => {
|
||||
// Ohne Freigabepflicht lässt sich abschließen; danach ist der Abschließende
|
||||
// Bearbeiter und gibt nicht frei.
|
||||
const { versionId, schrittId, techniker, redakteur } = await aufbau(false);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: versionId }, techniker);
|
||||
await schrittergebnisSetzen(
|
||||
{ laufId: lauf.id, vorlagenSchrittId: schrittId, zustand: SchrittZustand.ERLEDIGT },
|
||||
techniker,
|
||||
);
|
||||
await laufAbschliessen(lauf.id, redakteur);
|
||||
|
||||
await expect(laufFreigeben(lauf.id, redakteur)).rejects.toThrow(/an diesem Lauf gearbeitet/u);
|
||||
});
|
||||
|
||||
it("wird auch von der Datenbank abgewiesen, nicht nur vom Dienst", async () => {
|
||||
/*
|
||||
* An der Dienstschicht vorbei: der CHECK `Lauf_freigabe_nicht_selbst` hält
|
||||
* die wichtigste Hälfte der Regel — der Ersteller gibt nicht frei (CLAUDE.md,
|
||||
* „Regeln stehen in der Datenbank“).
|
||||
*/
|
||||
const { versionId, admin } = await aufbau(true);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: versionId }, admin);
|
||||
|
||||
await expect(
|
||||
testPrisma.lauf.update({
|
||||
where: { id: lauf.id },
|
||||
data: { freigegebenVonId: admin.id, freigegebenAm: new Date() },
|
||||
}),
|
||||
).rejects.toThrow(/Lauf_freigabe_nicht_selbst/u);
|
||||
});
|
||||
|
||||
it("verlangt Freigeber und Zeitpunkt zusammen", async () => {
|
||||
const { versionId, techniker, redakteur } = await aufbau(true);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: versionId }, techniker);
|
||||
|
||||
await expect(
|
||||
testPrisma.lauf.update({ where: { id: lauf.id }, data: { freigegebenVonId: redakteur.id } }),
|
||||
).rejects.toThrow(/Lauf_freigabe_mit_zeitpunkt/u);
|
||||
await expect(
|
||||
testPrisma.lauf.update({ where: { id: lauf.id }, data: { freigegebenAm: new Date() } }),
|
||||
).rejects.toThrow(/Lauf_freigabe_mit_zeitpunkt/u);
|
||||
});
|
||||
});
|
||||
|
||||
describe.runIf(datenbankVerfuegbar)("Zurückweisen", () => {
|
||||
beforeEach(async () => {
|
||||
await datenbankLeeren();
|
||||
});
|
||||
|
||||
it("verlangt eine Begründung", async () => {
|
||||
// Dieselbe Regel wie bei NICHT_ZUTREFFEND und FEHLGESCHLAGEN: wer etwas
|
||||
// zurückgibt, sagt warum.
|
||||
const { laufId, redakteur } = await abgearbeitet(true);
|
||||
await expect(laufZurueckweisen(laufId, redakteur, " ")).rejects.toThrow(/Begründung/u);
|
||||
});
|
||||
|
||||
it("setzt den Lauf zurück auf IN_ARBEIT und hält die Notiz fest", async () => {
|
||||
const { laufId, redakteur } = await abgearbeitet(true);
|
||||
const nachher = await laufZurueckweisen(laufId, redakteur, "Prüfsumme fehlt im Protokoll");
|
||||
|
||||
expect(nachher.status).toBe(LaufStatus.IN_ARBEIT);
|
||||
expect(nachher.freigabeNotiz).toBe("Prüfsumme fehlt im Protokoll");
|
||||
expect(nachher.freigegebenVonId).toBeNull();
|
||||
|
||||
const eintrag = await testPrisma.protokoll.findFirstOrThrow({
|
||||
where: { laufId, aktion: "Lauf zurückgewiesen" },
|
||||
});
|
||||
expect(JSON.stringify(eintrag.nachher)).toContain("Prüfsumme fehlt");
|
||||
});
|
||||
|
||||
it("nimmt eine erteilte Freigabe zurück", async () => {
|
||||
const { laufId, redakteur, admin } = await abgearbeitet(true);
|
||||
await laufFreigeben(laufId, redakteur);
|
||||
await laufZurueckweisen(laufId, admin, "Doch nicht in Ordnung");
|
||||
|
||||
const zeile = await testPrisma.lauf.findUniqueOrThrow({ where: { id: laufId } });
|
||||
expect(zeile.freigegebenVonId).toBeNull();
|
||||
expect(zeile.freigegebenAm).toBeNull();
|
||||
// Und der Abschluss wartet wieder.
|
||||
await expect(laufAbschliessen(laufId, redakteur)).rejects.toThrow(/Freigabe/u);
|
||||
});
|
||||
|
||||
it("holt einen abgeschlossenen Lauf zurück in die Arbeit", async () => {
|
||||
const { laufId, techniker, redakteur, admin } = await abgearbeitet(true);
|
||||
await laufFreigeben(laufId, redakteur);
|
||||
await laufAbschliessen(laufId, techniker);
|
||||
|
||||
const nachher = await laufZurueckweisen(laufId, admin, "Nacharbeit nötig");
|
||||
expect(nachher.status).toBe(LaufStatus.IN_ARBEIT);
|
||||
expect(nachher.abgeschlossenAm).toBeNull();
|
||||
expect(nachher.abgeschlossenVonId).toBeNull();
|
||||
});
|
||||
|
||||
it("verlangt das Recht freigeben", async () => {
|
||||
const { laufId, techniker } = await abgearbeitet(true);
|
||||
await expect(laufZurueckweisen(laufId, techniker, "Grund")).rejects.toThrow(/Recht "freigeben"/u);
|
||||
});
|
||||
});
|
||||
|
||||
describe.runIf(datenbankVerfuegbar)("Die Aufforderung an die Freigabeberechtigten", () => {
|
||||
beforeEach(async () => {
|
||||
await datenbankLeeren();
|
||||
});
|
||||
|
||||
it("geht beim gescheiterten Abschlussversuch hinaus – an die mit dem Recht", async () => {
|
||||
/*
|
||||
* **Der Fall, der die ganze Regel wertlos machen würde:** ein Lauf wartet auf
|
||||
* eine Freigabe, und niemand weiß es. Die Aufforderung entsteht deshalb genau
|
||||
* dort, wo der Bearbeiter „ich bin fertig“ sagt — beim Abschlussversuch.
|
||||
*
|
||||
* Empfänger ist **nicht** der Zuständige, sondern jeder mit dem Recht
|
||||
* `freigeben`. Das ist der einzige Anlass mit diesem Kreis.
|
||||
*/
|
||||
const { laufId, techniker, redakteur, admin } = await abgearbeitet(true);
|
||||
await expect(laufAbschliessen(laufId, techniker)).rejects.toThrow(LaufFehler);
|
||||
|
||||
const zustellungen = await testPrisma.zustellung.findMany({
|
||||
where: { laufId, anlass: "LAUF_FREIGABE_OFFEN", weg: "MAIL" },
|
||||
select: { empfaenger: true },
|
||||
});
|
||||
const empfaenger = zustellungen.map((zeile) => zeile.empfaenger).sort();
|
||||
|
||||
const redZeile = await testPrisma.benutzer.findUniqueOrThrow({ where: { id: redakteur.id } });
|
||||
const adminZeile = await testPrisma.benutzer.findUniqueOrThrow({ where: { id: admin.id } });
|
||||
const tinaZeile = await testPrisma.benutzer.findUniqueOrThrow({ where: { id: techniker.id } });
|
||||
|
||||
expect(empfaenger).toEqual([adminZeile.email, redZeile.email].sort());
|
||||
// Der Techniker hat kein `freigeben` – ihn aufzufordern wäre eine
|
||||
// Aufforderung zu etwas, das der Dienst abweist.
|
||||
expect(empfaenger).not.toContain(tinaZeile.email);
|
||||
});
|
||||
|
||||
it("lässt den aus, der an dem Lauf gearbeitet hat", async () => {
|
||||
// Der Redakteur hat das Recht, aber er hat abgearbeitet: er darf nicht
|
||||
// freigeben, also bekommt er auch keine Aufforderung.
|
||||
const { versionId, schrittId, techniker, redakteur, admin } = await aufbau(true);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: versionId }, techniker);
|
||||
await schrittergebnisSetzen(
|
||||
{ laufId: lauf.id, vorlagenSchrittId: schrittId, zustand: SchrittZustand.ERLEDIGT },
|
||||
redakteur,
|
||||
);
|
||||
await expect(laufAbschliessen(lauf.id, techniker)).rejects.toThrow(LaufFehler);
|
||||
|
||||
const zustellungen = await testPrisma.zustellung.findMany({
|
||||
where: { laufId: lauf.id, anlass: "LAUF_FREIGABE_OFFEN", weg: "MAIL" },
|
||||
select: { empfaenger: true },
|
||||
});
|
||||
const adminZeile = await testPrisma.benutzer.findUniqueOrThrow({ where: { id: admin.id } });
|
||||
expect(zustellungen.map((z) => z.empfaenger)).toEqual([adminZeile.email]);
|
||||
});
|
||||
|
||||
it("geht bei mehreren Versuchen nur einmal hinaus", async () => {
|
||||
/*
|
||||
* Ein Techniker versucht es mehrmals — und ohne den partiellen Unique-Index
|
||||
* bekäme jeder Berechtigte bei jedem Versuch eine Mail, jede für sich
|
||||
* korrekt. Derselbe Fall wie „überfällig“ in #034, nur schneller erreicht.
|
||||
*/
|
||||
const { laufId, techniker } = await abgearbeitet(true);
|
||||
for (let versuch = 0; versuch < 3; versuch += 1) {
|
||||
await expect(laufAbschliessen(laufId, techniker)).rejects.toThrow(LaufFehler);
|
||||
}
|
||||
|
||||
const anzahl = await testPrisma.zustellung.count({
|
||||
where: { laufId, anlass: "LAUF_FREIGABE_OFFEN", weg: "MAIL" },
|
||||
});
|
||||
// Zwei Berechtigte, drei Versuche – zwei Zeilen.
|
||||
expect(anzahl).toBe(2);
|
||||
});
|
||||
|
||||
it("nennt im Rumpf die Regel und keinen Schrittinhalt", async () => {
|
||||
// Der Empfänger ist meist nicht der Bearbeiter und soll nicht raten müssen,
|
||||
// warum er gefragt wird. Schrittinhalte gehören auch hier nicht hinein (#034).
|
||||
const { laufId, techniker } = await abgearbeitet(true);
|
||||
await expect(laufAbschliessen(laufId, techniker)).rejects.toThrow(LaufFehler);
|
||||
|
||||
const zeile = await testPrisma.zustellung.findFirstOrThrow({
|
||||
where: { laufId, anlass: "LAUF_FREIGABE_OFFEN" },
|
||||
});
|
||||
expect(zeile.betreff).toContain("wartet auf Freigabe");
|
||||
expect(zeile.rumpf).toMatch(/nicht daran gearbeitet/u);
|
||||
expect(zeile.rumpf).not.toContain("Sicherung zurückspielen");
|
||||
});
|
||||
});
|
||||
|
||||
describe.runIf(datenbankVerfuegbar)("Ein Lauf ohne Freigabepflicht", () => {
|
||||
beforeEach(async () => {
|
||||
await datenbankLeeren();
|
||||
});
|
||||
|
||||
it("verhält sich unverändert", async () => {
|
||||
/*
|
||||
* **Die Zusage an alles Bestehende.** Jede Vorlage und jeder Lauf von vor
|
||||
* diesem Meilenstein hat `freigabePflicht = false`; nichts daran darf sich
|
||||
* ändern, sonst wäre es ein kaputtes Update.
|
||||
*/
|
||||
const { laufId, techniker } = await abgearbeitet(false);
|
||||
const fertig = await laufAbschliessen(laufId, techniker);
|
||||
expect(fertig.status).toBe(LaufStatus.ABGESCHLOSSEN);
|
||||
expect(fertig.freigegebenVonId).toBeNull();
|
||||
});
|
||||
|
||||
it("lässt sich trotzdem freigeben – nach denselben Regeln", async () => {
|
||||
/*
|
||||
* Eine Freigabe an einem Lauf, der keine braucht, ist kein Fehler. Sie folgt
|
||||
* aber denselben Regeln: sonst wäre sie ein Weg, einen Freigeber in die Akte
|
||||
* zu schreiben, der dort nicht hingehört.
|
||||
*/
|
||||
const { laufId, techniker, redakteur } = await abgearbeitet(false);
|
||||
await expect(laufFreigeben(laufId, redakteur)).resolves.toBeDefined();
|
||||
await expect(laufFreigeben(laufId, techniker)).rejects.toThrow(/Recht "freigeben"/u);
|
||||
});
|
||||
|
||||
it("lässt sich nicht zweimal freigeben", async () => {
|
||||
const { laufId, redakteur, admin } = await abgearbeitet(false);
|
||||
await laufFreigeben(laufId, redakteur);
|
||||
await expect(laufFreigeben(laufId, admin)).rejects.toThrow(/bereits freigegeben/u);
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,11 @@ const MATRIX: Record<Recht, Record<Rolle, boolean>> = {
|
||||
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 },
|
||||
};
|
||||
@@ -34,11 +39,11 @@ describe("Rechtematrix", () => {
|
||||
|
||||
it("prueft jede Rolle gegen jedes Recht", () => {
|
||||
// Haelt fest, dass die Tabelle oben wirklich vollstaendig ist: fuenf Rollen,
|
||||
// acht Rechte. Die Zahlen stehen hier ausgeschrieben, damit eine neue Rolle
|
||||
// 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(8);
|
||||
expect(faelle).toHaveLength(40);
|
||||
expect(ALLE_RECHTE).toHaveLength(9);
|
||||
expect(faelle).toHaveLength(45);
|
||||
});
|
||||
|
||||
it.each(faelle)("%s: %s -> %s", (recht, rolle, erwartet) => {
|
||||
@@ -58,6 +63,7 @@ describe("Rollen im Einzelnen", () => {
|
||||
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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user