import { DummyDriver, Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler } from "kysely"; import { describe, expect, it } from "vitest"; import type { Schema } from "@/lib/db/schema"; import { baueEntwuerfe, entwuerfeAbfrage, type EntwurfZeile } from "@/lib/entwuerfe"; // Wessen Entwürfe jemand sieht — und, wichtiger, an welchen die Schaltflächen // hängen. Die Regeln in der Datenbank sind die Grenze; hier wird gelesen, was // die Abfrage daraus macht, weil ein Fehler darin nicht auffiele: die Karte // zeigte eine Liste, nur die falsche. const db = new Kysely({ dialect: { createAdapter: () => new PostgresAdapter(), createDriver: () => new DummyDriver(), createIntrospector: (d) => new PostgresIntrospector(d), createQueryCompiler: () => new PostgresQueryCompiler(), }, }); const ICH = "11111111-1111-1111-1111-111111111111"; const ANDERE = "22222222-2222-2222-2222-222222222222"; // entwuerfeAbfrage erwartet den Ausdrucksbauer aus selectNoFrom — genau so // hängt lib/dashboard-data.ts sie ein. function abfrage(userId = ICH) { return db.selectNoFrom((eb) => [entwuerfeAbfrage(eb, userId).as("x")]).compile(); } const text = (userId?: string) => abfrage(userId).sql.replace(/\s+/g, " "); describe("entwuerfeAbfrage", () => { it("zeigt die eigenen Entwürfe", () => { expect(text()).toContain('"d"."created_by" = $'); }); it("holt dazu, wer hinzugewählt wurde", () => { // Vorzeichen und Tabelle zusammen: ein `not exists` kehrte die Bedeutung // um, ohne dass ein Wort sich änderte. const s = text(); expect(s).toContain("exists"); expect(s).not.toContain("not exists"); expect(s).toContain("from colleague_subscriptions s"); expect(s).toContain('s.author_user_id = "d"."created_by"'); }); it("verknüpft eigene und hinzugewählte mit ODER, nicht mit UND", () => { // Mit UND bliebe die Liste immer leer: kein Entwurf ist gleichzeitig von // mir und von jemandem, den ich hinzugewählt habe. expect(text()).toMatch(/"d"\."created_by" = \$\d+ or exists/); }); it("holt den Namen der verfassenden Person dazu", () => { // Ohne ihn stünde an einem fremden Entwurf nur, dass er fremd ist. const s = text(); expect(s).toContain('left join "profiles" as "p" on "p"."id" = "d"."created_by"'); expect(s).toContain('"p"."full_name" as "author_name"'); expect(s).toContain('"p"."email" as "author_email"'); }); it("stellt die eigenen Entwürfe nach vorn", () => { // Vor der Freigabe standen dort nur die eigenen; wer seinen halbfertigen // Entwurf sucht, soll ihn nicht zwischen fremden suchen. const s = text(); expect(s).toMatch(/order by case when "d"\."created_by" = \$\d+ then 0 else 1 end/); expect(s).toContain('"d"."updated_at" desc'); }); it("bindet die Kennung als Parameter, nicht in den Text", () => { const { sql: roh, parameters } = abfrage("bösartig'; drop table hire_drafts; --"); expect(roh).not.toContain("drop table"); expect(parameters).toContain("bösartig'; drop table hire_drafts; --"); }); it("formatiert den Zeitstempel innerhalb von JSON ausdrücklich", () => { // In JSON gibt Postgres ihn anders aus als der Treiber es täte, und der // Unterschied fällt erst beim Vergleichen zweier Listen auf. expect(text()).toContain("to_char("); }); }); describe("baueEntwuerfe", () => { function zeile(teil: Partial = {}): EntwurfZeile { return { id: "d1", step: 2, payload: { firstName: "Manuel", lastName: "Aigner" }, updated_at: "2026-09-09T08:00:00.000Z", created_by: ANDERE, author_name: "Anna Berger", author_email: "a@example.test", ...teil, }; } it("erkennt den eigenen Entwurf", () => { const [e] = baueEntwuerfe([zeile({ created_by: ICH })], ICH); expect(e.vonMir).toBe(true); }); it("nennt bei eigenen Entwürfen keinen Verfasser", () => { // Der eigene Name stünde an jeder Zeile und sagte nichts. const [e] = baueEntwuerfe([zeile({ created_by: ICH })], ICH); expect(e.autor).toBeNull(); }); it("nennt bei fremden Entwürfen den Namen", () => { const [e] = baueEntwuerfe([zeile()], ICH); expect(e.vonMir).toBe(false); expect(e.autor).toBe("Anna Berger"); }); it("fällt ohne Namen auf die E-Mail zurück", () => { // Auch ein Name aus Leerzeichen zählt als keiner. expect(baueEntwuerfe([zeile({ author_name: " " })], ICH)[0].autor).toBe("a@example.test"); expect(baueEntwuerfe([zeile({ author_name: null })], ICH)[0].autor).toBe("a@example.test"); }); it("nennt jemanden, auch wenn beides fehlt", () => { // Ein Entwurf ohne jede Zuordnung wäre einer, bei dem niemand weiss, wen // er fragen soll. expect(baueEntwuerfe([zeile({ author_name: null, author_email: null })], ICH)[0].autor).toBe("Unbekannt"); }); it("hält ohne Anmeldung nichts für eigen", () => { // created_by kann leer sein, userId auch. Beide leer heisst nicht „meiner" // — sonst hinge an einer herrenlosen Zeile eine Löschen-Schaltfläche. const [e] = baueEntwuerfe([zeile({ created_by: null })], null); expect(e.vonMir).toBe(false); }); it("reicht Inhalt und Zeitpunkt unverändert weiter", () => { const [e] = baueEntwuerfe([zeile()], ICH); expect(e).toMatchObject({ id: "d1", step: 2, updated_at: "2026-09-09T08:00:00.000Z" }); expect(e.payload).toEqual({ firstName: "Manuel", lastName: "Aigner" }); }); });