feat(protokoll): Protokoll ansehen, mit Filtern und Mandantensicht (#45)
This commit was merged in pull request #45.
This commit is contained in:
@@ -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"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user