742 lines
30 KiB
TypeScript
742 lines
30 KiB
TypeScript
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/);
|
||
});
|
||
});
|
||
});
|