feat(seed): mitgelieferte Vorlagen am Schluessel erkennen
Prüfung / pruefung (push) Canceled after 0s
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s

Der Seed erkannte seine sieben Vorlagen am Namen. Wer eine umbenannte, bekam
beim naechsten Lauf eine zweite -- und der Schnellstart stand auf der
Geraeteseite doppelt.

Vorlage.seedSchluessel traegt die Kennung, eindeutig wo gesetzt (partieller
Index, weil eigene Vorlagen keinen tragen). Das Austauschformat kennt das Feld
als seedSchluessel; nicht als schluessel, wie das Issue plante, denn so heisst
dort schon das Feld einer Variablen.

Der Rueckfall auf den Namen bleibt, sonst bekaeme eine Installation von vor
diesem Feld beim ersten Lauf danach genau die Dubletten, die es verhindern
soll. Wird sie so gefunden, traegt der Seed den Schluessel nach -- an der
Entwicklungsdatenbank gefahren: sieben nachgetragen, nichts neu angelegt.

Der Import setzt den Schluessel nicht, auch nicht wenn das Dokument ihn
mitbringt, und der Export schreibt ihn nicht: eine eingespielte Kopie soll
nicht den Platz des Originals einnehmen.

Gegenprobe gefahren: ohne die Suche am Schluessel werden vier Tests rot.

