Files
alpenwerk-hr/tests/unit/honestly.test.ts
Maximilian Stubhan 683e7cc2d7
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m30s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m6s
Keep private email out of the Honestly export
The Email column exported employees.email, which is the person's
private address. That does not belong in a file sent to an outside
survey provider, least of all as the address invitations go to in the
employer's name.

The column now stays as a placeholder for the work email, which does
not exist in the schema yet and will be added later. It is empty until
then, but keeps its place so the column mapping set up in Honestly does
not have to change once the address arrives. The export no longer
reads employees.email at all, so it cannot end up in another column
by accident either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:10:44 +02:00

149 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from "vitest";
import { toCsv } from "@/lib/export";
import { baueHonestlyZeilen, honestlySpalten, orgKette, ouUeberschrift } from "@/lib/honestly";
import { buildOrgMaps, type OrgUnit } from "@/lib/org";
// Die Datei geht an eine fremde Plattform, die sie über Spaltennamen
// zuordnet. Ein Fehler darin zeigt sich nicht hier, sondern dort — als
// Befragung, die an die falschen Leute geht oder nach der falschen Einheit
// auswertet. Deshalb wird hier die Form festgehalten, nicht nur der Inhalt.
const units: OrgUnit[] = [
{ id: "g", org_number: "1", name: "Manner AG", parent_id: null, unit_type: "Gesellschaft" },
{ id: "b", org_number: "2", name: "Produktion", parent_id: "g", unit_type: "Bereich" },
{ id: "a", org_number: "3", name: "Waffel", parent_id: "b", unit_type: "Abteilung" },
{ id: "t", org_number: "4", name: "Linie 1", parent_id: "a", unit_type: "Team" },
{ id: "v", org_number: "5", name: "Vertrieb", parent_id: "g", unit_type: "Bereich" },
];
const maps = buildOrgMaps(units, []);
const standorte = new Map([["wien", "Wien"], ["wolkersdorf", "Wolkersdorf"]]);
function person(teil: Partial<Parameters<typeof baueHonestlyZeilen>[0][number]> = {}) {
return {
personnel_number: 4711,
// Die private Adresse, wie sie auf der Zeile aus der Datenbank steht —
// sie darf im Export nirgends auftauchen.
email: "anna.privat@example.test",
first_name: "Anna",
last_name: "Berger",
location_id: "wien",
org_unit_id: "t",
...teil,
};
}
describe("orgKette", () => {
it("fängt bei der eigenen Einheit an und endet ganz oben", () => {
// OU ist die Einheit der Person, OU+1 die darüber — so ist es bestellt.
// ancestorsOf liefert es andersherum; ohne das Umdrehen stünde die
// Gesellschaft in der Spalte „OU".
expect(orgKette(maps, "t")).toEqual(["Linie 1", "Waffel", "Produktion", "Manner AG"]);
});
it("ist leer ohne Einheit", () => {
expect(orgKette(maps, null)).toEqual([]);
});
});
describe("Spalten", () => {
it("stehen in der vorgegebenen Reihenfolge", () => {
expect(honestlySpalten(3).map((s) => s.header)).toEqual([
"Personalnummer",
"Email",
"Firstname",
"Last Name",
"Language",
"Location",
"OU",
"OU+1",
"OU+2",
"Role",
]);
});
it("heissen OU, OU+1, OU+2 …", () => {
expect([0, 1, 2, 5].map(ouUeberschrift)).toEqual(["OU", "OU+1", "OU+2", "OU+5"]);
});
it("behalten mindestens eine OU-Spalte", () => {
// Eine Datei, deren Spalten je nach Inhalt verschwinden, bricht die
// Zuordnung, die auf der anderen Seite eingerichtet wurde.
expect(honestlySpalten(0).filter((s) => s.header.startsWith("OU"))).toHaveLength(1);
});
});
describe("baueHonestlyZeilen", () => {
it("richtet die Zahl der OU-Spalten nach der tiefsten Kette", () => {
const { tiefe } = baueHonestlyZeilen([person({ org_unit_id: "v" }), person({ org_unit_id: "t" })], maps, standorte);
expect(tiefe).toBe(4);
});
it("füllt kürzere Ketten hinten mit Leerzellen auf", () => {
const { zeilen, tiefe } = baueHonestlyZeilen([person({ org_unit_id: "v" }), person()], maps, standorte);
const spalten = honestlySpalten(tiefe);
const werte = (i: number) => spalten.map((s) => s.get(zeilen[i]));
// Vertrieb hängt direkt unter der Gesellschaft: zwei Einheiten, zwei leere.
expect(werte(0).slice(6, 10)).toEqual(["Vertrieb", "Manner AG", "", ""]);
expect(werte(1).slice(6, 10)).toEqual(["Linie 1", "Waffel", "Produktion", "Manner AG"]);
});
it("setzt Sprache und Rolle fest", () => {
const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte);
const zeile = Object.fromEntries(honestlySpalten(tiefe).map((s) => [s.header, s.get(zeilen[0])]));
expect(zeile.Language).toBe("de");
expect(zeile.Role).toBe("Respondee");
});
it("übernimmt die Personendaten unverändert", () => {
const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte);
const zeile = Object.fromEntries(honestlySpalten(tiefe).map((s) => [s.header, s.get(zeilen[0])]));
expect(zeile).toMatchObject({
Personalnummer: 4711,
Firstname: "Anna",
"Last Name": "Berger",
Location: "Wien",
});
});
it("lässt die E-Mail leer, bis es die Firmenadresse gibt", () => {
// Die Spalte ist ein Platzhalter für die Firmen-E-Mail. Die private
// Adresse (employees.email) gehört nicht in eine Datei an einen fremden
// Anbieter — auch nicht, wenn sie vorhanden ist.
const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte);
const zeile = Object.fromEntries(honestlySpalten(tiefe).map((s) => [s.header, s.get(zeilen[0])]));
expect(zeile.Email).toBe("");
});
it("trägt die private Adresse in keine Spalte ein", () => {
// Nicht nur „Email" prüfen: rutschte sie in eine andere Spalte, fiele es
// dort erst in Honestly auf.
const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte);
const csv = toCsv(zeilen, honestlySpalten(tiefe));
expect(csv).not.toContain("anna.privat@example.test");
});
it("behält die Spalte Email, auch solange sie leer ist", () => {
// Die Zuordnung in Honestly wird einmal eingerichtet. Kommt die
// Firmenadresse später dazu, soll sich an der Datei nur der Inhalt ändern.
expect(honestlySpalten(1).map((s) => s.header)).toContain("Email");
});
it("gibt Personen ohne Einheit eine leere Kette, aber eine Zeile", () => {
// Wer nicht zugeordnet ist, soll trotzdem befragt werden.
const { zeilen, tiefe } = baueHonestlyZeilen([person({ org_unit_id: null })], maps, standorte);
expect(zeilen).toHaveLength(1);
expect(tiefe).toBe(1);
expect(honestlySpalten(tiefe).find((s) => s.header === "OU")?.get(zeilen[0])).toBe("");
});
});
describe("als CSV", () => {
it("schreibt die Kopfzeile genau so, wie Honestly sie zuordnet", () => {
const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte);
const kopf = toCsv(zeilen, honestlySpalten(tiefe)).replace(/^/, "").split(/\r?\n/)[0];
expect(kopf).toContain("Personalnummer");
expect(kopf).toContain("Last Name");
expect(kopf).toMatch(/OU\+3.*Role/);
});
});