Files
Laufzettel/tests/abteilungen.test.ts
T
Florian WildeandClaude Opus 5 86d55917f9
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(stammdaten): Mitarbeiter, Abteilungen und Geraete
Die Stammdaten, an denen die Historie haengt. Personen werden deaktiviert,
Geraete und Abteilungen archiviert — geloescht wird nichts, woran ein Lauf
haengen kann.

- Mitarbeiterliste mit Suche ueber Anzeigename, Mail, UPN und sAMAccountName,
  Filter nach Abteilung, standardmaessig nur aktive; die Suche ist ein eigenes
  Bauteil, weil #013 und #014 sie wieder brauchen
- Personenseite mit Stammdaten, Geraeten, Vorgesetztem; was aus dem Verzeichnis
  kommt, ist gesperrt — der naechste Abgleich wuerde es ohnehin zurueckdrehen
- Abteilungen unter Admin: Ziel-OU, Standardgruppen, Mail-Domain, dazu die
  Namen, die bei Personen vorkommen, aber nicht gepflegt sind
- Geraete mit Suche ueber Hostname, Bezeichnung, Seriennummer und Tag-Code;
  CSV-Import mit einem Ergebnis je Zeile
- Neues Recht mitarbeiterBearbeiten; die Seitenleiste fuehrt Personen und
  Geraete unter einem Eintrag, im Modus Dienstleistung anders beschriftet

Der unabhaengige Review fand zwei Blocker. Der schwerere meldete Erfolg: ein
nicht geschlossenes Anfuehrungszeichen verschluckte im CSV-Import alle
Folgezeilen bis zum naechsten `"`, und der Import sagte „3 von 3 Zeilen
angelegt". Ein Wert reicht jetzt nicht mehr ueber die Zeile hinaus — fuer
Hostnamen und Seriennummern ist ein Umbruch praktisch immer ein vergessenes
Anfuehrungszeichen, und so endet der Schaden an seiner Zeile. Der zweite:
ungepflegte Abteilungen wurden ueber alle Kunden hinweg gruppiert.

Dazu sieben weitere Befunde, alle behoben — darunter eine Besitzerauswahl, die
Personen fremder Kunden anbot, und ein Techniker, der ein Geraet endgueltig
loeschen konnte.

610 Tests gruen, 18 E2E-Ablaeufe gegen den gebauten Stand.

Schliesst #011

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 12:51:54 +02:00