Closes #32
This commit is contained in:
Florian Wilde
2026-10-04 16:48:30 +02:00
parent 88d7db8c3d
commit 7eb81bd191
20 changed files with 316 additions and 36 deletions
+5 -1
View File
@@ -62,7 +62,11 @@ describe.runIf(datenbankVerfuegbar)("Handgeschriebene Datenbankobjekte", () => {
await namen(
`SELECT indexname AS name FROM pg_indexes WHERE schemaname='public' AND indexdef LIKE '%WHERE%'`,
),
).toEqual(["Kunde_eigene_organisation_einmalig", "VorlagenVersion_ein_entwurf"]);
).toEqual([
"Kunde_eigene_organisation_einmalig",
"Vorlage_seedSchluessel_einmalig",
"VorlagenVersion_ein_entwurf",
]);
});
it("hat alle Prüfbedingungen", async () => {
+21
View File
@@ -321,6 +321,27 @@ describe.runIf(datenbankVerfuegbar)("Datenmodell: Regeln in der Datenbank", () =
});
});
describe("Vorlage", () => {
it("haelt den Seed-Schluessel eindeutig, wo er gesetzt ist", async () => {
await testPrisma.vorlage.create({
data: { name: "PC Neuinstallation", seedSchluessel: "pc-neuinstallation" },
});
await expect(
testPrisma.vorlage.create({
data: { name: "Anders benannt", seedSchluessel: "pc-neuinstallation" },
}),
).rejects.toThrow(/Vorlage_seedSchluessel_einmalig/);
});
it("laesst beliebig viele Vorlagen ohne Seed-Schluessel zu", async () => {
// Der Index ist partiell, sonst waere die zweite eigene Vorlage eine
// Dublette von null (#021).
await testPrisma.vorlage.create({ data: { name: "Eigene A" } });
await testPrisma.vorlage.create({ data: { name: "Eigene B" } });
expect(await testPrisma.vorlage.count({ where: { seedSchluessel: null } })).toBe(2);
});
});
describe("Vorlagen-Version", () => {
it("haelt die Versionsnummer je Vorlage eindeutig", async () => {
const vorlage = await testPrisma.vorlage.create({ data: { name: "PC Neuinstallation" } });
+53
View File
@@ -342,6 +342,59 @@ describe.runIf(datenbankVerfuegbar)("Seed gegen die Datenbank", () => {
expect(await testPrisma.vorlagenVersion.count({ where: { vorlageId: erste.vorlageId } })).toBe(2);
});
it("erkennt eine umbenannte Vorlage am Schluessel und legt keine zweite an", async () => {
// Der Fall, fuer den das Feld da ist (#021). Vorher erkannte der Seed seine
// Vorlagen am Namen: wer eine umbenannte, bekam beim naechsten Lauf eine
// zweite - und der Schnellstart stand auf der Geraeteseite doppelt.
const [erste] = await vorlagenSaeen(testPrisma);
if (!erste) throw new Error("Der Seed hat keine Vorlage angelegt.");
await testPrisma.vorlage.update({
where: { id: erste.vorlageId },
data: { name: "Von Hand umbenannt" },
});
const zweit = await vorlagenSaeen(testPrisma);
expect(zweit.every((eintrag) => !eintrag.neu)).toBe(true);
expect(await testPrisma.vorlage.count()).toBe(7);
// Und der Name bleibt der, den jemand gewaehlt hat.
const nachher = await testPrisma.vorlage.findUniqueOrThrow({ where: { id: erste.vorlageId } });
expect(nachher.name).toBe("Von Hand umbenannt");
});
it("legt zu einer umbenannten Vorlage keine zweite Schnellstart-Zeile an", async () => {
const gesaet = await vorlagenSaeen(testPrisma);
await typVorgabenSaeen(testPrisma, gesaet);
const vorher = await testPrisma.geraetetypVorlage.count();
// „PC Neuinstallation“ steht im Schnellstart fuer PC und NOTEBOOK - genau die
// Zeilen, die sich vorher verdoppelt haben.
const pc = gesaet.find((eintrag) => eintrag.name === "PC Neuinstallation");
if (!pc) throw new Error("Die PC-Vorlage fehlt im Seed.");
await testPrisma.vorlage.update({
where: { id: pc.vorlageId },
data: { name: "Arbeitsplatz neu aufsetzen" },
});
const nochmal = await vorlagenSaeen(testPrisma);
expect(await typVorgabenSaeen(testPrisma, nochmal)).toBe(0);
expect(await testPrisma.geraetetypVorlage.count()).toBe(vorher);
});
it("traegt den Schluessel bei einer Installation nach, die ihn noch nicht hat", async () => {
// Eine Installation von vor diesem Feld: die Zeilen stehen, der Schluessel
// fehlt. Ohne den Rueckfall auf den Namen bekaeme sie beim ersten Lauf
// danach genau die Dubletten, die das Feld verhindern soll.
const gesaet = await vorlagenSaeen(testPrisma);
await testPrisma.vorlage.updateMany({ data: { seedSchluessel: null } });
expect(await testPrisma.vorlage.count({ where: { seedSchluessel: null } })).toBe(7);
const zweit = await vorlagenSaeen(testPrisma);
expect(zweit.every((eintrag) => !eintrag.neu)).toBe(true);
expect(await testPrisma.vorlage.count()).toBe(7);
expect(await testPrisma.vorlage.count({ where: { seedSchluessel: null } })).toBe(0);
expect(zweit.map((e) => e.vorlageId).sort()).toEqual(gesaet.map((e) => e.vorlageId).sort());
});
it("verknuepft die Schnellstart-Vorgaben mit den richtigen Vorlagen", async () => {
const gesaet = await vorlagenSaeen(testPrisma);
const angelegt = await typVorgabenSaeen(testPrisma, gesaet);
+31
View File
@@ -354,6 +354,24 @@ describe.runIf(datenbankVerfuegbar)("Vorlagen-Austausch", () => {
expect(await testPrisma.vorlagenVersion.count({ where: { vorlageId } })).toBe(1);
});
it("gibt einer eingespielten Kopie keinen Seed-Schluessel", async () => {
// Sonst hielte der Seed die Kopie fuer das Original und legte das Original
// ein zweites Mal an (#021). Geprueft wird mit einem Dokument, das den
// Schluessel ausdruecklich mitbringt - ein Angreifer oder ein Versehen
// koennte ihn ja hineinschreiben.
const redakteur = await redakteurAnlegen();
const quelle = await beispielVersionAnlegen();
const austausch = await vorlageExportieren(quelle);
const { vorlageId } = await vorlageImportieren(
{ ...austausch, seedSchluessel: "pc-neuinstallation" },
redakteur.id,
);
const kopie = await testPrisma.vorlage.findUniqueOrThrow({ where: { id: vorlageId } });
expect(kopie.seedSchluessel).toBeNull();
});
it("leitet die Positionen aus der Reihenfolge der Listen ab", async () => {
const redakteur = await redakteurAnlegen();
const { versionId } = await vorlageImportieren(
@@ -437,6 +455,19 @@ describe.runIf(datenbankVerfuegbar)("Vorlagen-Austausch", () => {
});
});
describe("Seed-Schluessel", () => {
it("steht in keinem Export – auch nicht bei einer mitgelieferten Vorlage", async () => {
// Ein Feld, das beim Einlesen verfaellt, gehoert nicht in die Datei: wer es
// dort sieht, haelt es fuer uebertragbar.
const quelle = await beispielVersionAnlegen();
await testPrisma.vorlage.updateMany({ data: { seedSchluessel: "pc-neuinstallation" } });
const austausch = await vorlageExportieren(quelle);
expect(austausch).not.toHaveProperty("seedSchluessel");
expect(JSON.stringify(austausch)).not.toContain("pc-neuinstallation");
});
});
describe("Rundlauf", () => {
it("ergibt ein inhaltsgleiches Dokument – bis auf die Version", async () => {
const redakteur = await redakteurAnlegen();