import { Kysely, PostgresDialect } from "kysely"; import { Pool } from "pg"; import { afterAll, describe, expect, it } from "vitest"; import { derivedStatusFilter } from "@/lib/employee-status-filter"; import type { Schema } from "@/lib/db/schema"; import { todayIso } from "@/lib/format"; import { deriveStatusAsOf } from "@/lib/reports"; import type { EmploymentStatus } from "@/lib/supabase/types"; // lib/employee-status-filter.ts is a SQL restatement of deriveStatusAsOf(): // the employee list pages in the database and cannot derive status in JS, so // the same rule exists twice. Two copies of a rule drift, and the drift is // invisible — a dashboard tile and the list it links to just quietly show // different numbers. // // Only a real database can settle it, so this runs both over the whole // seeded roster and demands the same set of ids. // // Eigene Verbindung statt der Zugriffsschicht: geprüft wird die Bedingung, // nicht die Berechtigung. Mit RLS dazwischen liefe der Test gegen eine // gefilterte Teilmenge und bewiese nichts über die Regel. const db = new Kysely({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.DATABASE_URL, max: 2 }) }), }); describe.skipIf(!process.env.DATABASE_URL)("derived status filter matches deriveStatusAsOf", () => { const asOf = todayIso(); afterAll(async () => { await db.destroy(); }); async function idsFromDatabase(statuses: EmploymentStatus[]): Promise> { const rows = await db .selectFrom("employees") .select("id") .where((eb) => derivedStatusFilter(eb, statuses, asOf) ?? eb.val(true)) .execute(); return new Set(rows.map((r) => r.id)); } async function idsFromDerivation(statuses: EmploymentStatus[]): Promise> { const rows = await db .selectFrom("employees") .select(["id", "entry_date", "exit_date", "karenz_start_date", "karenz_return_date"]) .execute(); return new Set(rows.filter((e) => statuses.includes(deriveStatusAsOf(e, asOf))).map((e) => e.id)); } async function expectSameSet(statuses: EmploymentStatus[]) { const [fromDb, fromJs] = await Promise.all([idsFromDatabase(statuses), idsFromDerivation(statuses)]); const onlyInDb = [...fromDb].filter((id) => !fromJs.has(id)); const onlyInJs = [...fromJs].filter((id) => !fromDb.has(id)); expect({ statuses, onlyInDb: onlyInDb.slice(0, 5), onlyInJs: onlyInJs.slice(0, 5) }).toEqual({ statuses, onlyInDb: [], onlyInJs: [], }); // Guards against a filter so narrow it matches nothing and passes // trivially. expect(fromJs.size).toBeGreaterThan(0); } it("agrees for Aktiv + Karenz — the definition the dashboard headcount uses", async () => { await expectSameSet(["Aktiv", "Karenz"]); }); it("agrees for Aktiv alone", async () => { await expectSameSet(["Aktiv"]); }); it("agrees for Karenz alone", async () => { await expectSameSet(["Karenz"]); }); it("agrees for Geplant", async () => { await expectSameSet(["Geplant"]); }); it("agrees for Ausgetreten", async () => { await expectSameSet(["Ausgetreten"]); }); it("returns everyone when no status is selected", async () => { const total = await db .selectFrom("employees") .select(({ fn }) => fn.countAll().as("anzahl")) .executeTakeFirstOrThrow(); const filtered = await idsFromDatabase([]); expect(filtered.size).toBe(Number(total.anzahl)); }); });