The picker in the bell now governs both lists, so note_subscriptions is renamed to colleague_subscriptions -- a name that only mentions notes would mislead the next reader. Reading and writing a draft now reach differently far. hire_drafts_owner (for all) is split into four policies: select lets in your own drafts and those of the people you added, while insert/update/delete stay with the owner. A draft is unfinished work with no lock and no history; two people writing into the same row would overwrite each other silently. That split forces a change in the actions: a policy does not reject a write, it lets it hit no rows. saveHireDraft and deleteHireDraft now read the row count instead of reporting success over a row that never changed. The card shows a foreign draft with its author and without Fortsetzen or Loeschen -- offering a button that reliably ends in a database error is a promise without cover. check-schema-types.mjs learns `alter table ... rename to`; without it the drift check reports one rename as two errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
124 lines
4.6 KiB
TypeScript
124 lines
4.6 KiB
TypeScript
import { DummyDriver, Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler } from "kysely";
|
|
import { describe, expect, it } from "vitest";
|
|
import type { Schema } from "@/lib/db/schema";
|
|
import { sichtbareNotizen } from "@/lib/notes";
|
|
import { baueKollegen } from "@/lib/shell-data";
|
|
|
|
// Wessen Notizen jemand sieht, ist die eine Stelle, an der ein Fehler nicht
|
|
// auffällt: die Glocke zeigt weiter eine Zahl, nur die falsche. Zu wenig, und
|
|
// eine Wiedervorlage bleibt liegen; zu viel, und die Einstellung wirkt nicht.
|
|
// Deshalb wird hier die Abfrage gelesen, die tatsächlich herauskommt.
|
|
|
|
const db = new Kysely<Schema>({
|
|
dialect: {
|
|
createAdapter: () => new PostgresAdapter(),
|
|
createDriver: () => new DummyDriver(),
|
|
createIntrospector: (d) => new PostgresIntrospector(d),
|
|
createQueryCompiler: () => new PostgresQueryCompiler(),
|
|
},
|
|
});
|
|
|
|
const ICH = "11111111-1111-1111-1111-111111111111";
|
|
|
|
function abfrage(userId = ICH) {
|
|
return db
|
|
.selectFrom("employee_notes as n")
|
|
.select("n.id")
|
|
.where(sichtbareNotizen(userId))
|
|
.compile();
|
|
}
|
|
|
|
const sql = (userId?: string) => abfrage(userId).sql.replace(/\s+/g, " ");
|
|
|
|
describe("sichtbareNotizen", () => {
|
|
it("lässt Notizen ohne Verfasser durch", () => {
|
|
// author_user_id kann leer sein. Eine Notiz auszublenden, weil niemand
|
|
// weiss, von wem sie ist, wäre genau der stille Verlust, den die Liste
|
|
// verhindern soll.
|
|
expect(sql()).toContain('"n"."author_user_id" is null');
|
|
});
|
|
|
|
it("lässt die eigenen Notizen immer durch", () => {
|
|
expect(sql()).toContain('"n"."author_user_id" = $');
|
|
});
|
|
|
|
it("holt dazu, wer ausdrücklich hinzugewählt wurde", () => {
|
|
// Vorzeichen und Tabelle zusammen: ein `not exists` auf derselben
|
|
// Tabelle kehrte die Bedeutung um, ohne dass ein Wort sich änderte.
|
|
const s = sql();
|
|
expect(s).toContain("exists");
|
|
expect(s).not.toContain("not exists");
|
|
expect(s).toContain("from colleague_subscriptions s");
|
|
expect(s).toContain("s.user_id = $");
|
|
expect(s).toContain("s.author_user_id = \"n\".\"author_user_id\"");
|
|
});
|
|
|
|
it("verknüpft die drei Fälle mit ODER, nicht mit UND", () => {
|
|
// Mit UND sähe niemand mehr etwas: keine Notiz ist gleichzeitig ohne
|
|
// Verfasser und von mir.
|
|
const s = sql();
|
|
expect(s).toMatch(/is null\s+or/);
|
|
expect(s).not.toMatch(/is null\s+and/);
|
|
});
|
|
|
|
it("bindet die Kennung als Parameter, nicht in den Text", () => {
|
|
// Sie kommt aus der Sitzung, nicht aus der Adresse — trotzdem hat sie im
|
|
// Abfragetext nichts verloren.
|
|
const { sql: text, parameters } = abfrage("bösartig'; drop table employee_notes; --");
|
|
expect(text).not.toContain("drop table");
|
|
expect(parameters).toContain("bösartig'; drop table employee_notes; --");
|
|
});
|
|
|
|
it("nennt die Kennung zweimal — für die eigenen Notizen und für die Ausnahmen", () => {
|
|
expect(abfrage().parameters.filter((p) => p === ICH)).toHaveLength(2);
|
|
});
|
|
|
|
it("lässt sich auf einen anderen Aliasnamen setzen", () => {
|
|
// Die Übersicht bindet dieselbe Tabelle ein; käme sie je unter anderem
|
|
// Namen, muss die Regel mitkönnen statt kopiert zu werden.
|
|
const s = db
|
|
.selectFrom("employee_notes as notiz")
|
|
.select("notiz.id")
|
|
.where(sichtbareNotizen(ICH, "notiz.author_user_id"))
|
|
.compile().sql;
|
|
expect(s).toContain('"notiz"."author_user_id"');
|
|
});
|
|
});
|
|
|
|
describe("baueKollegen", () => {
|
|
const leute = [
|
|
{ id: "a", full_name: "Anna Berger", email: "a@example.test" },
|
|
{ id: "b", full_name: null, email: "b@example.test" },
|
|
{ id: "c", full_name: " ", email: "c@example.test" },
|
|
];
|
|
|
|
it("lässt ohne Auswahl alle Haken leer", () => {
|
|
// Die Vorgabe ist die eigene Person; wer mehr will, wählt hinzu.
|
|
expect(baueKollegen(leute, []).map((k) => k.sichtbar)).toEqual([false, false, false]);
|
|
});
|
|
|
|
it("setzt den Haken, wo ein Abo steht", () => {
|
|
expect(baueKollegen(leute, ["b"]).map((k) => [k.id, k.sichtbar])).toEqual([
|
|
["a", false],
|
|
["b", true],
|
|
["c", false],
|
|
]);
|
|
});
|
|
|
|
it("fällt ohne Namen auf die E-Mail zurück", () => {
|
|
// Ein Haken ohne Beschriftung wäre einer, von dem niemand weiss, wen er
|
|
// betrifft. Auch ein Name aus Leerzeichen zählt als keiner.
|
|
expect(baueKollegen(leute, []).map((k) => k.name)).toEqual([
|
|
"Anna Berger",
|
|
"b@example.test",
|
|
"c@example.test",
|
|
]);
|
|
});
|
|
|
|
it("kommt mit einem Abo zurecht, zu dem es niemanden mehr gibt", () => {
|
|
// Wer die Personalabteilung verlässt, verschwindet aus der Liste; die
|
|
// Zeile bleibt, bis der Fremdschlüssel sie räumt.
|
|
expect(baueKollegen(leute, ["längst-weg"])).toHaveLength(3);
|
|
});
|
|
});
|