410 lines
16 KiB
TypeScript
410 lines
16 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { Betriebsmodus, Herkunft } from "@/generated/prisma/enums";
|
|
import {
|
|
ABGEGLICHENE_FELDER,
|
|
MitarbeiterFehler,
|
|
mitarbeiterAktivSetzen,
|
|
mitarbeiterAnlegen,
|
|
mitarbeiterBearbeiten,
|
|
} from "@/server/services/mitarbeiter";
|
|
import {
|
|
abteilungsNamen,
|
|
geraeteDesMitarbeiters,
|
|
mitarbeiterHolen,
|
|
mitarbeiterListe,
|
|
} from "@/server/queries/mitarbeiter";
|
|
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
|
|
|
/**
|
|
* Mitarbeiter (#011).
|
|
*
|
|
* Zwei Regeln tragen das Ganze: nichts wird geloescht, und was aus dem
|
|
* Verzeichnis kommt, wird nicht von Hand ueberschrieben. Alles hier dreht sich
|
|
* darum — dazu die Mandantensicht, die im Review von #009 dreimal die Fundstelle
|
|
* war.
|
|
*/
|
|
|
|
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 admin = await testPrisma.benutzer.create({
|
|
data: { email: "admin@firma.example", name: "Admin", rolle: "ADMIN" },
|
|
});
|
|
return { eigene, admin: { id: admin.id, rolle: admin.rolle } };
|
|
}
|
|
|
|
describe.runIf(datenbankVerfuegbar)("Mitarbeiter", () => {
|
|
beforeEach(async () => {
|
|
await datenbankLeeren();
|
|
});
|
|
|
|
describe("Anlegen", () => {
|
|
it("bildet den Anzeigenamen aus Vor- und Nachname, wenn keiner angegeben ist", async () => {
|
|
const { admin } = await grundbestand();
|
|
const person = await mitarbeiterAnlegen({ vorname: " Maria ", nachname: " Huber " }, admin);
|
|
expect(person.anzeigename).toBe("Maria Huber");
|
|
expect(person.herkunft).toBe(Herkunft.MANUELL);
|
|
expect(person.aktiv).toBe(true);
|
|
});
|
|
|
|
it("nimmt einen eigenen Anzeigenamen an", async () => {
|
|
const { admin } = await grundbestand();
|
|
const person = await mitarbeiterAnlegen(
|
|
{ vorname: "Maria", nachname: "Huber", anzeigename: "Huber, Maria" },
|
|
admin,
|
|
);
|
|
expect(person.anzeigename).toBe("Huber, Maria");
|
|
});
|
|
|
|
it("weist einen leeren Namen ab", async () => {
|
|
const { admin } = await grundbestand();
|
|
await expect(mitarbeiterAnlegen({ vorname: " ", nachname: "Huber" }, admin)).rejects.toThrow(
|
|
MitarbeiterFehler,
|
|
);
|
|
expect(await testPrisma.mitarbeiter.count()).toBe(0);
|
|
});
|
|
|
|
it("weist einen sAMAccountName über 20 Zeichen ab", async () => {
|
|
// Das Active Directory kuerzt nicht, es lehnt ab — und zwar erst beim
|
|
// New-ADUser vor dem Kunden.
|
|
const { admin } = await grundbestand();
|
|
await expect(
|
|
mitarbeiterAnlegen({ vorname: "Jörg", nachname: "Müller", sam: "j".repeat(21) }, admin),
|
|
).rejects.toThrow(/höchstens 20/);
|
|
await expect(
|
|
mitarbeiterAnlegen({ vorname: "Jörg", nachname: "Müller", sam: "j".repeat(20) }, admin),
|
|
).resolves.toBeTruthy();
|
|
});
|
|
|
|
it("schreibt einen Protokolleintrag", async () => {
|
|
const { admin } = await grundbestand();
|
|
const person = await mitarbeiterAnlegen({ vorname: "Maria", nachname: "Huber" }, admin);
|
|
const eintrag = await testPrisma.protokoll.findFirstOrThrow({
|
|
where: { entitaetId: person.id },
|
|
});
|
|
expect(eintrag.aktion).toBe("Mitarbeiter angelegt");
|
|
expect(eintrag.benutzerId).toBe(admin.id);
|
|
});
|
|
});
|
|
|
|
describe("Aus dem Verzeichnis abgeglichene Personen", () => {
|
|
async function abgeglichen(kundeId: string) {
|
|
return testPrisma.mitarbeiter.create({
|
|
data: {
|
|
kundeId,
|
|
vorname: "Maria",
|
|
nachname: "Huber",
|
|
anzeigename: "Maria Huber",
|
|
mail: "maria.huber@firma.example",
|
|
abteilung: "Buchhaltung",
|
|
herkunft: Herkunft.ABGLEICH,
|
|
},
|
|
});
|
|
}
|
|
|
|
it("lässt abgeglichene Felder nicht von Hand ändern", async () => {
|
|
// Der naechste Abgleich wuerde die Aenderung ohnehin zurueckdrehen. Ein
|
|
// Wert, der zwischen zwei Laeufen hin- und herspringt, ist schlimmer als
|
|
// ein falscher, weil niemand ihm mehr traut.
|
|
const { admin, eigene } = await grundbestand();
|
|
const person = await abgeglichen(eigene.id);
|
|
|
|
for (const aenderung of [
|
|
{ vorname: "Marie" },
|
|
{ nachname: "Hubert" },
|
|
{ mail: "anders@firma.example" },
|
|
{ abteilung: "Vertrieb" },
|
|
]) {
|
|
await expect(
|
|
mitarbeiterBearbeiten(person.id, aenderung, admin),
|
|
JSON.stringify(aenderung),
|
|
).rejects.toThrow(/Verzeichnis/);
|
|
}
|
|
});
|
|
|
|
it("stört sich nicht an unverändert mitgeschickten Feldern", async () => {
|
|
// Das Formular schickt alle Felder mit. Wuerde schon das Mitschicken
|
|
// scheitern, liefe der Benutzer gegen eine Fehlermeldung, ohne etwas
|
|
// geaendert zu haben. Abgewiesen wird nur, was sich wirklich aendern soll.
|
|
const { admin, eigene } = await grundbestand();
|
|
const person = await abgeglichen(eigene.id);
|
|
const vorherProtokoll = await testPrisma.protokoll.count();
|
|
|
|
const nachher = await mitarbeiterBearbeiten(
|
|
person.id,
|
|
{
|
|
vorname: "Maria",
|
|
nachname: "Huber",
|
|
anzeigename: "Maria Huber",
|
|
mail: "maria.huber@firma.example",
|
|
abteilung: "Buchhaltung",
|
|
},
|
|
admin,
|
|
);
|
|
expect(nachher.anzeigename).toBe("Maria Huber");
|
|
expect(await testPrisma.protokoll.count()).toBe(vorherProtokoll);
|
|
});
|
|
|
|
it("sperrt bei ABGLEICH jedes gepflegte Feld, nicht nur einige", async () => {
|
|
// Alle Stammdaten einer abgeglichenen Person kommen aus dem Verzeichnis —
|
|
// auch der Titel. Uebrig bleibt genau ein Weg: aktivieren und
|
|
// deaktivieren. Das ist Absicht und keine Luecke.
|
|
const { admin, eigene } = await grundbestand();
|
|
const person = await abgeglichen(eigene.id);
|
|
|
|
for (const feld of ABGEGLICHENE_FELDER) {
|
|
await expect(
|
|
mitarbeiterBearbeiten(person.id, { [feld]: `anders-${feld}` }, admin),
|
|
feld,
|
|
).rejects.toThrow(/Verzeichnis/);
|
|
}
|
|
});
|
|
|
|
it("lässt eine manuell angelegte Person vollständig ändern", async () => {
|
|
const { admin } = await grundbestand();
|
|
const person = await mitarbeiterAnlegen({ vorname: "Maria", nachname: "Huber" }, admin);
|
|
const nachher = await mitarbeiterBearbeiten(
|
|
person.id,
|
|
{ vorname: "Marie", mail: "marie@firma.example" },
|
|
admin,
|
|
);
|
|
expect(nachher.vorname).toBe("Marie");
|
|
expect(nachher.mail).toBe("marie@firma.example");
|
|
});
|
|
|
|
it("lässt sich auch bei Herkunft ABGLEICH deaktivieren", async () => {
|
|
// Das Offboarding muss auch dann gehen, wenn das Verzeichnis noch nicht
|
|
// nachgezogen hat.
|
|
const { admin, eigene } = await grundbestand();
|
|
const person = await abgeglichen(eigene.id);
|
|
const nachher = await mitarbeiterAktivSetzen(person.id, false, admin);
|
|
expect(nachher.aktiv).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("Bearbeiten", () => {
|
|
it("schreibt nichts, wenn sich nichts ändert", async () => {
|
|
const { admin } = await grundbestand();
|
|
const person = await mitarbeiterAnlegen({ vorname: "Maria", nachname: "Huber" }, admin);
|
|
const vorher = await testPrisma.protokoll.count();
|
|
await mitarbeiterBearbeiten(person.id, { vorname: "Maria", nachname: "Huber" }, admin);
|
|
expect(await testPrisma.protokoll.count()).toBe(vorher);
|
|
});
|
|
|
|
it("nennt im Protokoll, was sich geändert hat", async () => {
|
|
const { admin } = await grundbestand();
|
|
const person = await mitarbeiterAnlegen({ vorname: "Maria", nachname: "Huber" }, admin);
|
|
await mitarbeiterBearbeiten(person.id, { mail: "m.huber@firma.example" }, admin);
|
|
const eintrag = await testPrisma.protokoll.findFirstOrThrow({
|
|
where: { entitaetId: person.id, aktion: { contains: "geändert" } },
|
|
});
|
|
expect(eintrag.aktion).toBe("Mail-Adresse geändert");
|
|
expect(eintrag.vorher).toEqual({ mail: null });
|
|
expect(eintrag.nachher).toEqual({ mail: "m.huber@firma.example" });
|
|
});
|
|
});
|
|
|
|
describe("Deaktivieren statt löschen", () => {
|
|
it("setzt aktiv auf false und protokolliert es", async () => {
|
|
const { admin } = await grundbestand();
|
|
const person = await mitarbeiterAnlegen({ vorname: "Maria", nachname: "Huber" }, admin);
|
|
await mitarbeiterAktivSetzen(person.id, false, admin);
|
|
|
|
expect((await testPrisma.mitarbeiter.findUniqueOrThrow({ where: { id: person.id } })).aktiv).toBe(
|
|
false,
|
|
);
|
|
const eintrag = await testPrisma.protokoll.findFirstOrThrow({
|
|
where: { entitaetId: person.id, aktion: "Mitarbeiter deaktiviert" },
|
|
});
|
|
expect(eintrag).toBeTruthy();
|
|
});
|
|
|
|
it("bietet gar keinen Löschweg an", async () => {
|
|
// Waere Loeschen manchmal moeglich, muesste jeder Aufrufer die Bedingung
|
|
// kennen — und beim Offboarding entscheidet sich niemand bewusst zwischen
|
|
// zwei Wegen. Geprueft wird der Dienst selbst: was er nicht anbietet, kann
|
|
// auch niemand versehentlich rufen.
|
|
const dienst = await import("@/server/services/mitarbeiter");
|
|
const verdaechtig = Object.keys(dienst).filter((name) => /loesch|entfern|delete/iu.test(name));
|
|
expect(verdaechtig).toEqual([]);
|
|
expect(Object.keys(dienst)).toContain("mitarbeiterAktivSetzen");
|
|
});
|
|
});
|
|
|
|
describe("Liste und Suche", () => {
|
|
async function bestand() {
|
|
const { admin, eigene } = await grundbestand();
|
|
await mitarbeiterAnlegen(
|
|
{
|
|
vorname: "Maria",
|
|
nachname: "Huber",
|
|
mail: "maria.huber@firma.example",
|
|
upn: "maria.huber@firma.example",
|
|
sam: "mhuber",
|
|
abteilung: "Buchhaltung",
|
|
},
|
|
admin,
|
|
);
|
|
const jens = await mitarbeiterAnlegen(
|
|
{ vorname: "Jens", nachname: "Ex", sam: "jex", abteilung: "Vertrieb" },
|
|
admin,
|
|
);
|
|
await mitarbeiterAktivSetzen(jens.id, false, admin);
|
|
return { admin, eigene, jens };
|
|
}
|
|
|
|
it("zeigt standardmäßig nur aktive Personen", async () => {
|
|
const { admin } = await bestand();
|
|
const namen = (await mitarbeiterListe(admin)).map((z) => z.anzeigename);
|
|
expect(namen).toEqual(["Maria Huber"]);
|
|
});
|
|
|
|
it("zeigt mit Umschalter auch deaktivierte", async () => {
|
|
const { admin } = await bestand();
|
|
const namen = (await mitarbeiterListe(admin, { auchDeaktivierte: true })).map((z) => z.anzeigename);
|
|
expect(namen.sort()).toEqual(["Jens Ex", "Maria Huber"]);
|
|
});
|
|
|
|
it("sucht über Anzeigename, Mail, UPN und sam", async () => {
|
|
const { admin } = await bestand();
|
|
for (const suche of ["huber", "MARIA.HUBER@firma", "mhuber"]) {
|
|
const treffer = await mitarbeiterListe(admin, { suche });
|
|
expect(
|
|
treffer.map((z) => z.anzeigename),
|
|
suche,
|
|
).toEqual(["Maria Huber"]);
|
|
}
|
|
});
|
|
|
|
it("lässt den Aktiv-Filter auch beim Suchen stehen", async () => {
|
|
// Genau der Blocker aus #009 in neuem Gewand: der Suchfilter bringt ein
|
|
// eigenes `OR` mit. Liegt es nicht in einem `AND`, gewinnt es.
|
|
const { admin } = await bestand();
|
|
const treffer = await mitarbeiterListe(admin, { suche: "e" });
|
|
expect(treffer.map((z) => z.anzeigename)).toEqual(["Maria Huber"]);
|
|
});
|
|
|
|
it("filtert nach Abteilung", async () => {
|
|
const { admin } = await bestand();
|
|
expect((await mitarbeiterListe(admin, { abteilung: "Buchhaltung" })).length).toBe(1);
|
|
expect((await mitarbeiterListe(admin, { abteilung: "Vertrieb" })).length).toBe(0);
|
|
});
|
|
|
|
it("liefert die vorkommenden Abteilungsnamen", async () => {
|
|
const { admin } = await bestand();
|
|
expect(await abteilungsNamen(admin)).toEqual(["Buchhaltung", "Vertrieb"]);
|
|
});
|
|
|
|
it("zählt Geräte und Läufe", async () => {
|
|
const { admin, eigene } = await bestand();
|
|
const person = (await mitarbeiterListe(admin))[0]!;
|
|
await testPrisma.geraet.create({
|
|
data: { kundeId: eigene.id, mitarbeiterId: person.id, tagCode: "LZ-7K3F9Q", hostname: "PC-001" },
|
|
});
|
|
const frisch = (await mitarbeiterListe(admin))[0]!;
|
|
expect(frisch.anzahlGeraete).toBe(1);
|
|
expect(frisch.anzahlLaeufe).toBe(0);
|
|
});
|
|
|
|
it("gibt die Geräte einer Person zurück, Hostname vor Bezeichnung", async () => {
|
|
const { admin, eigene } = await bestand();
|
|
const person = (await mitarbeiterListe(admin))[0]!;
|
|
await testPrisma.geraet.create({
|
|
data: { kundeId: eigene.id, mitarbeiterId: person.id, tagCode: "LZ-7K3F9Q", hostname: "PC-001" },
|
|
});
|
|
await testPrisma.geraet.create({
|
|
data: {
|
|
kundeId: eigene.id,
|
|
mitarbeiterId: person.id,
|
|
tagCode: "LZ-8M4G2R",
|
|
bezeichnung: "Monitor rechts",
|
|
},
|
|
});
|
|
const geraete = await geraeteDesMitarbeiters(person.id, admin);
|
|
expect(geraete.map((g) => g.name).sort()).toEqual(["Monitor rechts", "PC-001"]);
|
|
});
|
|
});
|
|
|
|
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-";
|
|
|
|
for (const [kundeId, name] of [
|
|
[meiner.id, "Meine Person"],
|
|
[fremder.id, "Fremde Person"],
|
|
] as const) {
|
|
await testPrisma.mitarbeiter.create({
|
|
data: { kundeId, vorname: name, nachname: "X", anzeigename: name, abteilung: "Geheim" },
|
|
});
|
|
}
|
|
return {
|
|
eigene,
|
|
techniker: { id: techniker.id, rolle: techniker.rolle },
|
|
fremdePerson: await testPrisma.mitarbeiter.findFirstOrThrow({ where: { kundeId: fremder.id } }),
|
|
};
|
|
}
|
|
|
|
it("zeigt einem gebundenen Techniker keine fremde Person", async () => {
|
|
const { techniker } = await zweiKunden();
|
|
expect((await mitarbeiterListe(techniker)).map((z) => z.anzeigename)).toEqual(["Meine Person"]);
|
|
});
|
|
|
|
it("hält die Grenze auch beim Suchen", async () => {
|
|
const { techniker } = await zweiKunden();
|
|
expect((await mitarbeiterListe(techniker, { suche: "Person" })).map((z) => z.anzeigename)).toEqual([
|
|
"Meine Person",
|
|
]);
|
|
});
|
|
|
|
it("gibt eine fremde Person nicht einzeln heraus", async () => {
|
|
const { techniker, fremdePerson } = await zweiKunden();
|
|
expect(await mitarbeiterHolen(fremdePerson.id, techniker)).toBeUndefined();
|
|
});
|
|
|
|
it("zeigt die Geräte einer fremden Person nicht", async () => {
|
|
const { techniker, fremdePerson } = await zweiKunden();
|
|
await testPrisma.geraet.create({
|
|
data: { kundeId: fremdePerson.kundeId, mitarbeiterId: fremdePerson.id, tagCode: "LZ-7K3F9Q" },
|
|
});
|
|
expect(await geraeteDesMitarbeiters(fremdePerson.id, techniker)).toEqual([]);
|
|
});
|
|
|
|
it("blendet fremde Abteilungsnamen aus dem Filter aus", async () => {
|
|
const { techniker } = await zweiKunden();
|
|
await testPrisma.mitarbeiter.updateMany({
|
|
where: { anzeigename: "Fremde Person" },
|
|
data: { abteilung: "Nur-fuer-Fremde" },
|
|
});
|
|
expect(await abteilungsNamen(techniker)).not.toContain("Nur-fuer-Fremde");
|
|
});
|
|
|
|
it("lässt eine fremde Person nicht deaktivieren", async () => {
|
|
const { techniker, fremdePerson } = await zweiKunden();
|
|
await expect(mitarbeiterAktivSetzen(fremdePerson.id, false, techniker)).rejects.toThrow(/Berechtigung/);
|
|
});
|
|
|
|
it("lässt eine fremde Person nicht bearbeiten", async () => {
|
|
const { techniker, fremdePerson } = await zweiKunden();
|
|
await expect(mitarbeiterBearbeiten(fremdePerson.id, { titel: "Chef" }, techniker)).rejects.toThrow(
|
|
/Berechtigung/,
|
|
);
|
|
});
|
|
});
|
|
});
|