Files
Laufzettel/tests/bausteine.test.ts
T
Florian WildeandClaude Opus 5 80d97467da
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
fix: Reviewbefunde aus der Durchsicht von Phase 2 (#036, #027)
Acht Meilensteine lagen ohne fremden Blick auf main. Ein Review mit eigener
Datenbank, Gegenproben und vollem Suitenlauf hat sie gegen die
Akzeptanzkriterien und gegen CLAUDE.md geprueft.

Die Kernregeln halten: fuenf von fuenf Gegenproben gegen Versionierung,
Mandantengrenze, Abschlussregel und Selbstauskunft wurden rot. Alle Befunde
lagen am Rand — und die drei wertvollsten waren Gegenproben, die gruen blieben.

Schwer:

- Der Link in der Mail "Verzeichnis-Abgleich fehlgeschlagen" war ein 404
  (/verwaltung/verzeichnis statt /admin/verzeichnis). Die eine Meldung, die an
  alle Administratoren geht, im Moment eines Ausfalls. Ein Test prueft jetzt
  jeden Meldungspfad gegen die vorhandenen Routen.
- Der Export eines Entwurfs mit Baustein lieferte einen leeren Abschnitt unter
  dem Platzhalter "wird ersetzt" — der Zweck des Formats ist, eine Vorlage in
  eine zweite Installation zu bringen. Bausteine werden jetzt aufgeloest.

Mittel:

- Der Anhang-Handler hatte keinen Test; die ganze Rechtepruefung liess sich
  durch einen hartcodierten ADMIN ersetzen, Suite gruen. Es ist die einzige
  Stelle, die rohe Dateibytes ausliefert. tests/anhang-route.test.ts faehrt ihn
  jetzt echt, acht Pruefungen.
- /api/scim in der Proxy-Allowlist stand in keinem Test. OEFFENTLICH ist
  exportiert, der Test vergleicht beide Richtungen: eine Liste, die nur in einer
  Richtung geprueft wird, laeuft dem Code hinterher.
- Ein offener Ereignisstrom ueberlebte Abmelden, Deaktivieren und
  Sitzungswiderruf. Der Herzschlag prueft die Sitzung jetzt nach. Dass es kein
  Test zeigte, lag an einer Attrappe `sitzung: {}` — eine Attrappe ohne die Form
  der Wirklichkeit prueft etwas anderes als die Wirklichkeit.
- README kannte MITARBEITER nicht und behauptete, unbekannte Benutzer wuerden
  angelegt. Wer auf v0.2.0 hebt und nur das README liest, haette genau diese
  Leute ausgesperrt.
- sicherung.md nannte einen Anhaenge-Pfad, den es nirgends gibt — als
  Rueckfallwert in einem Skript mit rm -rf. Jetzt bricht es ab statt zu raten.
- Attributnamen wurden nur in der Server Action geprueft, waehrend der
  Graph-Leser sich im Kommentar darauf berief.

Dazu #027: die Ende-zu-Ende-Ablaeufe raeumen jetzt zentral auf, vor und nach
dem Lauf, und ein struktureller Test bewacht es. Beim ersten echten Lauf seit
dem 26. September waren sieben Ablaeufe rot, vier Ursachen, keine ein
Produktfehler — unbemerkt, weil pnpm test:e2e in keinem Tor steht. Das ist jetzt
#038 und keine Fussnote.

Ein Befund des Reviews war falsch (der Zustands-Handler habe keinen Test; er hat
acht direkte Aufrufe). Nachgeprueft, bevor etwas geaendert wurde.

1454 Vitest-Tests (vorher 1432), 113 E2E-Ablaeufe gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-11 01:32:09 +02:00

607 lines
24 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { beforeEach, describe, expect, it } from "vitest";
import { VersionStatus } from "@/generated/prisma/enums";
import {
BausteinFehler,
bausteinAnlegen,
bausteinArchivieren,
bausteinBearbeiten,
bausteinEntwurfHolenOderErzeugen,
bausteinEntwurfSpeichern,
bausteinEntwurfVerwerfen,
bausteinVersionVeroeffentlichen,
} from "@/server/services/bausteine";
import {
bausteinHolen,
bausteinListe,
bausteinVerwendungen,
einsetzbareBausteine,
} from "@/server/queries/bausteine";
import {
VorlagenFehler,
entwurfHolenOderErzeugen,
entwurfSpeichern,
versionVeroeffentlichen,
vorlageAnlegen,
} from "@/server/services/vorlagen";
import { vorlageExportieren, vorlageImportieren } from "@/server/services/vorlagen-austausch";
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
/**
* Wiederverwendbare Bausteine (#030).
*
* Die eine Zusage, die alles traegt: **eine Baustein-Aenderung fasst nichts
* Veroeffentlichtes an.** Ein Entwurf verweist, eine veroeffentlichte Version
* traegt Kopien — und ein Lauf sieht deshalb weiterhin konkrete Schritte.
*/
async function grundbestand() {
const admin = await testPrisma.benutzer.create({
data: { email: "admin@firma.example", name: "Admin", rolle: "ADMIN" },
});
return { adminId: admin.id };
}
/** Ein veroeffentlichter Baustein mit den uebergebenen Schritttiteln. */
async function bausteinMitSchritten(
name: string,
titel: string,
schritte: readonly string[],
adminId: string,
): Promise<{ bausteinId: string; versionId: string }> {
const { bausteinId, versionId } = await bausteinAnlegen({ name }, adminId);
await bausteinEntwurfSpeichern(versionId, {
titel,
schritte: schritte.map((schritt) => ({ titel: schritt })),
});
await bausteinVersionVeroeffentlichen(versionId, null, adminId);
return { bausteinId, versionId };
}
/** Eine Vorlage mit einem Baustein-Abschnitt und einem eigenen, veroeffentlicht. */
async function vorlageMitBaustein(
name: string,
bausteinVersionId: string,
adminId: string,
): Promise<{ vorlageId: string; versionId: string }> {
const { vorlage } = await vorlageAnlegen({ name }, adminId);
const entwurf = await entwurfHolenOderErzeugen(vorlage.id, adminId);
const versionId = entwurf.id;
await entwurfSpeichern(versionId, {
variablen: [],
abschnitte: [
{ titel: "Eigener Abschnitt", schritte: [{ titel: "Eigener Schritt" }] },
{ titel: "wird ersetzt", bausteinVersionId, schritte: [] },
],
});
await versionVeroeffentlichen(versionId, null, adminId);
return { vorlageId: vorlage.id, versionId };
}
async function schritteVon(versionId: string): Promise<readonly string[]> {
const abschnitte = await testPrisma.vorlagenAbschnitt.findMany({
where: { versionId },
orderBy: { position: "asc" },
include: { schritte: { orderBy: { position: "asc" } } },
});
return abschnitte.flatMap((abschnitt) => [
`# ${abschnitt.titel}`,
...abschnitt.schritte.map((schritt) => schritt.titel),
]);
}
describe.runIf(datenbankVerfuegbar)("Bausteine", () => {
beforeEach(async () => {
await datenbankLeeren();
});
describe("Pflegen", () => {
it("legt einen Baustein mit Entwurf in Version 1 an", async () => {
const { adminId } = await grundbestand();
const { bausteinId, versionId } = await bausteinAnlegen({ name: "Datensicherung prüfen" }, adminId);
const ansicht = await bausteinHolen(bausteinId);
expect(ansicht?.name).toBe("Datensicherung prüfen");
expect(ansicht?.versionen).toHaveLength(1);
expect(ansicht?.versionen[0]).toMatchObject({
id: versionId,
version: 1,
status: VersionStatus.ENTWURF,
// Der Titel beginnt als Name – in den meisten Fällen stimmt das.
titel: "Datensicherung prüfen",
});
});
it("hält Namen eindeutig, mit einem Satz statt eines Datenbankfehlers", async () => {
const { adminId } = await grundbestand();
await bausteinAnlegen({ name: "Datensicherung prüfen" }, adminId);
await expect(bausteinAnlegen({ name: "Datensicherung prüfen" }, adminId)).rejects.toThrow(
/gibt schon einen Baustein/,
);
});
it("lässt je Baustein höchstens einen Entwurf zu – auch an der Service-Schicht vorbei", async () => {
const { adminId } = await grundbestand();
const { bausteinId } = await bausteinAnlegen({ name: "Einmal" }, adminId);
await expect(
testPrisma.bausteinVersion.create({
data: { bausteinId, version: 2, status: VersionStatus.ENTWURF, titel: "zweiter" },
}),
).rejects.toThrow(/BausteinVersion_ein_entwurf/);
});
it("weist einen Baustein ohne Schritte beim Veröffentlichen ab", async () => {
const { adminId } = await grundbestand();
const { versionId } = await bausteinAnlegen({ name: "Leer" }, adminId);
await expect(bausteinVersionVeroeffentlichen(versionId, null, adminId)).rejects.toThrow(
/ohne Schritte/,
);
});
it("macht aus einer veröffentlichten Version einen Entwurf als tiefe Kopie", async () => {
const { adminId } = await grundbestand();
const { bausteinId } = await bausteinMitSchritten(
"Datensicherung prüfen",
"Datensicherung",
["Sicherung läuft", "Rücksicherung getestet"],
adminId,
);
const entwurfId = await bausteinEntwurfHolenOderErzeugen(bausteinId, adminId);
const ansicht = await bausteinHolen(bausteinId);
expect(ansicht?.versionen).toHaveLength(2);
const entwurf = ansicht?.versionen.find((version) => version.id === entwurfId);
expect(entwurf?.version).toBe(2);
expect(entwurf?.schritte.map((schritt) => schritt.titel)).toEqual([
"Sicherung läuft",
"Rücksicherung getestet",
]);
// Zweiter Aufruf gibt denselben Entwurf, nicht einen dritten.
expect(await bausteinEntwurfHolenOderErzeugen(bausteinId, adminId)).toBe(entwurfId);
});
it("lässt eine veröffentlichte Version nicht ändern – der Trigger hält das fest", async () => {
const { adminId } = await grundbestand();
const { versionId } = await bausteinMitSchritten("Fest", "Fest", ["Eins"], adminId);
await expect(
bausteinEntwurfSpeichern(versionId, { titel: "anders", schritte: [{ titel: "Zwei" }] }),
).rejects.toThrow(/unveränderlich/);
// Und auch an der Service-Schicht vorbei nicht.
await expect(
testPrisma.bausteinSchritt.create({ data: { versionId, titel: "Geschmuggelt" } }),
).rejects.toThrow(/unveraenderlich/);
});
it("lässt eine veröffentlichte Version nicht zum Entwurf zurückmachen", async () => {
const { adminId } = await grundbestand();
const { versionId } = await bausteinMitSchritten("Einbahn", "Einbahn", ["Eins"], adminId);
await expect(
testPrisma.bausteinVersion.update({
where: { id: versionId },
data: { status: VersionStatus.ENTWURF },
}),
).rejects.toThrow(/nicht wieder zum Entwurf/);
});
it("verwirft einen Entwurf nur, wenn es noch eine veröffentlichte Version gibt", async () => {
const { adminId } = await grundbestand();
const { bausteinId, versionId } = await bausteinAnlegen({ name: "Einzig" }, adminId);
await expect(bausteinEntwurfVerwerfen(versionId, adminId)).rejects.toThrow(/einzige Version/);
await bausteinEntwurfSpeichern(versionId, { titel: "Einzig", schritte: [{ titel: "Eins" }] });
await bausteinVersionVeroeffentlichen(versionId, null, adminId);
const zweiter = await bausteinEntwurfHolenOderErzeugen(bausteinId, adminId);
await bausteinEntwurfVerwerfen(zweiter, adminId);
expect((await bausteinHolen(bausteinId))?.versionen).toHaveLength(1);
});
it("benennt um und protokolliert", async () => {
const { adminId } = await grundbestand();
const { bausteinId } = await bausteinAnlegen({ name: "Alt" }, adminId);
await bausteinBearbeiten(bausteinId, { name: "Neu", beschreibung: "Erklärung" }, adminId);
expect((await bausteinHolen(bausteinId))?.name).toBe("Neu");
const eintraege = await testPrisma.protokoll.findMany({
where: { entitaet: "Baustein", entitaetId: bausteinId },
orderBy: { erstelltAm: "asc" },
select: { aktion: true },
});
expect(eintraege.map((eintrag) => eintrag.aktion)).toEqual([
"Baustein angelegt",
"Name und Beschreibung geändert",
]);
});
});
describe("Einsetzen und Auflösen", () => {
it("setzt die Schritte des Bausteins beim Veröffentlichen ein", async () => {
const { adminId } = await grundbestand();
const { versionId: bausteinVersionId } = await bausteinMitSchritten(
"Datensicherung prüfen",
"Datensicherung",
["Sicherung läuft", "Rücksicherung getestet"],
adminId,
);
const { versionId } = await vorlageMitBaustein("PC Neuinstallation", bausteinVersionId, adminId);
expect(await schritteVon(versionId)).toEqual([
"# Eigener Abschnitt",
"Eigener Schritt",
// Titel und Schritte kommen aus dem Baustein, nicht aus dem Entwurf.
"# Datensicherung",
"Sicherung läuft",
"Rücksicherung getestet",
]);
});
it("lässt den Verweis als Herkunft stehen", async () => {
const { adminId } = await grundbestand();
const { versionId: bausteinVersionId } = await bausteinMitSchritten(
"Herkunft",
"Herkunft",
["Eins"],
adminId,
);
const { versionId } = await vorlageMitBaustein("Mit Herkunft", bausteinVersionId, adminId);
const abschnitt = await testPrisma.vorlagenAbschnitt.findFirstOrThrow({
where: { versionId, bausteinVersionId: { not: null } },
include: { bausteinVersion: { include: { baustein: true } } },
});
expect(abschnitt.bausteinVersion?.baustein.name).toBe("Herkunft");
});
it("weist einen Verweis auf einen Entwurf ab", async () => {
// Einen Entwurf einzusetzen hieße, beim Veröffentlichen etwas zu kopieren,
// das sich danach noch ändert.
const { adminId } = await grundbestand();
const { versionId: entwurfId } = await bausteinAnlegen({ name: "Noch Entwurf" }, adminId);
const { vorlage } = await vorlageAnlegen({ name: "Will einen Entwurf" }, adminId);
const versionId = (await entwurfHolenOderErzeugen(vorlage.id, adminId)).id;
await expect(
entwurfSpeichern(versionId, {
variablen: [],
abschnitte: [{ titel: "x", bausteinVersionId: entwurfId, schritte: [] }],
}),
).rejects.toThrow(/veroeffentlichte Baustein-Version/);
});
it("weist das Veröffentlichen ab, wenn der Baustein archiviert wurde", async () => {
const { adminId } = await grundbestand();
const { bausteinId, versionId: bausteinVersionId } = await bausteinMitSchritten(
"Weggelegt",
"Weggelegt",
["Eins"],
adminId,
);
const { vorlage } = await vorlageAnlegen({ name: "Mit weggelegtem" }, adminId);
const versionId = (await entwurfHolenOderErzeugen(vorlage.id, adminId)).id;
await entwurfSpeichern(versionId, {
variablen: [],
abschnitte: [{ titel: "x", bausteinVersionId, schritte: [] }],
});
// Archivieren geht hier nicht: der Baustein steht in einem Entwurf.
await expect(bausteinArchivieren(bausteinId, true, adminId)).rejects.toThrow(/Entwurf/);
// An der Service-Schicht vorbei archiviert – dann greift die Prüfung beim
// Veröffentlichen.
await testPrisma.baustein.update({ where: { id: bausteinId }, data: { archiviert: true } });
await expect(versionVeroeffentlichen(versionId, null, adminId)).rejects.toThrow(/archiviert/);
});
it("hält im Protokoll fest, welcher Baustein in welcher Fassung eingesetzt wurde", async () => {
const { adminId } = await grundbestand();
const { versionId: bausteinVersionId } = await bausteinMitSchritten(
"Datensicherung prüfen",
"Datensicherung",
["Eins"],
adminId,
);
const { versionId } = await vorlageMitBaustein("Mit Protokoll", bausteinVersionId, adminId);
const eintrag = await testPrisma.protokoll.findFirstOrThrow({
where: { entitaet: "VorlagenVersion", entitaetId: versionId, aktion: "Version veröffentlicht" },
});
expect(eintrag.nachher).toMatchObject({
bausteine: [{ name: "Datensicherung prüfen", version: 1 }],
});
});
});
describe("Unveränderlichkeit über Baustein-Änderungen hinweg", () => {
it("ändert keine veröffentlichte Vorlagenversion, wenn der Baustein neu veröffentlicht wird", async () => {
const { adminId } = await grundbestand();
const { bausteinId, versionId: ersteBausteinVersion } = await bausteinMitSchritten(
"Datensicherung prüfen",
"Datensicherung",
["Sicherung läuft"],
adminId,
);
const { versionId } = await vorlageMitBaustein("PC Neuinstallation", ersteBausteinVersion, adminId);
const vorher = await schritteVon(versionId);
// Zweite Fassung des Bausteins, inhaltlich ganz anders.
const zweiterEntwurf = await bausteinEntwurfHolenOderErzeugen(bausteinId, adminId);
await bausteinEntwurfSpeichern(zweiterEntwurf, {
titel: "Datensicherung, neu gedacht",
schritte: [{ titel: "Ganz anders" }, { titel: "Und noch einer" }],
});
await bausteinVersionVeroeffentlichen(zweiterEntwurf, "Umgebaut", adminId);
expect(await schritteVon(versionId)).toEqual(vorher);
});
it("lässt einen laufenden Lauf unberührt", async () => {
const { adminId } = await grundbestand();
const kunde = await testPrisma.kunde.create({
data: { name: "Eigene Organisation", kuerzel: "EIGEN", istEigeneOrganisation: true },
});
const { bausteinId, versionId: ersteBausteinVersion } = await bausteinMitSchritten(
"Datensicherung prüfen",
"Datensicherung",
["Sicherung läuft"],
adminId,
);
const { versionId } = await vorlageMitBaustein("PC Neuinstallation", ersteBausteinVersion, adminId);
const lauf = await testPrisma.lauf.create({
data: { vorlagenVersionId: versionId, titel: "Ein Lauf", kundeId: kunde.id },
});
const zweiterEntwurf = await bausteinEntwurfHolenOderErzeugen(bausteinId, adminId);
await bausteinEntwurfSpeichern(zweiterEntwurf, {
titel: "Anders",
schritte: [{ titel: "Ganz anders" }],
});
await bausteinVersionVeroeffentlichen(zweiterEntwurf, null, adminId);
const nachher = await testPrisma.lauf.findUniqueOrThrow({
where: { id: lauf.id },
include: {
vorlagenVersion: {
include: { abschnitte: { include: { schritte: true }, orderBy: { position: "asc" } } },
},
},
});
const titel = nachher.vorlagenVersion.abschnitte.flatMap((abschnitt) =>
abschnitt.schritte.map((schritt) => schritt.titel),
);
expect(titel).toContain("Sicherung läuft");
expect(titel).not.toContain("Ganz anders");
});
it("nimmt die neue Fassung in die nächste veröffentlichte Vorlagenversion", async () => {
const { adminId } = await grundbestand();
const { bausteinId, versionId: ersteBausteinVersion } = await bausteinMitSchritten(
"Datensicherung prüfen",
"Datensicherung",
["Sicherung läuft"],
adminId,
);
const { vorlageId } = await vorlageMitBaustein("PC Neuinstallation", ersteBausteinVersion, adminId);
const zweiterEntwurf = await bausteinEntwurfHolenOderErzeugen(bausteinId, adminId);
await bausteinEntwurfSpeichern(zweiterEntwurf, {
titel: "Datensicherung",
schritte: [{ titel: "Sicherung läuft" }, { titel: "Rücksicherung getestet" }],
});
await bausteinVersionVeroeffentlichen(zweiterEntwurf, null, adminId);
const neueFassung = await testPrisma.bausteinVersion.findFirstOrThrow({
where: { bausteinId, status: VersionStatus.VEROEFFENTLICHT },
orderBy: { version: "desc" },
});
// Neue Vorlagenversion, die auf die neue Fassung zeigt.
const zweiteVorlagenVersion = (await entwurfHolenOderErzeugen(vorlageId, adminId)).id;
await entwurfSpeichern(zweiteVorlagenVersion, {
variablen: [],
abschnitte: [
{ titel: "Eigener Abschnitt", schritte: [{ titel: "Eigener Schritt" }] },
{ titel: "x", bausteinVersionId: neueFassung.id, schritte: [] },
],
});
await versionVeroeffentlichen(zweiteVorlagenVersion, null, adminId);
expect(await schritteVon(zweiteVorlagenVersion)).toEqual([
"# Eigener Abschnitt",
"Eigener Schritt",
"# Datensicherung",
"Sicherung läuft",
"Rücksicherung getestet",
]);
});
});
describe("Löschen und Verwendungen", () => {
it("lässt eine Baustein-Version nicht löschen, auf die eine Vorlage zeigt", async () => {
const { adminId } = await grundbestand();
const { versionId: bausteinVersionId } = await bausteinMitSchritten(
"Benutzt",
"Benutzt",
["Eins"],
adminId,
);
await vorlageMitBaustein("Benutzt ihn", bausteinVersionId, adminId);
await expect(testPrisma.bausteinVersion.delete({ where: { id: bausteinVersionId } })).rejects.toThrow();
});
it("nennt, wo ein Baustein steckt – mit Vorlage, Version und Fassung", async () => {
const { adminId } = await grundbestand();
const { bausteinId, versionId: bausteinVersionId } = await bausteinMitSchritten(
"Überall",
"Überall",
["Eins"],
adminId,
);
await vorlageMitBaustein("Vorlage A", bausteinVersionId, adminId);
await vorlageMitBaustein("Vorlage B", bausteinVersionId, adminId);
const verwendungen = await bausteinVerwendungen(bausteinId);
expect(verwendungen).toHaveLength(2);
expect(verwendungen.map((eintrag) => eintrag.vorlageName)).toEqual(["Vorlage A", "Vorlage B"]);
expect(verwendungen[0]).toMatchObject({
vorlagenVersion: 1,
status: VersionStatus.VEROEFFENTLICHT,
bausteinVersion: 1,
});
});
it("zählt in der Liste Verwendungen und Schritte", async () => {
const { adminId } = await grundbestand();
const { versionId: bausteinVersionId } = await bausteinMitSchritten(
"Gezählt",
"Gezählt",
["Eins", "Zwei"],
adminId,
);
await vorlageMitBaustein("Benutzt ihn", bausteinVersionId, adminId);
const liste = await bausteinListe();
expect(liste).toHaveLength(1);
expect(liste[0]).toMatchObject({
name: "Gezählt",
veroeffentlichteVersion: 1,
hatEntwurf: false,
anzahlSchritte: 2,
verwendungen: 1,
});
});
it("bietet zum Einsetzen nur veröffentlichte Fassungen nicht archivierter Bausteine", async () => {
const { adminId } = await grundbestand();
await bausteinMitSchritten("Brauchbar", "Brauchbar", ["Eins"], adminId);
await bausteinAnlegen({ name: "Nur Entwurf" }, adminId);
const { bausteinId } = await bausteinMitSchritten("Weggelegt", "Weggelegt", ["Eins"], adminId);
await bausteinArchivieren(bausteinId, true, adminId);
const einsetzbar = await einsetzbareBausteine();
expect(einsetzbar.map((eintrag) => eintrag.name)).toEqual(["Brauchbar"]);
expect(einsetzbar[0]).toMatchObject({ version: 1, anzahlSchritte: 1, titel: "Brauchbar" });
});
});
describe("Austausch", () => {
it("exportiert die aufgelösten Schritte und keinen Verweis", async () => {
// Eine Kennung aus dieser Installation bedeutet in einer anderen nichts.
const { adminId } = await grundbestand();
const { versionId: bausteinVersionId } = await bausteinMitSchritten(
"Datensicherung prüfen",
"Datensicherung",
["Sicherung läuft"],
adminId,
);
const { versionId } = await vorlageMitBaustein("Zum Ausfuhren", bausteinVersionId, adminId);
const austausch = await vorlageExportieren(versionId);
expect(JSON.stringify(austausch)).not.toContain(bausteinVersionId);
const abschnitte = austausch.struktur.abschnitte.map((abschnitt) => ({
titel: abschnitt.titel,
schritte: abschnitt.schritte.map((schritt) => schritt.titel),
}));
expect(abschnitte).toEqual([
{ titel: "Eigener Abschnitt", schritte: ["Eigener Schritt"] },
{ titel: "Datensicherung", schritte: ["Sicherung läuft"] },
]);
});
it("exportiert auch einen Entwurf mit seinen Baustein-Schritten", async () => {
/*
* **Der Fall, der still einen leeren Abschnitt lieferte** (Reviewbefund zu
* #029). Ein Baustein-Abschnitt im Entwurf hat keine eigenen Schritte —
* die entstehen erst beim Veröffentlichen. Der Export schrieb deshalb
* `{"titel":"wird ersetzt","schritte":[]}`, und der Import nahm das
* stillschweigend an: das Schema lässt einen leeren Abschnitt zu.
*
* Ein Redakteur bringt eine noch unveröffentlichte Vorlage in eine zweite
* Installation — genau der Zweck des Formats. Dort kam der geteilte
* Abschnitt unter dem Editor-Platzhalter „wird ersetzt“ und ohne einen
* einzigen Schritt an. Die Checkliste war unvollständig, und nichts sagte
* es.
*/
const { adminId } = await grundbestand();
const { versionId: bausteinVersionId } = await bausteinMitSchritten(
"Datensicherung prüfen",
"Datensicherung",
["Sicherung läuft", "Rücksicherung geprüft"],
adminId,
);
// Wie `vorlageMitBaustein`, aber **ohne** zu veröffentlichen.
const { vorlage } = await vorlageAnlegen({ name: "Noch Entwurf" }, adminId);
const entwurf = await entwurfHolenOderErzeugen(vorlage.id, adminId);
await entwurfSpeichern(entwurf.id, {
variablen: [],
abschnitte: [
{ titel: "Eigener Abschnitt", schritte: [{ titel: "Eigener Schritt" }] },
{ titel: "wird ersetzt", bausteinVersionId, schritte: [] },
],
});
const austausch = await vorlageExportieren(entwurf.id);
expect(
austausch.struktur.abschnitte.map((abschnitt) => ({
titel: abschnitt.titel,
schritte: abschnitt.schritte.map((schritt) => schritt.titel),
})),
).toEqual([
{ titel: "Eigener Abschnitt", schritte: ["Eigener Schritt"] },
// Der Titel aus dem Baustein, nicht der Platzhalter – und die Schritte.
{ titel: "Datensicherung", schritte: ["Sicherung läuft", "Rücksicherung geprüft"] },
]);
// Und weiterhin kein Verweis: in der anderen Installation gibt es den
// Baustein vielleicht nicht.
expect(JSON.stringify(austausch)).not.toContain(bausteinVersionId);
// Kein Abschnitt ohne Schritte – das ist die Zusage, die vorher brach.
for (const abschnitt of austausch.struktur.abschnitte) {
expect(abschnitt.schritte.length, abschnitt.titel).toBeGreaterThan(0);
}
});
it("übernimmt beim Import keinen mitgeschickten Verweis", async () => {
const { adminId } = await grundbestand();
const { versionId: bausteinVersionId } = await bausteinMitSchritten(
"Datensicherung prüfen",
"Datensicherung",
["Sicherung läuft"],
adminId,
);
const { versionId } = await vorlageMitBaustein("Quelle", bausteinVersionId, adminId);
const austausch = await vorlageExportieren(versionId);
const { versionId: kopie } = await vorlageImportieren(
{
...austausch,
name: "Kopie",
struktur: {
...austausch.struktur,
abschnitte: austausch.struktur.abschnitte.map((abschnitt) => ({
...abschnitt,
bausteinVersionId,
})),
},
},
adminId,
);
const abschnitte = await testPrisma.vorlagenAbschnitt.findMany({ where: { versionId: kopie } });
expect(abschnitte.every((abschnitt) => abschnitt.bausteinVersionId === null)).toBe(true);
// Die Schritte sind trotzdem alle da – sie stehen in der Datei.
expect(await schritteVon(kopie)).toEqual([
"# Eigener Abschnitt",
"Eigener Schritt",
"# Datensicherung",
"Sicherung läuft",
]);
});
});
it("wirft eigene Fehlerklassen, damit die Actions sie unterscheiden können", async () => {
const { adminId } = await grundbestand();
await expect(bausteinAnlegen({ name: " " }, adminId)).rejects.toThrow(BausteinFehler);
await expect(versionVeroeffentlichen("gibt-es-nicht", null, adminId)).rejects.toThrow(VorlagenFehler);
});
});