Files
Laufzettel/tests/protokoll.test.ts
florian.wilde e1f244e4e8
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(protokoll): Protokoll ansehen, mit Filtern und Mandantensicht (#45)
2026-10-04 16:06:26 +00:00

306 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { Betriebsmodus, VersionStatus } from "@/generated/prisma/enums";
import { RECHTE } from "@/server/auth/rollen";
import { protokollDesLaufs, protokollSeite, protokollUrheber } from "@/server/queries/protokoll";
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
/**
* Das Protokoll ansehen (#023).
*
* Drei Fragen tragen das Ganze: sieht jemand nur seine Kunden, kommt ein
* Techniker ueberhaupt heran, und steht irgendwo ein erzeugtes Passwort.
*/
/**
* Ein auf Kunden beschraenkter Benutzer entsteht nur unter drei Bedingungen —
* Modus DIENSTLEISTUNG, Rolle unter REDAKTEUR, und ein gesetztes
* `OIDC_CUSTOMER_GROUP_PREFIX`. Ohne die dritte liefert `sichtbareKundenIds`
* `undefined`, und jeder Filtertest waere ein Test ohne Gegenstand.
*/
const VORHER = process.env["OIDC_CUSTOMER_GROUP_PREFIX"];
async function aufbau() {
const eigene = await testPrisma.kunde.create({
data: { name: "Eigene Organisation", kuerzel: "EIGEN", istEigeneOrganisation: true },
});
const fremd = await testPrisma.kunde.create({ data: { name: "Fremd GmbH", kuerzel: "FREMD" } });
await testPrisma.einstellung.create({
data: { id: "global", modus: Betriebsmodus.DIENSTLEISTUNG },
});
const admin = await testPrisma.benutzer.create({
data: { email: "admin@firma.example", name: "Admin", rolle: "ADMIN" },
});
const techniker = await testPrisma.benutzer.create({
data: { email: "tech@firma.example", name: "Techniker", rolle: "TECHNIKER" },
});
// Nur an die eigene Organisation gebunden – „Fremd GmbH“ ist für ihn unsichtbar.
await testPrisma.benutzerKunde.create({
data: { benutzerId: techniker.id, kundeId: eigene.id },
});
return {
eigene,
fremd,
admin: { id: admin.id, rolle: admin.rolle },
techniker: { id: techniker.id, rolle: techniker.rolle },
};
}
describe("Recht auf das Protokoll", () => {
it("liegt bei verwaltung, also nur bei ADMIN", () => {
// Das Protokoll sagt, wer was getan hat, und ist damit selbst eine Auskunft
// über Menschen. TECHNIKER und LESER kommen nicht auf die Seite.
expect(RECHTE.verwaltung).toEqual(["ADMIN"]);
expect(RECHTE.verwaltung).not.toContain("TECHNIKER");
expect(RECHTE.verwaltung).not.toContain("LESER");
});
});
describe.runIf(datenbankVerfuegbar)("Protokoll ansehen", () => {
beforeEach(async () => {
await datenbankLeeren();
process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "kunde-";
});
afterEach(() => {
if (VORHER === undefined) delete process.env["OIDC_CUSTOMER_GROUP_PREFIX"];
else process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = VORHER;
});
it("zeigt einem beschränkten Benutzer keinen Eintrag eines fremden Kunden", async () => {
const { eigene, fremd, admin, techniker } = await aufbau();
await testPrisma.protokoll.createMany({
data: [
{
kundeId: eigene.id,
entitaet: "Geraet",
entitaetId: "g-eigen",
aktion: "Gerät angelegt (eigen)",
benutzerId: admin.id,
},
{
kundeId: fremd.id,
entitaet: "Geraet",
entitaetId: "g-fremd",
aktion: "Gerät angelegt (fremd)",
benutzerId: admin.id,
},
],
});
const fuerTechniker = await protokollSeite({}, techniker);
expect(fuerTechniker.zeilen.map((zeile) => zeile.aktion)).toEqual(["Gerät angelegt (eigen)"]);
expect(fuerTechniker.gesamt).toBe(1);
// Der ADMIN sieht beide – er ist auf keinen Kunden beschränkt.
const fuerAdmin = await protokollSeite({}, admin);
expect(fuerAdmin.gesamt).toBe(2);
});
it("zeigt globale Einträge auch einem beschränkten Benutzer", async () => {
const { admin, techniker } = await aufbau();
await testPrisma.protokoll.create({
data: {
entitaet: "Einstellung",
entitaetId: "global",
aktion: "Betriebsmodus gewechselt",
benutzerId: admin.id,
},
});
const ergebnis = await protokollSeite({}, techniker);
expect(ergebnis.zeilen.map((zeile) => zeile.aktion)).toEqual(["Betriebsmodus gewechselt"]);
});
it("verbirgt einen nicht zuordenbaren Eintrag vor einem beschränkten Benutzer", async () => {
// Ein Eintrag zu einem inzwischen gelöschten Gerät: ohne `kundeId` und ohne
// Lauf lässt er sich keinem Kunden zuordnen. `vorher`/`nachher` können einen
// Hostnamen enthalten, deshalb wird im Zweifel zugemacht (#023).
const { admin, techniker } = await aufbau();
await testPrisma.protokoll.create({
data: {
entitaet: "Geraet",
entitaetId: "laengst-geloescht",
aktion: "Gerät gelöscht",
benutzerId: admin.id,
vorher: { hostname: "FREMD-PC-007" },
},
});
expect((await protokollSeite({}, techniker)).gesamt).toBe(0);
expect((await protokollSeite({}, admin)).gesamt).toBe(1);
});
it("ordnet einen laufbezogenen Eintrag über den Lauf zu, auch ohne eigene kundeId", async () => {
const { fremd, admin, techniker } = await aufbau();
const vorlage = await testPrisma.vorlage.create({ data: { name: "Irgendeine" } });
const version = await testPrisma.vorlagenVersion.create({
data: { vorlageId: vorlage.id, version: 1, status: VersionStatus.ENTWURF },
});
await testPrisma.vorlagenVersion.update({
where: { id: version.id },
data: { status: VersionStatus.VEROEFFENTLICHT, veroeffentlichtAm: new Date() },
});
const lauf = await testPrisma.lauf.create({
data: { vorlagenVersionId: version.id, titel: "Lauf bei Fremd", kundeId: fremd.id },
});
await testPrisma.protokoll.create({
data: {
laufId: lauf.id,
entitaet: "Lauf",
aktion: "Lauf angelegt",
entitaetId: lauf.id,
benutzerId: admin.id,
},
});
expect((await protokollSeite({}, techniker)).gesamt).toBe(0);
const fuerAdmin = await protokollSeite({}, admin);
expect(fuerAdmin.zeilen[0]?.laufTitel).toBe("Lauf bei Fremd");
expect(fuerAdmin.zeilen[0]?.kundeName).toBe("Fremd GmbH");
});
it("nennt einen Eintrag ohne Urheber als Systemlauf, nicht als Lücke", async () => {
const { eigene, admin } = await aufbau();
await testPrisma.protokoll.create({
data: {
kundeId: eigene.id,
entitaet: "Verzeichnisquelle",
entitaetId: "q-1",
aktion: "Verzeichnis abgeglichen",
benutzerId: null,
},
});
const ergebnis = await protokollSeite({}, admin);
expect(ergebnis.zeilen[0]?.urheber).toBeNull();
});
it("filtert nach Urheber, Zeitraum und Volltext", async () => {
const { eigene, admin, techniker } = await aufbau();
await testPrisma.protokoll.createMany({
data: [
{
kundeId: eigene.id,
entitaet: "Benutzer",
entitaetId: "b-1",
aktion: "Rolle geändert",
benutzerId: admin.id,
erstelltAm: new Date("2026-09-01T10:00:00Z"),
},
{
kundeId: eigene.id,
entitaet: "Geraet",
entitaetId: "g-1",
aktion: "Gerät archiviert",
benutzerId: techniker.id,
erstelltAm: new Date("2026-10-04T10:00:00Z"),
},
],
});
expect((await protokollSeite({ suche: "rolle" }, admin)).gesamt).toBe(1);
expect((await protokollSeite({ urheberId: techniker.id }, admin)).gesamt).toBe(1);
expect((await protokollSeite({ vonDatum: "2026-10-01" }, admin)).gesamt).toBe(1);
// Einschliesslich des bis-Tages – sonst fehlt der Eintrag von heute.
expect((await protokollSeite({ bisDatum: "2026-10-04" }, admin)).gesamt).toBe(2);
expect((await protokollSeite({ vonDatum: "2026-09-01", bisDatum: "2026-09-01" }, admin)).gesamt).toBe(1);
});
it("blättert und zählt richtig", async () => {
const { eigene, admin } = await aufbau();
await testPrisma.protokoll.createMany({
data: Array.from({ length: 55 }, (_, nummer) => ({
kundeId: eigene.id,
entitaet: "Geraet",
entitaetId: `g-${nummer}`,
aktion: `Eintrag ${String(nummer).padStart(2, "0")}`,
benutzerId: admin.id,
erstelltAm: new Date(Date.UTC(2026, 9, 1, 0, nummer)),
})),
});
const erste = await protokollSeite({}, admin);
expect(erste.gesamt).toBe(55);
expect(erste.seiten).toBe(2);
expect(erste.zeilen).toHaveLength(50);
// Neueste zuerst.
expect(erste.zeilen[0]?.aktion).toBe("Eintrag 54");
const zweite = await protokollSeite({ seite: 2 }, admin);
expect(zweite.zeilen).toHaveLength(5);
expect(zweite.zeilen.at(-1)?.aktion).toBe("Eintrag 00");
});
it("nennt als Urheber nur die, die auch im sichtbaren Teil vorkommen", async () => {
const { eigene, fremd, admin, techniker } = await aufbau();
await testPrisma.protokoll.createMany({
data: [
{
kundeId: eigene.id,
entitaet: "Geraet",
entitaetId: "g-1",
aktion: "eigen",
benutzerId: techniker.id,
},
{
kundeId: fremd.id,
entitaet: "Geraet",
entitaetId: "g-2",
aktion: "fremd",
benutzerId: admin.id,
},
],
});
expect((await protokollUrheber(techniker)).map((eintrag) => eintrag.name)).toEqual(["Techniker"]);
expect((await protokollUrheber(admin)).map((eintrag) => eintrag.name).sort()).toEqual([
"Admin",
"Techniker",
]);
});
it("gibt am Lauf nur dessen Einträge, neueste zuerst", async () => {
const { eigene, admin } = await aufbau();
const vorlage = await testPrisma.vorlage.create({ data: { name: "Irgendeine" } });
const version = await testPrisma.vorlagenVersion.create({
data: { vorlageId: vorlage.id, version: 1, status: VersionStatus.ENTWURF },
});
const lauf = await testPrisma.lauf.create({
data: { vorlagenVersionId: version.id, titel: "Der Lauf", kundeId: eigene.id },
});
const anderer = await testPrisma.lauf.create({
data: { vorlagenVersionId: version.id, titel: "Ein anderer", kundeId: eigene.id },
});
await testPrisma.protokoll.createMany({
data: [
{
laufId: lauf.id,
entitaet: "Lauf",
entitaetId: lauf.id,
aktion: "zuerst",
benutzerId: admin.id,
erstelltAm: new Date("2026-10-01T10:00:00Z"),
},
{
laufId: lauf.id,
entitaet: "Lauf",
entitaetId: lauf.id,
aktion: "danach",
benutzerId: admin.id,
erstelltAm: new Date("2026-10-02T10:00:00Z"),
},
{
laufId: anderer.id,
entitaet: "Lauf",
entitaetId: anderer.id,
aktion: "fremder Lauf",
benutzerId: admin.id,
},
],
});
const eintraege = await protokollDesLaufs(lauf.id);
expect(eintraege.map((eintrag) => eintrag.aktion)).toEqual(["danach", "zuerst"]);
});
});