import { PrismaClient } from "@prisma/client"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { auskunftSammeln, auskunftUmfang } from "@/lib/dsgvo/auskunft"; import type { MandantenClient } from "@/lib/db"; /** * Die Auskunft nach Art. 15 DSGVO. * * Zwei Dinge müssen stimmen, und sie ziehen in verschiedene Richtungen. * * **Vollständigkeit.** Was über die Person gespeichert ist, muss drinstehen — * auch das Unangenehme. Eine Auskunft, die den gemahnten Beitrag oder die * nicht bestandene G26 auslässt, ist keine. * * **Begrenzung.** Was die Person *getan* hat, gehört zum Nachweisbestand der * Wehr. Stünde es vollständig drin, käme jeder über eine Selbstauskunft an * die Geräteakten — und die Auskunftspflicht wäre eine Hintertür. * * Der Test prüft deshalb beides: dass die Abschnitte da sind, und dass die * Tätigkeiten nur gezählt werden. * * Braucht eine laufende Datenbank; ohne sie überspringt er sich selbst. */ const SYSTEM_URL = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL ?? ""; const system = new PrismaClient({ datasources: { db: { url: SYSTEM_URL } } }); const SLUG = "test-auskunft-wehr"; let erreichbar = false; let organisationId = ""; let personId = ""; beforeAll(async () => { try { await system.$queryRaw`SELECT 1`; erreichbar = true; } catch { return; } const organisation = await system.organisation.upsert({ where: { slug: SLUG }, update: {}, create: { slug: SLUG, name: "Auskunfts-Testwehr", kurzname: "ATW" }, }); organisationId = organisation.id; const person = await system.person.upsert({ where: { organisationId_mitgliedsnummer: { organisationId, mitgliedsnummer: "A-1" } }, update: {}, create: { organisationId, vorname: "Auskunft", nachname: "Suchend", mitgliedsnummer: "A-1", geburtsdatum: new Date("1985-04-12"), eintrittsdatum: new Date("2010-01-01"), email: "auskunft@test-auskunft.de", strasse: "Musterweg 1", plz: "91315", ort: "Musterhausen", abteilung: "Aktive", }, }); personId = person.id; // Gesundheitsdaten — die besondere Kategorie, um die es geht. await system.g26Untersuchung.create({ data: { organisationId, personId, untersuchtAm: new Date("2025-03-01"), gueltigBis: new Date("2028-03-01"), ergebnis: "TAUGLICH", arzt: "Dr. Beispiel", }, }); // Ein gemahnter Beitrag — das Unangenehme, das drinstehen muss. await system.beitrag.create({ data: { organisationId, personId, jahr: 2025, betrag: "25.00", stand: "GEMAHNT", mahnstufe: 2, }, }); }); afterAll(async () => { if (erreichbar) { await system.$transaction(async (tx) => { await tx.$executeRaw`SELECT set_config('app.audit_bereinigung', 'an', true)`; await tx.organisation.deleteMany({ where: { slug: SLUG } }); }); } await system.$disconnect(); }); describe.runIf(process.env.DATABASE_URL)("Auskunft nach Art. 15", () => { it("nennt Person, Wehr und Zeitpunkt", async ({ skip }) => { if (!erreichbar) return skip(); const auskunft = await auskunftSammeln(system as unknown as MandantenClient, personId); expect(auskunft.person.name).toBe("Auskunft Suchend"); expect(auskunft.person.mitgliedsnummer).toBe("A-1"); expect(auskunft.wehr.name).toBe("Auskunfts-Testwehr"); expect(auskunft.erstelltAm).toBeInstanceOf(Date); }); it("gibt zu jedem Abschnitt an, wozu die Daten vorliegen", async ({ skip }) => { if (!erreichbar) return skip(); // Art. 15 Abs. 1 verlangt nicht nur die Daten, sondern auch die Zwecke. // Eine Liste ohne sie sieht aus wie eine Auskunft und ist keine. const auskunft = await auskunftSammeln(system as unknown as MandantenClient, personId); expect(auskunft.abschnitte.length).toBeGreaterThan(5); for (const abschnitt of auskunft.abschnitte) { expect(abschnitt.titel.length).toBeGreaterThan(0); expect(abschnitt.zweck.length).toBeGreaterThan(20); } }); it("enthält die Stammdaten vollständig", async ({ skip }) => { if (!erreichbar) return skip(); const auskunft = await auskunftSammeln(system as unknown as MandantenClient, personId); const stammdaten = auskunft.abschnitte.find((eintrag) => eintrag.titel === "Stammdaten"); expect(stammdaten?.eintraege[0]).toMatchObject({ Vorname: "Auskunft", Nachname: "Suchend", Geburtsdatum: "1985-04-12", Anschrift: "Musterweg 1, 91315 Musterhausen", }); }); it("kennzeichnet Gesundheitsdaten als besondere Kategorie", async ({ skip }) => { if (!erreichbar) return skip(); const auskunft = await auskunftSammeln(system as unknown as MandantenClient, personId); const g26 = auskunft.abschnitte.find((eintrag) => eintrag.titel.includes("G 26")); expect(g26?.besondereKategorie).toBe(true); expect(g26?.eintraege).toHaveLength(1); expect(g26?.eintraege[0]?.Ergebnis).toBe("TAUGLICH"); }); it("lässt das Unangenehme nicht weg", async ({ skip }) => { if (!erreichbar) return skip(); // Ein gemahnter Beitrag ist ein Datum über diese Person. Eine Auskunft, // die auswählt, was gut aussieht, erfüllt die Pflicht nicht. const auskunft = await auskunftSammeln(system as unknown as MandantenClient, personId); const beitraege = auskunft.abschnitte.find((eintrag) => eintrag.titel.includes("Mitgliedsbeiträge"), ); expect(beitraege?.eintraege[0]).toMatchObject({ Stand: "GEMAHNT", Mahnstufe: "2" }); }); it("behält leere Abschnitte — auch das ist eine Auskunft", async ({ skip }) => { if (!erreichbar) return skip(); // Diese Person war in keinem Einsatz. Der Abschnitt muss trotzdem // dastehen: ein weggelassener wirft die Frage auf, ob da doch etwas ist. const auskunft = await auskunftSammeln(system as unknown as MandantenClient, personId); const einsaetze = auskunft.abschnitte.find((eintrag) => eintrag.titel.includes("Einsatzbeteiligung"), ); expect(einsaetze).toBeDefined(); expect(einsaetze?.eintraege).toHaveLength(0); }); it("zählt Tätigkeiten, statt die Unterlagen mitzuliefern", async ({ skip }) => { if (!erreichbar) return skip(); // Die Grenze zwischen „über mich" und „von mir". Stünden hier die // Prüfprotokolle im Volltext, käme jeder über eine Selbstauskunft an die // Geräteakten der Wehr. const auskunft = await auskunftSammeln(system as unknown as MandantenClient, personId); expect(auskunft.taetigkeiten.length).toBeGreaterThan(0); for (const eintrag of auskunft.taetigkeiten) { expect(typeof eintrag.anzahl).toBe("number"); expect(eintrag).not.toHaveProperty("eintraege"); } }); it("zählt den Umfang über alle Abschnitte", async ({ skip }) => { if (!erreichbar) return skip(); const auskunft = await auskunftSammeln(system as unknown as MandantenClient, personId); const gezaehlt = auskunft.abschnitte.reduce( (summe, abschnitt) => summe + abschnitt.eintraege.length, 0, ); expect(auskunftUmfang(auskunft)).toBe(gezaehlt); expect(auskunftUmfang(auskunft)).toBeGreaterThan(2); }); it("weist eine unbekannte Person ab, statt eine leere Auskunft zu bauen", async ({ skip }) => { if (!erreichbar) return skip(); // Sonst bekäme jemand ein Schreiben mit lauter leeren Abschnitten und // dem Eindruck, es lägen keine Daten vor. await expect( auskunftSammeln( system as unknown as MandantenClient, "00000000-0000-0000-0000-000000000000", ), ).rejects.toThrow(/gibt es nicht/i); }); });