Files
ProxLedger/tests/kundensicht.test.ts
T
florian.wildeandClaude Opus 5 2f45e33e20 feat(auth): Rollen, Benutzerverwaltung und Passkeys (#027, #028)
Drei Rollen: Administrator, Techniker, Kunde. Jede der 28 Aktionen nennt das
Recht, das sie verlangt; die Rechte stehen an einer Stelle. Kundenkonten
haengen an genau einem Kunden und sehen nur dessen Hosts und Rechnungen -
ohne Einkaufspreise und Marge. Die Einschraenkung sitzt in den Abfragen
selbst, damit keine Seite vergessen kann, sie weiterzugeben; sechs Tests
pruefen das.

Dabei gefunden: Neu ueber OIDC angelegte Konten haetten die Vorgabe des
Datenmodells geerbt - ADMIN. Jetzt gibt es dafuer eine Einstellung mit der
Vorgabe Techniker.

Passkeys (#028) als gleichwertiger, passwortloser Anmeldeweg neben Passwort
und OIDC. Mehrere je Konto, Aufgabe in einem kurzlebigen Cookie, rpID aus der
oeffentlichen Adresse. Ohne HTTPS sagt die Seite es, statt still zu scheitern.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 00:46:29 +02:00

146 lines
5.0 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 { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
/**
* Prüft, dass ein Kundenkonto wirklich nur die eigenen Daten sieht – und keine Einkaufspreise.
* Die Anmeldung wird ersetzt, weil in einem Test keine Cookies existieren.
*/
let sichtbar: string | null = null;
vi.mock("@/server/auth/sitzungspflicht", () => ({
sichtbarerKunde: async () => sichtbar,
darfEinkaufSehen: async () => sichtbar === null,
angemeldet: async () => null,
sitzungErforderlich: async () => {
throw new Error("in diesem Test nicht vorgesehen");
},
}));
const { hostListe } = await import("@/server/queries/hostliste");
const { rechnungsListe } = await import("@/server/queries/rechnungen");
const { kennzahlen } = await import("@/server/queries/hosts");
const { kundenListe } = await import("@/server/queries/kunden");
async function daten() {
const [eigener, fremder] = await Promise.all([
testPrisma.kunde.create({ data: { name: "Eigen GmbH", kundennummer: "K-1" } }),
testPrisma.kunde.create({ data: { name: "Fremd AG", kundennummer: "K-2" } }),
]);
const [hostA, hostB] = await Promise.all([
testPrisma.host.create({
data: { agentId: "a-1", hostname: "pve-eigen", produkt: "PVE", kundeId: eigener.id, subStatus: "ACTIVE" },
}),
testPrisma.host.create({
data: { agentId: "a-2", hostname: "pve-fremd", produkt: "PVE", kundeId: fremder.id, subStatus: "ACTIVE" },
}),
]);
// Je eine Laufzeit mit Einkauf (Proxmox) und Verkauf (Kunde).
for (const [host, kunde, nummer] of [
[hostA, eigener, "PX-1"],
[hostB, fremder, "PX-2"],
] as const) {
const laufzeit = await testPrisma.laufzeit.create({
data: {
hostId: host.id,
key: `pve1c-${host.hostname}`,
start: new Date("2026-01-01"),
ende: new Date("2026-12-31"),
status: "BESTAETIGT",
},
});
const rechnung = await testPrisma.proxmoxRechnung.create({
data: { nummer, datum: new Date("2026-01-02"), nettoBetrag: 100, ustSatz: 19, bezahlt: true },
});
// Die Verbindung liegt auf der Laufzeit, nicht auf der Position.
const position = await testPrisma.proxmoxRechnungPosition.create({
data: { rechnungId: rechnung.id, bezeichnung: "Position", nettoBetrag: 100 },
});
await testPrisma.laufzeit.update({
where: { id: laufzeit.id },
data: { proxmoxPosition: { connect: { id: position.id } } },
});
const kundenrechnung = await testPrisma.kundenrechnung.create({
data: { kundeId: kunde.id, nummer: `KR-${nummer}`, ustSatz: 19, erstellt: true, bezahlt: false },
});
const kundenPosition = await testPrisma.kundenrechnungPosition.create({
data: { rechnungId: kundenrechnung.id, bezeichnung: "Position", nettoBetrag: 130 },
});
await testPrisma.laufzeit.update({
where: { id: laufzeit.id },
data: { kundenPosition: { connect: { id: kundenPosition.id } } },
});
}
return { eigener, fremder };
}
describe.runIf(datenbankVerfuegbar)("Sicht eines Kundenkontos", () => {
beforeEach(async () => {
await datenbankLeeren();
sichtbar = null;
});
afterAll(async () => {
await testPrisma.$disconnect();
});
it("zeigt ohne Einschränkung alle Hosts", async () => {
await daten();
const liste = await hostListe({});
expect(liste.gesamt).toBe(2);
});
it("zeigt einem Kundenkonto nur die eigenen Hosts", async () => {
const { eigener } = await daten();
sichtbar = eigener.id;
const liste = await hostListe({});
expect(liste.gesamt).toBe(1);
expect(liste.hosts[0].hostname).toBe("pve-eigen");
});
it("verbirgt Einkaufspreis und Marge vor dem Kundenkonto", async () => {
const { eigener } = await daten();
const alles = await rechnungsListe({});
expect(alles.zeilen.every((z) => z.einkaufBetrag === 100)).toBe(true);
expect(alles.summen.marge).toBe(60);
sichtbar = eigener.id;
const eingeschraenkt = await rechnungsListe({});
expect(eingeschraenkt.zeilen).toHaveLength(1);
expect(eingeschraenkt.zeilen[0].einkaufBetrag).toBeNull();
expect(eingeschraenkt.zeilen[0].einkaufNummer).toBeNull();
expect(eingeschraenkt.zeilen[0].marge).toBeNull();
expect(eingeschraenkt.zeilen[0].verkaufBetrag).toBe(130);
});
it("zählt für das Kundenkonto nur die eigenen Hosts", async () => {
const { eigener } = await daten();
sichtbar = eigener.id;
const zahlen = await kennzahlen();
expect(zahlen.hostsGesamt).toBe(1);
expect(zahlen.offeneEinkaufsrechnungen).toBe(0);
});
it("zeigt in der Kundenliste nur den eigenen Kunden", async () => {
const { eigener } = await daten();
sichtbar = eigener.id;
const liste = await kundenListe();
expect(liste).toHaveLength(1);
expect(liste[0].name).toBe("Eigen GmbH");
});
it("sperrt ein Kundenkonto ohne Kundenbindung vollständig aus", async () => {
await daten();
sichtbar = "kein-kunde-zugeordnet";
const liste = await hostListe({});
expect(liste.gesamt).toBe(0);
});
});