222 lines
7.5 KiB
TypeScript
222 lines
7.5 KiB
TypeScript
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);
|
|
});
|
|
});
|