269 lines
12 KiB
TypeScript
Raw 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 { beforeEach, describe, expect, it } from "vitest";
import { Betriebsmodus } from "@/generated/prisma/enums";
import {
AbteilungsFehler,
abteilungAnlegen,
abteilungArchivieren,
abteilungBearbeiten,
} from "@/server/services/abteilungen";
import { abteilungsListe, ungepflegteAbteilungen } from "@/server/queries/abteilungen";
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
/**
* Abteilungen (#011).
*
* An einer Abteilung haengen Ziel-OU und Standardgruppen — das, was die
* spaeteren PowerShell-Befehle in ein Verzeichnis schreiben (#012). Deshalb wird
* hier genauer geprueft als bei einer blossen Beschriftung.
*/
async function grundbestand() {
const eigene = await testPrisma.kunde.create({
data: { name: "Eigene Organisation", kuerzel: "EIGEN", istEigeneOrganisation: true },
});
await testPrisma.einstellung.create({ data: { id: "global", modus: Betriebsmodus.INTERN } });
const benutzer = await testPrisma.benutzer.create({
data: { email: "admin@firma.example", name: "Admin", rolle: "ADMIN" },
});
return { eigene, admin: { id: benutzer.id, rolle: benutzer.rolle } };
}
describe.runIf(datenbankVerfuegbar)("Abteilungen", () => {
beforeEach(async () => {
await datenbankLeeren();
});
describe("Anlegen", () => {
it("legt Name, Ziel-OU, Gruppen und Mail-Domain an", async () => {
const { admin } = await grundbestand();
const abteilung = await abteilungAnlegen(
{
name: " Buchhaltung ",
zielOu: "OU=Buchhaltung,OU=Benutzer,DC=firma,DC=de",
standardGruppen: ["G-Buchhaltung", "G-Alle"],
mailDomain: "firma.de",
},
admin,
);
expect(abteilung.name).toBe("Buchhaltung");
expect(abteilung.standardGruppen).toEqual(["G-Buchhaltung", "G-Alle"]);
expect(abteilung.mailDomain).toBe("firma.de");
});
it("weist denselben Namen ein zweites Mal ab, mit lesbarer Meldung", async () => {
const { admin } = await grundbestand();
await abteilungAnlegen({ name: "Buchhaltung" }, admin);
await expect(abteilungAnlegen({ name: "Buchhaltung" }, admin)).rejects.toThrow(/gibt es schon/);
});
it("räumt leere und doppelte Gruppen weg, behält die Reihenfolge", async () => {
// Die Reihenfolge landet spaeter im Add-ADGroupMember-Befehl; sie ist
// keine Menge, sondern eine Liste.
const { admin } = await grundbestand();
const abteilung = await abteilungAnlegen(
{ name: "Vertrieb", standardGruppen: ["G-Alle", " ", "G-Vertrieb", "G-Alle", ""] },
admin,
);
expect(abteilung.standardGruppen).toEqual(["G-Alle", "G-Vertrieb"]);
});
it("weist eine Mail-Adresse als Domain ab", async () => {
const { admin } = await grundbestand();
await expect(abteilungAnlegen({ name: "Vertrieb", mailDomain: "max@firma.de" }, admin)).rejects.toThrow(
/Mail-Domain/,
);
await expect(abteilungAnlegen({ name: "Einkauf", mailDomain: "firma" }, admin)).rejects.toThrow(
AbteilungsFehler,
);
});
it("nimmt eine Domain mit führendem @ an und normalisiert sie", async () => {
const { admin } = await grundbestand();
const abteilung = await abteilungAnlegen({ name: "IT", mailDomain: "@Firma.DE" }, admin);
expect(abteilung.mailDomain).toBe("firma.de");
});
it("weist einen leeren Namen ab", async () => {
const { admin } = await grundbestand();
await expect(abteilungAnlegen({ name: " " }, admin)).rejects.toThrow(AbteilungsFehler);
expect(await testPrisma.abteilung.count()).toBe(0);
});
});
describe("Bearbeiten", () => {
it("schreibt nichts, wenn sich nichts ändert", async () => {
const { admin } = await grundbestand();
const abteilung = await abteilungAnlegen({ name: "Buchhaltung" }, admin);
const vorher = await testPrisma.protokoll.count();
await abteilungBearbeiten(abteilung.id, { name: "Buchhaltung" }, admin);
expect(await testPrisma.protokoll.count()).toBe(vorher);
});
it("erkennt eine geänderte Gruppenreihenfolge als Änderung", async () => {
const { admin } = await grundbestand();
const abteilung = await abteilungAnlegen(
{ name: "Buchhaltung", standardGruppen: ["G-Alle", "G-Buchhaltung"] },
admin,
);
const nachher = await abteilungBearbeiten(
abteilung.id,
{ standardGruppen: ["G-Buchhaltung", "G-Alle"] },
admin,
);
expect(nachher.standardGruppen).toEqual(["G-Buchhaltung", "G-Alle"]);
expect(await testPrisma.protokoll.count({ where: { aktion: "Standardgruppen geändert" } })).toBe(1);
});
it("weist einen Namen ab, den es schon gibt", async () => {
const { admin } = await grundbestand();
await abteilungAnlegen({ name: "Buchhaltung" }, admin);
const vertrieb = await abteilungAnlegen({ name: "Vertrieb" }, admin);
await expect(abteilungBearbeiten(vertrieb.id, { name: "Buchhaltung" }, admin)).rejects.toThrow(
/gibt es schon/,
);
});
});
describe("Archivieren statt löschen", () => {
it("archiviert und gibt wieder frei", async () => {
const { admin } = await grundbestand();
const abteilung = await abteilungAnlegen({ name: "Buchhaltung" }, admin);
expect((await abteilungArchivieren(abteilung.id, true, admin)).archiviert).toBe(true);
expect((await abteilungArchivieren(abteilung.id, false, admin)).archiviert).toBe(false);
});
it("bietet gar keinen Löschweg an", async () => {
const dienst = await import("@/server/services/abteilungen");
expect(Object.keys(dienst).filter((name) => /loesch|entfern|delete/iu.test(name))).toEqual([]);
});
it("blendet archivierte auf Wunsch aus", async () => {
const { admin } = await grundbestand();
const abteilung = await abteilungAnlegen({ name: "Buchhaltung" }, admin);
await abteilungArchivieren(abteilung.id, true, admin);
expect(await abteilungsListe(admin, false)).toEqual([]);
expect(await abteilungsListe(admin, true)).toHaveLength(1);
});
});
describe("Zuordnung über den Namen", () => {
it("zählt nur aktive Personen mit passendem Abteilungsnamen", async () => {
const { admin, eigene } = await grundbestand();
await abteilungAnlegen({ name: "Buchhaltung" }, admin);
for (const [name, aktiv] of [
["Maria", true],
["Jens", true],
["Ex", false],
] as const) {
await testPrisma.mitarbeiter.create({
data: {
kundeId: eigene.id,
vorname: name,
nachname: "X",
anzeigename: name,
abteilung: "Buchhaltung",
aktiv,
},
});
}
const zeile = (await abteilungsListe(admin))[0]!;
expect(zeile.anzahlMitarbeiter).toBe(2);
});
it("meldet Abteilungsnamen, die es bei Personen gibt, aber nicht gepflegt sind", async () => {
// Das ist der Vorschlag nach einem Verzeichnisabgleich: der Name steht
// schon in den Daten, es fehlen nur Ziel-OU und Gruppen.
const { admin, eigene } = await grundbestand();
await abteilungAnlegen({ name: "Buchhaltung" }, admin);
for (const abteilung of ["Buchhaltung", "Vertrieb", "Vertrieb"]) {
await testPrisma.mitarbeiter.create({
data: { kundeId: eigene.id, vorname: "P", nachname: "X", anzeigename: "P X", abteilung },
});
}
expect(await ungepflegteAbteilungen(admin)).toEqual([
{ name: "Vertrieb", anzahl: 2, kundeId: eigene.id },
]);
});
});
describe("Mandantensicht", () => {
async function zweiKunden() {
const eigene = await testPrisma.kunde.create({
data: { name: "Eigene", kuerzel: "EIGEN", istEigeneOrganisation: true },
});
const meiner = await testPrisma.kunde.create({ data: { name: "Meiner", kuerzel: "MEIN" } });
const fremder = await testPrisma.kunde.create({ data: { name: "Fremder", kuerzel: "FREMD" } });
await testPrisma.einstellung.create({
data: { id: "global", modus: Betriebsmodus.DIENSTLEISTUNG },
});
const techniker = await testPrisma.benutzer.create({
data: { email: "t@firma.example", name: "Techniker", rolle: "TECHNIKER" },
});
await testPrisma.benutzerKunde.create({ data: { benutzerId: techniker.id, kundeId: meiner.id } });
process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "lz-kunde-";
await testPrisma.abteilung.create({ data: { kundeId: meiner.id, name: "Meine Abteilung" } });
const fremde = await testPrisma.abteilung.create({
data: { kundeId: fremder.id, name: "Fremde Abteilung", zielOu: "OU=Geheim" },
});
return { eigene, techniker: { id: techniker.id, rolle: techniker.rolle }, fremde };
}
it("zeigt einem gebundenen Techniker keine fremde Abteilung", async () => {
const { techniker } = await zweiKunden();
expect((await abteilungsListe(techniker)).map((z) => z.name)).toEqual(["Meine Abteilung"]);
});
it("lässt eine fremde Abteilung nicht bearbeiten", async () => {
const { techniker, fremde } = await zweiKunden();
await expect(abteilungBearbeiten(fremde.id, { zielOu: "OU=Anders" }, techniker)).rejects.toThrow(
/Berechtigung/,
);
});
it("hält bei ungepflegten Abteilungen die Kunden auseinander", async () => {
/*
* Der Blocker aus dem Review: gruppiert wurde nach Namen allein. Damit galt
* „Vertrieb“ als gepflegt, sobald **irgendein** Kunde eine Abteilung dieses
* Namens hatte — und beim Kunden ohne sie blieben `{{ou}}` und
* `{{gruppen}}` im Lauf leer, ohne dass die Seite es je sagte.
*/
const { eigene } = await zweiKunden();
const admin = await testPrisma.benutzer.create({
data: { email: "a@firma.example", name: "Admin", rolle: "ADMIN" },
});
const handelnder = { id: admin.id, rolle: admin.rolle };
const meiner = await testPrisma.kunde.findFirstOrThrow({ where: { kuerzel: "MEIN" } });
const fremder = await testPrisma.kunde.findFirstOrThrow({ where: { kuerzel: "FREMD" } });
// Nur der eine Kunde pflegt „Vertrieb“.
await testPrisma.abteilung.create({ data: { kundeId: meiner.id, name: "Vertrieb" } });
for (const kundeId of [meiner.id, fremder.id, fremder.id]) {
await testPrisma.mitarbeiter.create({
data: { kundeId, vorname: "P", nachname: "X", anzeigename: "P X", abteilung: "Vertrieb" },
});
}
const offen = await ungepflegteAbteilungen(handelnder);
expect(offen).toEqual([{ name: "Vertrieb", anzahl: 2, kundeId: fremder.id }]);
expect(offen.some((eintrag) => eintrag.kundeId === meiner.id)).toBe(false);
expect(eigene.id).toBeTruthy();
});
it("lässt eine fremde Abteilung nicht archivieren", async () => {
const { techniker, fremde } = await zweiKunden();
await expect(abteilungArchivieren(fremde.id, true, techniker)).rejects.toThrow(/Berechtigung/);
});
it("erlaubt denselben Abteilungsnamen bei zwei Kunden", async () => {
// Der Eindeutigkeitsindex geht ueber (kundeId, name) – „Buchhaltung“ gibt
// es bei jedem Kunden genau einmal, und das ist richtig so.
const { eigene } = await zweiKunden();
const zweiter = await testPrisma.kunde.findFirstOrThrow({ where: { kuerzel: "MEIN" } });
await testPrisma.abteilung.create({ data: { kundeId: eigene.id, name: "Buchhaltung" } });
await expect(
testPrisma.abteilung.create({ data: { kundeId: zweiter.id, name: "Buchhaltung" } }),
).resolves.toBeTruthy();
});
});
});