feat(protokoll): Protokoll ansehen, mit Filtern und Mandantensicht (#45)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

This commit was merged in pull request #45.
This commit is contained in:
2026-10-04 16:06:26 +00:00
parent f5936327a0
commit e1f244e4e8
23 changed files with 1103 additions and 22 deletions
+305
View File
@@ -0,0 +1,305 @@
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"]);
});
});