feat(vorlagen): Versionierung, Unveraenderlichkeit und JSON-Austausch (#23)
This commit was merged in pull request #23.
This commit is contained in:
@@ -0,0 +1,741 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
VorlagenFehler,
|
||||
entwurfHolenOderErzeugen,
|
||||
entwurfSpeichern,
|
||||
entwurfVerwerfen,
|
||||
veroeffentlichteVersionPruefen,
|
||||
versionMussEntwurfSein,
|
||||
versionVeroeffentlichen,
|
||||
vorlageAnlegen,
|
||||
vorlageArchivieren,
|
||||
vorlageBearbeiten,
|
||||
vorlageDuplizieren,
|
||||
} from "@/server/services/vorlagen";
|
||||
import type { StrukturEingabe } from "@/lib/vorlagen-format";
|
||||
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
||||
|
||||
/**
|
||||
* Versionierung der Vorlagen (#009).
|
||||
*
|
||||
* Die tragende Regel: eine veroeffentlichte Version wird nie wieder veraendert.
|
||||
* Ein Lauf zeigt deshalb auch in zehn Jahren genau die Schritte, die damals
|
||||
* abgearbeitet wurden. Alles hier dreht sich darum, dass diese Regel haelt.
|
||||
*/
|
||||
|
||||
const STRUKTUR: StrukturEingabe = {
|
||||
variablen: [
|
||||
{
|
||||
schluessel: "os_version",
|
||||
beschriftung: "Betriebssystem",
|
||||
typ: "AUSWAHL",
|
||||
optionen: ["Windows 11 Pro", "Windows 11 Enterprise"],
|
||||
pflicht: true,
|
||||
},
|
||||
{
|
||||
schluessel: "join_typ",
|
||||
beschriftung: "Einbindung",
|
||||
typ: "AUSWAHL",
|
||||
optionen: ["Active Directory", "Entra ID"],
|
||||
pflicht: true,
|
||||
},
|
||||
],
|
||||
abschnitte: [
|
||||
{
|
||||
titel: "Vorbereitung",
|
||||
schritte: [
|
||||
{ titel: "Ticket und Endbenutzer im Lauf erfasst", pflicht: true },
|
||||
{
|
||||
titel: "Datensicherung geprüft",
|
||||
hilfetext: "Auch nach lokal gespeicherten Daten außerhalb des Profils fragen.",
|
||||
pflicht: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
titel: "Installation",
|
||||
schritte: [
|
||||
{
|
||||
titel: "Hostname gesetzt",
|
||||
typ: "BEFEHL",
|
||||
pflicht: true,
|
||||
konfig: { shell: "powershell", befehl: 'Rename-Computer -NewName "{{hostname}}" -Restart' },
|
||||
},
|
||||
{ titel: "Build-Nummer", typ: "TEXT", pflicht: true, konfig: { mehrzeilig: false } },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async function benutzerAnlegen(email = "redakteur@firma.example") {
|
||||
return testPrisma.benutzer.create({
|
||||
data: { email, name: "Redakteur", rolle: "REDAKTEUR" },
|
||||
});
|
||||
}
|
||||
|
||||
/** Eine Vorlage mit einer veroeffentlichten Version 1. */
|
||||
async function veroeffentlichteVorlage(benutzerId: string) {
|
||||
const { vorlage, version } = await vorlageAnlegen(
|
||||
{ name: "PC Neuinstallation", kategorie: "Client" },
|
||||
benutzerId,
|
||||
);
|
||||
await entwurfSpeichern(version.id, STRUKTUR);
|
||||
await versionVeroeffentlichen(version.id, "Erste Fassung", benutzerId);
|
||||
return { vorlage, versionId: version.id };
|
||||
}
|
||||
|
||||
async function strukturLesen(versionId: string) {
|
||||
const version = await testPrisma.vorlagenVersion.findUniqueOrThrow({
|
||||
where: { id: versionId },
|
||||
include: {
|
||||
variablen: { orderBy: { position: "asc" } },
|
||||
abschnitte: {
|
||||
orderBy: { position: "asc" },
|
||||
include: { schritte: { orderBy: { position: "asc" } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
return version;
|
||||
}
|
||||
|
||||
describe.runIf(datenbankVerfuegbar)("Vorlagen-Versionierung", () => {
|
||||
beforeEach(async () => {
|
||||
await datenbankLeeren();
|
||||
});
|
||||
|
||||
describe("Anlegen", () => {
|
||||
it("legt eine Vorlage mit einem leeren Entwurf als Version 1 an", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage, version } = await vorlageAnlegen(
|
||||
{ name: " Mitarbeiter-Onboarding ", kategorie: "Benutzer", schlagworte: ["ad", "entra"] },
|
||||
benutzer.id,
|
||||
);
|
||||
|
||||
expect(vorlage.name).toBe("Mitarbeiter-Onboarding");
|
||||
expect(vorlage.schlagworte).toEqual(["ad", "entra"]);
|
||||
expect(version.version).toBe(1);
|
||||
expect(version.status).toBe("ENTWURF");
|
||||
expect(version.veroeffentlichtAm).toBeNull();
|
||||
});
|
||||
|
||||
it("weist einen leeren Namen ab", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
await expect(vorlageAnlegen({ name: " " }, benutzer.id)).rejects.toBeInstanceOf(VorlagenFehler);
|
||||
expect(await testPrisma.vorlage.count()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Entwurf speichern", () => {
|
||||
it("legt Variablen, Abschnitte und Schritte in der übergebenen Reihenfolge an", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { version } = await vorlageAnlegen({ name: "Probe" }, benutzer.id);
|
||||
await entwurfSpeichern(version.id, STRUKTUR);
|
||||
|
||||
const gelesen = await strukturLesen(version.id);
|
||||
expect(gelesen.variablen.map((v) => v.schluessel)).toEqual(["os_version", "join_typ"]);
|
||||
expect(gelesen.variablen.map((v) => v.position)).toEqual([0, 1]);
|
||||
expect(gelesen.abschnitte.map((a) => a.titel)).toEqual(["Vorbereitung", "Installation"]);
|
||||
expect(gelesen.abschnitte[1]?.schritte.map((s) => s.titel)).toEqual([
|
||||
"Hostname gesetzt",
|
||||
"Build-Nummer",
|
||||
]);
|
||||
expect(gelesen.abschnitte[1]?.schritte[0]?.konfig).toMatchObject({
|
||||
shell: "powershell",
|
||||
befehl: 'Rename-Computer -NewName "{{hostname}}" -Restart',
|
||||
});
|
||||
});
|
||||
|
||||
it("ersetzt den Aufbau vollständig, statt ihn zu ergänzen", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { version } = await vorlageAnlegen({ name: "Probe" }, benutzer.id);
|
||||
await entwurfSpeichern(version.id, STRUKTUR);
|
||||
await entwurfSpeichern(version.id, {
|
||||
variablen: [],
|
||||
abschnitte: [{ titel: "Nur noch das hier", schritte: [{ titel: "Ein Schritt" }] }],
|
||||
});
|
||||
|
||||
const gelesen = await strukturLesen(version.id);
|
||||
expect(gelesen.variablen).toHaveLength(0);
|
||||
expect(gelesen.abschnitte).toHaveLength(1);
|
||||
expect(await testPrisma.vorlagenSchritt.count()).toBe(1);
|
||||
});
|
||||
|
||||
it("weist einen doppelten Variablenschlüssel mit verständlicher Meldung ab", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { version } = await vorlageAnlegen({ name: "Probe" }, benutzer.id);
|
||||
await expect(
|
||||
entwurfSpeichern(version.id, {
|
||||
variablen: [
|
||||
{ schluessel: "upn", beschriftung: "UPN" },
|
||||
{ schluessel: "upn", beschriftung: "Nochmal UPN" },
|
||||
],
|
||||
abschnitte: [],
|
||||
}),
|
||||
).rejects.toThrow(/„upn“ gibt es zweimal/);
|
||||
});
|
||||
|
||||
it("nennt bei einem Fehler im Aufbau den Pfad", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { version } = await vorlageAnlegen({ name: "Probe" }, benutzer.id);
|
||||
await expect(
|
||||
entwurfSpeichern(version.id, {
|
||||
variablen: [],
|
||||
abschnitte: [{ titel: "Abschnitt", schritte: [{ titel: "gut" }, { titel: " " }] }],
|
||||
}),
|
||||
).rejects.toThrow(/abschnitte\.0\.schritte\.1\.titel/);
|
||||
});
|
||||
|
||||
it("hinterlässt bei einem Fehler keinen halben Entwurf", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { version } = await vorlageAnlegen({ name: "Probe" }, benutzer.id);
|
||||
await entwurfSpeichern(version.id, STRUKTUR);
|
||||
|
||||
await expect(
|
||||
entwurfSpeichern(version.id, {
|
||||
variablen: [],
|
||||
abschnitte: [{ titel: "", schritte: [] }],
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
|
||||
// Der alte Aufbau steht unveraendert.
|
||||
const gelesen = await strukturLesen(version.id);
|
||||
expect(gelesen.abschnitte).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Unveränderlichkeit", () => {
|
||||
it("lässt eine veröffentlichte Version nicht mehr bearbeiten", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
|
||||
await expect(entwurfSpeichern(versionId, { variablen: [], abschnitte: [] })).rejects.toThrow(
|
||||
/unveränderlich/,
|
||||
);
|
||||
});
|
||||
|
||||
it("lässt eine veröffentlichte Version nicht erneut veröffentlichen", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
await expect(versionVeroeffentlichen(versionId, null, benutzer.id)).rejects.toThrow(/unveränderlich/);
|
||||
});
|
||||
|
||||
it("lässt eine veröffentlichte Version nicht verwerfen", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
await expect(entwurfVerwerfen(versionId, benutzer.id)).rejects.toThrow(/unveränderlich/);
|
||||
});
|
||||
|
||||
it("prüft den Zustand an einer Stelle", () => {
|
||||
expect(() => versionMussEntwurfSein({ status: "ENTWURF", version: 1 })).not.toThrow();
|
||||
expect(() => versionMussEntwurfSein({ status: "VEROEFFENTLICHT", version: 3 })).toThrow(/Version 3/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bearbeiten erzeugt eine tiefe Kopie", () => {
|
||||
it("kopiert Variablen, Abschnitte und Schritte, statt sie zu teilen", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage, versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
|
||||
const entwurf = await entwurfHolenOderErzeugen(vorlage.id, benutzer.id);
|
||||
expect(entwurf.version).toBe(2);
|
||||
expect(entwurf.status).toBe("ENTWURF");
|
||||
|
||||
const veroeffentlicht = await strukturLesen(versionId);
|
||||
const kopie = await strukturLesen(entwurf.id);
|
||||
|
||||
// Gleicher Inhalt …
|
||||
expect(kopie.variablen.map((v) => v.schluessel)).toEqual(
|
||||
veroeffentlicht.variablen.map((v) => v.schluessel),
|
||||
);
|
||||
expect(kopie.abschnitte.map((a) => a.titel)).toEqual(veroeffentlicht.abschnitte.map((a) => a.titel));
|
||||
// … aber eigene Zeilen.
|
||||
const alteIds = veroeffentlicht.abschnitte.map((a) => a.id);
|
||||
for (const abschnitt of kopie.abschnitte) {
|
||||
expect(alteIds).not.toContain(abschnitt.id);
|
||||
}
|
||||
});
|
||||
|
||||
it("lässt die veröffentlichte Version unberührt, wenn der Entwurf geändert wird", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage, versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
const entwurf = await entwurfHolenOderErzeugen(vorlage.id, benutzer.id);
|
||||
|
||||
await entwurfSpeichern(entwurf.id, {
|
||||
variablen: [],
|
||||
abschnitte: [{ titel: "Komplett anders", schritte: [{ titel: "Neu" }] }],
|
||||
});
|
||||
|
||||
const veroeffentlicht = await strukturLesen(versionId);
|
||||
expect(veroeffentlicht.abschnitte.map((a) => a.titel)).toEqual(["Vorbereitung", "Installation"]);
|
||||
expect(veroeffentlicht.variablen).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("kopiert auch die konfig als Wert, nicht als geteilten Verweis", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage, versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
const entwurf = await entwurfHolenOderErzeugen(vorlage.id, benutzer.id);
|
||||
|
||||
const kopie = await strukturLesen(entwurf.id);
|
||||
const befehlsschritt = kopie.abschnitte[1]?.schritte[0];
|
||||
expect(befehlsschritt?.konfig).toMatchObject({ shell: "powershell" });
|
||||
|
||||
await testPrisma.vorlagenSchritt.update({
|
||||
where: { id: befehlsschritt!.id },
|
||||
data: { konfig: { shell: "bash", befehl: "echo neu" } },
|
||||
});
|
||||
|
||||
const original = await strukturLesen(versionId);
|
||||
expect(original.abschnitte[1]?.schritte[0]?.konfig).toMatchObject({ shell: "powershell" });
|
||||
});
|
||||
|
||||
it("gibt einen vorhandenen Entwurf zurück, statt einen zweiten anzulegen", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage } = await veroeffentlichteVorlage(benutzer.id);
|
||||
|
||||
const erst = await entwurfHolenOderErzeugen(vorlage.id, benutzer.id);
|
||||
const nochmal = await entwurfHolenOderErzeugen(vorlage.id, benutzer.id);
|
||||
|
||||
expect(nochmal.id).toBe(erst.id);
|
||||
expect(
|
||||
await testPrisma.vorlagenVersion.count({
|
||||
where: { vorlageId: vorlage.id, status: "ENTWURF" },
|
||||
}),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it("lässt eine archivierte Vorlage nicht bearbeiten", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage } = await veroeffentlichteVorlage(benutzer.id);
|
||||
await vorlageArchivieren(vorlage.id, true, benutzer.id);
|
||||
|
||||
await expect(entwurfHolenOderErzeugen(vorlage.id, benutzer.id)).rejects.toThrow(/archiviert/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Veröffentlichen", () => {
|
||||
it("setzt Status, Zeitpunkt, Änderungsnotiz und Urheber", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { version } = await vorlageAnlegen({ name: "Probe" }, benutzer.id);
|
||||
await entwurfSpeichern(version.id, STRUKTUR);
|
||||
|
||||
const veroeffentlicht = await versionVeroeffentlichen(
|
||||
version.id,
|
||||
" Secure Boot ergänzt ",
|
||||
benutzer.id,
|
||||
);
|
||||
|
||||
expect(veroeffentlicht.status).toBe("VEROEFFENTLICHT");
|
||||
expect(veroeffentlicht.veroeffentlichtAm).not.toBeNull();
|
||||
expect(veroeffentlicht.veroeffentlichtVonId).toBe(benutzer.id);
|
||||
expect(veroeffentlicht.aenderungsnotiz).toBe("Secure Boot ergänzt");
|
||||
});
|
||||
|
||||
it("hält das Veröffentlichen im Protokoll fest", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
|
||||
const eintrag = await testPrisma.protokoll.findFirstOrThrow({
|
||||
where: { entitaetId: versionId, aktion: "Version veröffentlicht" },
|
||||
});
|
||||
expect(eintrag.benutzerId).toBe(benutzer.id);
|
||||
expect(eintrag.nachher).toMatchObject({ status: "VEROEFFENTLICHT" });
|
||||
});
|
||||
|
||||
it("weist eine Vorlage ohne Abschnitte ab", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { version } = await vorlageAnlegen({ name: "Leer" }, benutzer.id);
|
||||
await expect(versionVeroeffentlichen(version.id, null, benutzer.id)).rejects.toThrow(/ohne Abschnitte/);
|
||||
});
|
||||
|
||||
it("verändert bestehende Läufe nicht", async () => {
|
||||
// Das ist der Grund für die ganze Versionierung.
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage, versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
const kunde = await testPrisma.kunde.create({
|
||||
data: { name: "Eigene Organisation", kuerzel: "EIGEN", istEigeneOrganisation: true },
|
||||
});
|
||||
const lauf = await testPrisma.lauf.create({
|
||||
data: {
|
||||
vorlagenVersionId: versionId,
|
||||
titel: "PC-001 neu aufsetzen",
|
||||
kundeId: kunde.id,
|
||||
erstelltVonId: benutzer.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Zweite Version mit ganz anderem Inhalt veroeffentlichen.
|
||||
const entwurf = await entwurfHolenOderErzeugen(vorlage.id, benutzer.id);
|
||||
await entwurfSpeichern(entwurf.id, {
|
||||
variablen: [],
|
||||
abschnitte: [{ titel: "Alles neu", schritte: [{ titel: "Ein einziger Schritt" }] }],
|
||||
});
|
||||
await versionVeroeffentlichen(entwurf.id, "Umgebaut", benutzer.id);
|
||||
|
||||
const frisch = await testPrisma.lauf.findUniqueOrThrow({
|
||||
where: { id: lauf.id },
|
||||
include: {
|
||||
vorlagenVersion: {
|
||||
include: { abschnitte: { orderBy: { position: "asc" } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(frisch.vorlagenVersionId).toBe(versionId);
|
||||
expect(frisch.vorlagenVersion.version).toBe(1);
|
||||
expect(frisch.vorlagenVersion.abschnitte.map((a) => a.titel)).toEqual(["Vorbereitung", "Installation"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Höchstens ein Entwurf", () => {
|
||||
it("lehnt einen zweiten Entwurf auch an der Datenbank ab", async () => {
|
||||
// Die Regel steht zusaetzlich als partieller Index in der Datenbank – der
|
||||
// Dienst prueft nur fuer die bessere Meldung.
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage } = await veroeffentlichteVorlage(benutzer.id);
|
||||
await entwurfHolenOderErzeugen(vorlage.id, benutzer.id);
|
||||
|
||||
await expect(
|
||||
testPrisma.vorlagenVersion.create({
|
||||
data: { vorlageId: vorlage.id, version: 99, status: "ENTWURF" },
|
||||
}),
|
||||
).rejects.toThrow(/VorlagenVersion_ein_entwurf/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Entwurf verwerfen", () => {
|
||||
it("entfernt den Entwurf samt Aufbau", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage } = await veroeffentlichteVorlage(benutzer.id);
|
||||
const entwurf = await entwurfHolenOderErzeugen(vorlage.id, benutzer.id);
|
||||
|
||||
await entwurfVerwerfen(entwurf.id, benutzer.id);
|
||||
|
||||
expect(await testPrisma.vorlagenVersion.count({ where: { vorlageId: vorlage.id } })).toBe(1);
|
||||
// Die veroeffentlichte Version behaelt ihren Aufbau.
|
||||
expect(await testPrisma.vorlagenAbschnitt.count()).toBe(2);
|
||||
});
|
||||
|
||||
it("verwirft nicht die einzige Version", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { version } = await vorlageAnlegen({ name: "Probe" }, benutzer.id);
|
||||
await expect(entwurfVerwerfen(version.id, benutzer.id)).rejects.toThrow(/einzige Version/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Duplizieren", () => {
|
||||
it("legt eine eigenständige Kopie als Entwurf an", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage } = await veroeffentlichteVorlage(benutzer.id);
|
||||
|
||||
const kopie = await vorlageDuplizieren(vorlage.id, "PC Neuinstallation (Außendienst)", benutzer.id);
|
||||
|
||||
expect(kopie.id).not.toBe(vorlage.id);
|
||||
expect(kopie.name).toBe("PC Neuinstallation (Außendienst)");
|
||||
expect(kopie.kategorie).toBe("Client");
|
||||
|
||||
const versionen = await testPrisma.vorlagenVersion.findMany({ where: { vorlageId: kopie.id } });
|
||||
expect(versionen).toHaveLength(1);
|
||||
expect(versionen[0]?.status).toBe("ENTWURF");
|
||||
expect(versionen[0]?.version).toBe(1);
|
||||
|
||||
const aufbau = await strukturLesen(versionen[0]!.id);
|
||||
expect(aufbau.abschnitte.map((a) => a.titel)).toEqual(["Vorbereitung", "Installation"]);
|
||||
expect(aufbau.variablen).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("lässt eine Änderung an der Kopie das Original unberührt", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage, versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
const kopie = await vorlageDuplizieren(vorlage.id, "Kopie", benutzer.id);
|
||||
const kopieVersion = await testPrisma.vorlagenVersion.findFirstOrThrow({
|
||||
where: { vorlageId: kopie.id },
|
||||
});
|
||||
|
||||
await entwurfSpeichern(kopieVersion.id, { variablen: [], abschnitte: [{ titel: "X", schritte: [] }] });
|
||||
|
||||
const original = await strukturLesen(versionId);
|
||||
expect(original.abschnitte).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("weist eine Vorlage ohne veröffentlichte Version ab", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage } = await vorlageAnlegen({ name: "Nur Entwurf" }, benutzer.id);
|
||||
await expect(vorlageDuplizieren(vorlage.id, "Kopie", benutzer.id)).rejects.toThrow(
|
||||
/keine veröffentlichte Version/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Archivieren statt Löschen", () => {
|
||||
it("blendet die Vorlage aus, behält aber ihre Läufe", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage, versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
const kunde = await testPrisma.kunde.create({
|
||||
data: { name: "Eigene Organisation", kuerzel: "EIGEN", istEigeneOrganisation: true },
|
||||
});
|
||||
await testPrisma.lauf.create({
|
||||
data: {
|
||||
vorlagenVersionId: versionId,
|
||||
titel: "Ein Lauf",
|
||||
kundeId: kunde.id,
|
||||
erstelltVonId: benutzer.id,
|
||||
},
|
||||
});
|
||||
|
||||
await vorlageArchivieren(vorlage.id, true, benutzer.id);
|
||||
|
||||
const frisch = await testPrisma.vorlage.findUniqueOrThrow({ where: { id: vorlage.id } });
|
||||
expect(frisch.archiviert).toBe(true);
|
||||
expect(await testPrisma.lauf.count()).toBe(1);
|
||||
expect(await testPrisma.protokoll.count({ where: { aktion: "Vorlage archiviert" } })).toBe(1);
|
||||
});
|
||||
|
||||
it("schreibt nichts, wenn sich nichts ändert", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage } = await veroeffentlichteVorlage(benutzer.id);
|
||||
const vorher = await testPrisma.protokoll.count();
|
||||
await vorlageArchivieren(vorlage.id, false, benutzer.id);
|
||||
expect(await testPrisma.protokoll.count()).toBe(vorher);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Läufe hängen nur an veröffentlichten Versionen", () => {
|
||||
it("lässt eine veröffentlichte Version zu", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
await expect(veroeffentlichteVersionPruefen(versionId)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("weist einen Entwurf ab", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage } = await veroeffentlichteVorlage(benutzer.id);
|
||||
const entwurf = await entwurfHolenOderErzeugen(vorlage.id, benutzer.id);
|
||||
await expect(veroeffentlichteVersionPruefen(entwurf.id)).rejects.toThrow(/noch ein Entwurf/);
|
||||
});
|
||||
|
||||
it("weist eine archivierte Vorlage ab", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage, versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
await vorlageArchivieren(vorlage.id, true, benutzer.id);
|
||||
await expect(veroeffentlichteVersionPruefen(versionId)).rejects.toThrow(/startet keine Läufe/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Gleichzeitigkeit", () => {
|
||||
it("gibt bei zwei gleichzeitigen Bearbeitungen denselben Entwurf zurück", async () => {
|
||||
// Ohne Wiederholung lief der zweite Aufruf in den eindeutigen Index und der
|
||||
// Benutzer sah „Der Entwurf konnte nicht geöffnet werden“ – obwohl er in
|
||||
// derselben Sekunde angelegt wurde.
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage } = await veroeffentlichteVorlage(benutzer.id);
|
||||
|
||||
const ergebnisse = await Promise.allSettled([
|
||||
entwurfHolenOderErzeugen(vorlage.id, benutzer.id),
|
||||
entwurfHolenOderErzeugen(vorlage.id, benutzer.id),
|
||||
]);
|
||||
|
||||
const erfuellt = ergebnisse.filter((e) => e.status === "fulfilled");
|
||||
expect(erfuellt).toHaveLength(2);
|
||||
const ids = new Set(erfuellt.map((e) => e.value.id));
|
||||
expect(ids.size).toBe(1);
|
||||
expect(
|
||||
await testPrisma.vorlagenVersion.count({
|
||||
where: { vorlageId: vorlage.id, status: "ENTWURF" },
|
||||
}),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it("lässt Speichern nicht in eine gerade veröffentlichte Version schreiben", async () => {
|
||||
// Der wichtigste Test dieses Meilensteins: ohne Zeilensperre meldeten beide
|
||||
// Aufrufe Erfolg, und die veröffentlichte Version trug hinterher Inhalt,
|
||||
// der nach dem Veröffentlichen geschrieben wurde.
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage } = await veroeffentlichteVorlage(benutzer.id);
|
||||
const entwurf = await entwurfHolenOderErzeugen(vorlage.id, benutzer.id);
|
||||
|
||||
await Promise.allSettled([
|
||||
versionVeroeffentlichen(entwurf.id, "Veröffentlicht", benutzer.id),
|
||||
entwurfSpeichern(entwurf.id, {
|
||||
variablen: [],
|
||||
abschnitte: [{ titel: "NACHTRAEGLICH", schritte: [] }],
|
||||
}),
|
||||
]);
|
||||
|
||||
const danach = await strukturLesen(entwurf.id);
|
||||
if (danach.status === "VEROEFFENTLICHT") {
|
||||
// Wurde veroeffentlicht, darf der nachträgliche Aufbau nicht drinstehen.
|
||||
expect(danach.abschnitte.map((a) => a.titel)).not.toContain("NACHTRAEGLICH");
|
||||
}
|
||||
});
|
||||
|
||||
it("veröffentlicht bei zwei gleichzeitigen Aufrufen nur einmal", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage } = await veroeffentlichteVorlage(benutzer.id);
|
||||
const entwurf = await entwurfHolenOderErzeugen(vorlage.id, benutzer.id);
|
||||
await entwurfSpeichern(entwurf.id, STRUKTUR);
|
||||
|
||||
const ergebnisse = await Promise.allSettled([
|
||||
versionVeroeffentlichen(entwurf.id, "A", benutzer.id),
|
||||
versionVeroeffentlichen(entwurf.id, "B", benutzer.id),
|
||||
]);
|
||||
|
||||
expect(ergebnisse.filter((e) => e.status === "fulfilled")).toHaveLength(1);
|
||||
expect(
|
||||
await testPrisma.protokoll.count({
|
||||
where: { entitaetId: entwurf.id, aktion: "Version veröffentlicht" },
|
||||
}),
|
||||
).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Zwei Redakteure am selben Entwurf", () => {
|
||||
it("weist ein Speichern auf einem überholten Stand ab", async () => {
|
||||
// Ohne diese Prüfung überschriebe der zweite die Arbeit des ersten
|
||||
// spurlos – und weil Entwürfe bewusst nicht protokolliert werden, wäre sie
|
||||
// auch nicht wiederherstellbar.
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { version } = await vorlageAnlegen({ name: "Probe" }, benutzer.id);
|
||||
const geladen = await testPrisma.vorlagenVersion.findUniqueOrThrow({
|
||||
where: { id: version.id },
|
||||
});
|
||||
|
||||
// Der erste speichert.
|
||||
const { stand } = await entwurfSpeichern(version.id, STRUKTUR, geladen.geaendertAm);
|
||||
|
||||
// Der zweite hat noch den alten Stand in der Hand.
|
||||
await expect(
|
||||
entwurfSpeichern(
|
||||
version.id,
|
||||
{ variablen: [], abschnitte: [{ titel: "Meine Fassung", schritte: [] }] },
|
||||
geladen.geaendertAm,
|
||||
),
|
||||
).rejects.toThrow(/zwischenzeitlich/);
|
||||
|
||||
// Mit dem frischen Stand geht es.
|
||||
await expect(
|
||||
entwurfSpeichern(
|
||||
version.id,
|
||||
{ variablen: [], abschnitte: [{ titel: "Meine Fassung", schritte: [] }] },
|
||||
stand,
|
||||
),
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
it("speichert ohne mitgegebenen Stand weiterhin", async () => {
|
||||
// Ein Aufrufer, der den Stand nicht kennt (Import, Seed), soll nicht
|
||||
// blockiert sein.
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { version } = await vorlageAnlegen({ name: "Probe" }, benutzer.id);
|
||||
await expect(entwurfSpeichern(version.id, STRUKTUR)).resolves.toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Die Datenbank schützt die Unveränderlichkeit mit", () => {
|
||||
it("lässt einen Schritt einer veröffentlichten Version nicht ändern", async () => {
|
||||
// Bisher hing die Regel allein am Dienst. Migrationen, psql und ab Phase 4
|
||||
// die Connectoren schreiben daran vorbei.
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
const gelesen = await strukturLesen(versionId);
|
||||
const schritt = gelesen.abschnitte[0]?.schritte[0];
|
||||
|
||||
await expect(
|
||||
testPrisma.vorlagenSchritt.update({
|
||||
where: { id: schritt!.id },
|
||||
data: { titel: "Heimlich geändert" },
|
||||
}),
|
||||
).rejects.toThrow(/unveraenderlich/i);
|
||||
});
|
||||
|
||||
it("lässt Abschnitte einer veröffentlichten Version nicht löschen", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
await expect(testPrisma.vorlagenAbschnitt.deleteMany({ where: { versionId } })).rejects.toThrow(
|
||||
/unveraenderlich/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("lässt keine Variable an eine veröffentlichte Version hängen", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
await expect(
|
||||
testPrisma.vorlagenVariable.create({
|
||||
data: { versionId, schluessel: "heimlich", beschriftung: "Heimlich" },
|
||||
}),
|
||||
).rejects.toThrow(/unveraenderlich/i);
|
||||
});
|
||||
|
||||
it("lässt eine veröffentlichte Version nicht zum Entwurf zurückdrehen", async () => {
|
||||
// Das wäre die Hintertür, durch die sich alles Obige aushebeln ließe.
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
await expect(
|
||||
testPrisma.vorlagenVersion.update({
|
||||
where: { id: versionId },
|
||||
data: { status: "ENTWURF" },
|
||||
}),
|
||||
).rejects.toThrow(/nicht wieder zum Entwurf/i);
|
||||
});
|
||||
|
||||
it("lässt einen Entwurf weiterhin frei bearbeiten", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage } = await veroeffentlichteVorlage(benutzer.id);
|
||||
const entwurf = await entwurfHolenOderErzeugen(vorlage.id, benutzer.id);
|
||||
const gelesen = await strukturLesen(entwurf.id);
|
||||
await expect(
|
||||
testPrisma.vorlagenAbschnitt.update({
|
||||
where: { id: gelesen.abschnitte[0]!.id },
|
||||
data: { titel: "Umbenannt" },
|
||||
}),
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Kopfdaten bearbeiten", () => {
|
||||
it("ändert Name, Kategorie und Schlagworte und protokolliert es", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage } = await veroeffentlichteVorlage(benutzer.id);
|
||||
|
||||
await vorlageBearbeiten(
|
||||
vorlage.id,
|
||||
{ name: "PC Neuinstallation (Standard)", kategorie: "Client", schlagworte: ["windows"] },
|
||||
benutzer.id,
|
||||
);
|
||||
|
||||
const frisch = await testPrisma.vorlage.findUniqueOrThrow({ where: { id: vorlage.id } });
|
||||
expect(frisch.name).toBe("PC Neuinstallation (Standard)");
|
||||
expect(frisch.schlagworte).toEqual(["windows"]);
|
||||
const eintrag = await testPrisma.protokoll.findFirstOrThrow({
|
||||
where: { entitaetId: vorlage.id, aktion: { contains: "geändert" } },
|
||||
});
|
||||
expect(eintrag.aktion).toContain("Name");
|
||||
});
|
||||
|
||||
it("lässt die Versionen unberührt", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage, versionId } = await veroeffentlichteVorlage(benutzer.id);
|
||||
await vorlageBearbeiten(vorlage.id, { name: "Anders" }, benutzer.id);
|
||||
const gelesen = await strukturLesen(versionId);
|
||||
expect(gelesen.abschnitte).toHaveLength(2);
|
||||
expect(gelesen.status).toBe("VEROEFFENTLICHT");
|
||||
});
|
||||
|
||||
it("schreibt nichts, wenn sich nichts ändert", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage } = await veroeffentlichteVorlage(benutzer.id);
|
||||
const vorher = await testPrisma.protokoll.count();
|
||||
await vorlageBearbeiten(vorlage.id, { name: "PC Neuinstallation" }, benutzer.id);
|
||||
expect(await testPrisma.protokoll.count()).toBe(vorher);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Archivierte Vorlage", () => {
|
||||
it("lässt sich nicht veröffentlichen", async () => {
|
||||
const benutzer = await benutzerAnlegen();
|
||||
const { vorlage } = await veroeffentlichteVorlage(benutzer.id);
|
||||
const entwurf = await entwurfHolenOderErzeugen(vorlage.id, benutzer.id);
|
||||
await vorlageArchivieren(vorlage.id, true, benutzer.id);
|
||||
|
||||
await expect(versionVeroeffentlichen(entwurf.id, null, benutzer.id)).rejects.toThrow(/archiviert/);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user