feat(bausteine): Abschnitte, die mehrere Vorlagen teilen (#57)
This commit was merged in pull request #57.
This commit is contained in:
@@ -0,0 +1,552 @@
|
||||
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("ü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);
|
||||
});
|
||||
});
|
||||
@@ -51,6 +51,9 @@ describe.runIf(datenbankVerfuegbar)("Handgeschriebene Datenbankobjekte", () => {
|
||||
|
||||
it("hat die Trigger, die eine veröffentlichte Version schützen", async () => {
|
||||
expect(await namen(`SELECT tgname AS name FROM pg_trigger WHERE NOT tgisinternal`)).toEqual([
|
||||
"BausteinSchritt_unveraenderlich",
|
||||
"BausteinVersion_status_einbahn",
|
||||
"VorlagenAbschnitt_baustein_veroeffentlicht",
|
||||
"VorlagenAbschnitt_unveraenderlich",
|
||||
"VorlagenSchritt_unveraenderlich",
|
||||
"VorlagenVariable_unveraenderlich",
|
||||
@@ -64,6 +67,7 @@ describe.runIf(datenbankVerfuegbar)("Handgeschriebene Datenbankobjekte", () => {
|
||||
`SELECT indexname AS name FROM pg_indexes WHERE schemaname='public' AND indexdef LIKE '%WHERE%'`,
|
||||
),
|
||||
).toEqual([
|
||||
"BausteinVersion_ein_entwurf",
|
||||
"Kunde_eigene_organisation_einmalig",
|
||||
"Vorlage_seedSchluessel_einmalig",
|
||||
"VorlagenVersion_ein_entwurf",
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
verwendetePlatzhalter,
|
||||
type EditorAufbau,
|
||||
type EditorVariable,
|
||||
bausteineAnreichern,
|
||||
neuerBausteinAbschnitt,
|
||||
} from "@/lib/editor-aufbau";
|
||||
import { EINGEBAUTE_PLATZHALTER } from "@/lib/platzhalter";
|
||||
import { StrukturSchema } from "@/lib/vorlagen-format";
|
||||
@@ -254,3 +256,70 @@ describe("Beispielwerte der Vorschau", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bausteine im Editor-Aufbau", () => {
|
||||
const BAUSTEIN = {
|
||||
bausteinVersionId: "bv-1",
|
||||
name: "Datensicherung prüfen",
|
||||
titel: "Datensicherung",
|
||||
version: 3,
|
||||
anzahlSchritte: 2,
|
||||
};
|
||||
|
||||
it("schickt die Platzhalter-Schritte eines Baustein-Abschnitts nicht mit", () => {
|
||||
/*
|
||||
* Der Fehler, den erst das Durchklicken des gebauten Stands gezeigt hat
|
||||
* (#030): die Schritte im Zustand sind nur da, damit die Anzahl in der Karte
|
||||
* stimmt — sie haben keinen Titel. Mitgeschickt wies `SchrittSchema` den
|
||||
* ganzen Entwurf ab, und das Speichern scheiterte.
|
||||
*/
|
||||
const abschnitt = neuerBausteinAbschnitt(BAUSTEIN);
|
||||
expect(abschnitt.schritte).toHaveLength(2);
|
||||
expect(abschnitt.schritte.every((schritt) => schritt.titel === "")).toBe(true);
|
||||
|
||||
const struktur = alsStruktur({ variablen: [], abschnitte: [abschnitt] });
|
||||
expect(struktur.abschnitte[0]?.schritte).toEqual([]);
|
||||
expect(struktur.abschnitte[0]?.bausteinVersionId).toBe("bv-1");
|
||||
// Und das Ergebnis muss durch das Schema gehen – genau das tat es vorher nicht.
|
||||
expect(() => StrukturSchema.parse(struktur)).not.toThrow();
|
||||
});
|
||||
|
||||
it("nimmt den Verweis mit und gibt ihn zurück", () => {
|
||||
const struktur = StrukturSchema.parse({
|
||||
variablen: [],
|
||||
abschnitte: [
|
||||
{ titel: "Eigen", schritte: [{ titel: "Eins" }] },
|
||||
{ titel: "x", bausteinVersionId: "bv-1", schritte: [] },
|
||||
],
|
||||
});
|
||||
const aufbau = alsEditorAufbau(struktur);
|
||||
expect(aufbau.abschnitte[0]?.baustein).toBeUndefined();
|
||||
expect(aufbau.abschnitte[1]?.baustein?.versionId).toBe("bv-1");
|
||||
expect(alsStruktur(aufbau).abschnitte[1]?.bausteinVersionId).toBe("bv-1");
|
||||
});
|
||||
|
||||
it("trägt Name und Fassung nach und benennt einen Verweis, der nicht mehr gilt", () => {
|
||||
const struktur = StrukturSchema.parse({
|
||||
variablen: [],
|
||||
abschnitte: [
|
||||
{ titel: "a", bausteinVersionId: "bv-1", schritte: [] },
|
||||
{ titel: "b", bausteinVersionId: "bv-weg", schritte: [] },
|
||||
],
|
||||
});
|
||||
const aufbau = bausteineAnreichern(alsEditorAufbau(struktur), [BAUSTEIN]);
|
||||
expect(aufbau.abschnitte[0]?.baustein).toEqual({
|
||||
versionId: "bv-1",
|
||||
name: "Datensicherung prüfen",
|
||||
version: 3,
|
||||
});
|
||||
// Ein Verweis, dessen Baustein nicht mehr einsetzbar ist, behält seine
|
||||
// Kennung – verschwinden darf er nicht, sonst wäre er beim nächsten
|
||||
// Speichern still weg.
|
||||
expect(aufbau.abschnitte[1]?.baustein).toEqual({
|
||||
versionId: "bv-weg",
|
||||
name: "(nicht mehr verfügbar)",
|
||||
version: 0,
|
||||
});
|
||||
expect(alsStruktur(aufbau).abschnitte[1]?.bausteinVersionId).toBe("bv-weg");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user