import { beforeEach, describe, expect, it } from "vitest"; import { Betriebsmodus } from "@/generated/prisma/enums"; import { versionMitAufbau, versionsHistorie, vorlageHolen, vorlagenFilterwerte, vorlagenListe, } from "@/server/queries/vorlagen"; import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db"; /** * Mandantensicht auf die Vorlagen (#009). * * Diese Datei gibt es, weil der unabhängige Review zwei Lücken gefunden hat, die * kein Test sehen konnte: der Suchfilter hat die Kundenbedingung überschrieben * (zwei `OR`-Schlüssel im selben Objekt, der zweite gewinnt), und Historie wie * Aufbau einer Version haben überhaupt nicht gefiltert. Beides fiel nicht auf, * weil es zu `src/server/queries/vorlagen.ts` keinen einzigen Test gab. */ async function aufbau() { const eigene = await testPrisma.kunde.create({ data: { name: "Eigene Organisation", kuerzel: "EIGEN", istEigeneOrganisation: true }, }); const meiner = await testPrisma.kunde.create({ data: { name: "Meiner", kuerzel: "MEIN" } }); const fremder = await testPrisma.kunde.create({ data: { name: "Fremder", kuerzel: "FREMD" } }); await testPrisma.einstellung.create({ data: { id: "global", modus: Betriebsmodus.DIENSTLEISTUNG }, }); const techniker = await testPrisma.benutzer.create({ data: { email: "t@firma.example", name: "Techniker", rolle: "TECHNIKER" }, }); await testPrisma.benutzerKunde.create({ data: { benutzerId: techniker.id, kundeId: meiner.id }, }); async function vorlageMit(name: string, kundeId: string | null) { const vorlage = await testPrisma.vorlage.create({ data: { name, kategorie: "Geheim", schlagworte: ["intern"], kundeId }, }); const version = await testPrisma.vorlagenVersion.create({ data: { vorlageId: vorlage.id, version: 1, status: "ENTWURF" }, }); const abschnitt = await testPrisma.vorlagenAbschnitt.create({ data: { versionId: version.id, titel: `Abschnitt von ${name}` }, }); await testPrisma.vorlagenSchritt.create({ data: { abschnittId: abschnitt.id, titel: "Ein Schritt" }, }); await testPrisma.vorlagenVersion.update({ where: { id: version.id }, data: { status: "VEROEFFENTLICHT", veroeffentlichtAm: new Date() }, }); return { vorlage, versionId: version.id }; } return { techniker, admin: { id: "admin", rolle: "ADMIN" as const }, meine: await vorlageMit("Geheimprojekt Meiner", meiner.id), fremde: await vorlageMit("Geheimprojekt Fremder", fremder.id), globale: await vorlageMit("Geheimprojekt Global", null), eigene, }; } describe.runIf(datenbankVerfuegbar)("Mandantensicht auf Vorlagen", () => { beforeEach(async () => { await datenbankLeeren(); process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "lz-kunde-"; }); describe("Liste", () => { it("zeigt einem gebundenen Techniker nur seine und die globalen Vorlagen", async () => { const { techniker } = await aufbau(); const namen = (await vorlagenListe(techniker)).map((z) => z.name).sort(); expect(namen).toEqual(["Geheimprojekt Global", "Geheimprojekt Meiner"]); }); it("lässt die Mandantengrenze auch beim Suchen stehen", async () => { // Genau der Blocker: der Suchfilter brachte ein zweites `OR` mit und // überschrieb die Kundenbedingung — ein Buchstabe im Suchfeld genügte. const { techniker } = await aufbau(); const namen = (await vorlagenListe(techniker, { suche: "Geheim" })).map((z) => z.name).sort(); expect(namen).toEqual(["Geheimprojekt Global", "Geheimprojekt Meiner"]); expect(namen).not.toContain("Geheimprojekt Fremder"); }); it("lässt sie auch bei Kategorie- und Schlagwortfilter stehen", async () => { const { techniker } = await aufbau(); for (const filter of [{ kategorie: "Geheim" }, { schlagwort: "intern" }]) { const namen = (await vorlagenListe(techniker, filter)).map((z) => z.name); expect(namen, JSON.stringify(filter)).not.toContain("Geheimprojekt Fremder"); } }); it("lässt sie auch bei allen Filtern gleichzeitig stehen", async () => { const { techniker } = await aufbau(); const namen = ( await vorlagenListe(techniker, { suche: "Geheim", kategorie: "Geheim", schlagwort: "intern", auchArchivierte: true, }) ).map((z) => z.name); expect(namen).not.toContain("Geheimprojekt Fremder"); }); it("zeigt einem Administrator alles", async () => { const { admin } = await aufbau(); expect(await vorlagenListe(admin, { suche: "Geheim" })).toHaveLength(3); }); it("blendet fremde Kategorien und Schlagworte aus den Filterwerten aus", async () => { const { techniker, fremde } = await aufbau(); await testPrisma.vorlage.update({ where: { id: fremde.vorlage.id }, data: { kategorie: "Nur-fuer-Fremde", schlagworte: ["geheimwort"] }, }); const werte = await vorlagenFilterwerte(techniker); expect(werte.kategorien).not.toContain("Nur-fuer-Fremde"); expect(werte.schlagworte).not.toContain("geheimwort"); }); }); describe("Historie und Aufbau", () => { it("zeigt die Historie einer fremden Vorlage nicht", async () => { const { techniker, fremde, meine } = await aufbau(); expect(await versionsHistorie(fremde.vorlage.id, techniker)).toEqual([]); expect(await versionsHistorie(meine.vorlage.id, techniker)).toHaveLength(1); }); it("gibt den Aufbau einer fremden Version nicht heraus", async () => { // Sonst stünden Abschnittstitel, Schritte und Befehle eines fremden Kunden // offen — die Vorlage ist der innere Ablauf seines Betriebs. const { techniker, fremde, meine, globale } = await aufbau(); expect(await versionMitAufbau(fremde.versionId, techniker)).toBeUndefined(); expect(await versionMitAufbau(meine.versionId, techniker)).toBeDefined(); expect(await versionMitAufbau(globale.versionId, techniker)).toBeDefined(); }); it("gibt einem Administrator alles heraus", async () => { const { admin, fremde } = await aufbau(); expect(await versionMitAufbau(fremde.versionId, admin)).toBeDefined(); }); it("schränkt ohne gesetztes Präfix niemanden ein", async () => { delete process.env["OIDC_CUSTOMER_GROUP_PREFIX"]; const { techniker, fremde } = await aufbau(); expect(await versionMitAufbau(fremde.versionId, techniker)).toBeDefined(); expect(await vorlagenListe(techniker)).toHaveLength(3); }); }); describe("Einzelne Vorlage", () => { it("gibt eine fremde Vorlage nicht heraus", async () => { // Die Vorlagenseite haengt allein an dieser Funktion. Gaebe sie die Zeile // heraus, staenden Name, Beschreibung und Versionsgeschichte offen – die // Kennung steht in jedem Link, den jemand weitergibt. const { techniker, fremde, meine, globale } = await aufbau(); expect(await vorlageHolen(fremde.vorlage.id, techniker)).toBeUndefined(); expect(await vorlageHolen(meine.vorlage.id, techniker)).toBeDefined(); expect(await vorlageHolen(globale.vorlage.id, techniker)).toBeDefined(); }); it("gibt einem Administrator auch eine fremde heraus", async () => { const { admin, fremde } = await aufbau(); expect(await vorlageHolen(fremde.vorlage.id, admin)).toBeDefined(); }); it("liefert dieselben Felder wie die Liste", async () => { // Zwei Lesemodelle fuer dieselbe Zeile laufen auseinander; deshalb bauen // beide aus derselben Auswahl. Der Test haelt das fest. const { techniker, meine } = await aufbau(); const ausListe = (await vorlagenListe(techniker, { auchArchivierte: true })).find( (zeile) => zeile.id === meine.vorlage.id, ); expect(await vorlageHolen(meine.vorlage.id, techniker)).toEqual(ausListe); }); it("findet auch eine archivierte Vorlage", async () => { // Die Seite muss sie zeigen koennen, sonst liesse sich nichts freigeben. const { techniker, meine } = await aufbau(); await testPrisma.vorlage.update({ where: { id: meine.vorlage.id }, data: { archiviert: true }, }); const zeile = await vorlageHolen(meine.vorlage.id, techniker); expect(zeile?.archiviert).toBe(true); }); }); });