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>
269 lines
12 KiB
TypeScript
269 lines
12 KiB
TypeScript
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();
|
||
});
|
||
});
|
||
});
|