test: Demodaten und Unit- sowie PostgreSQL-Tests ergänzen
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
1 parent
c1513c040b
commit
ef52cc1218
3 files changed
+519
No files matched your search
@@ -0,0 +1,138 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { localClock, localInstant, pickupState } from "../src/lib/time";
|
||||
import { childSchema, correctionSchema, daySchema, overrideSchema, aloneRulesSchema, contactSchema, slotSchema, applicationSchema } from "../src/lib/validation";
|
||||
import { hashPassword, verifyPassword } from "../src/lib/password";
|
||||
import { chooseWinners, invitationHash, newInvitation } from "../src/lib/admissions";
|
||||
|
||||
test("Abholfenster: genau jetzt und bis einschließlich 15 Minuten, überfällig erst davor", () => {
|
||||
assert.equal(pickupState(null, "14:00"), "none");
|
||||
assert.equal(pickupState("13:59", "14:00"), "overdue");
|
||||
assert.equal(pickupState("14:00", "14:00"), "due");
|
||||
assert.equal(pickupState("14:15", "14:00"), "due");
|
||||
assert.equal(pickupState("14:16", "14:00"), "later");
|
||||
assert.equal(pickupState("10:05", "09:59"), "due");
|
||||
});
|
||||
|
||||
test("Berliner Tag und Wochentag folgen der Zeitzone, einschließlich Sommerzeit", () => {
|
||||
const previous = process.env.APP_TIMEZONE;
|
||||
process.env.APP_TIMEZONE = "Europe/Berlin";
|
||||
try {
|
||||
assert.deepEqual(localClock(new Date("2026-11-02T23:30:00Z")), { day: "2026-11-03", time: "00:30", weekday: 2 });
|
||||
assert.deepEqual(localClock(new Date("2026-07-05T22:30:00Z")), { day: "2026-07-06", time: "00:30", weekday: 1 });
|
||||
assert.deepEqual(localClock(new Date("2026-11-01T10:00:00Z")), { day: "2026-11-01", time: "11:00", weekday: 7 });
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.APP_TIMEZONE;
|
||||
else process.env.APP_TIMEZONE = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test("Nur echte Kalendertage werden akzeptiert", () => {
|
||||
for (const date of ["2026-02-29", "2026-02-30", "2026-13-01", "oops", "1999-12-31", "2101-01-01"]) {
|
||||
assert.equal(daySchema.safeParse(date).success, false, date);
|
||||
}
|
||||
assert.equal(daySchema.safeParse("2028-02-29").success, true);
|
||||
});
|
||||
|
||||
test("Kinder benötigen Namen und Klasse; Wochenzeiten sind eindeutig und chronologisch", () => {
|
||||
const child = { firstName: " Emma ", lastName: "Test", className: "2a", schedules: [{ weekday: 1, arrival: "12:00", pickup: "16:00" }] };
|
||||
assert.equal(childSchema.parse(child).firstName, "Emma");
|
||||
assert.equal(childSchema.safeParse({ ...child, firstName: " " }).success, false);
|
||||
assert.equal(childSchema.safeParse({ ...child, schedules: [child.schedules[0], child.schedules[0]] }).success, false);
|
||||
assert.equal(childSchema.safeParse({ ...child, schedules: [{ weekday: 1, arrival: "16:00", pickup: "12:00" }] }).success, false);
|
||||
assert.equal(childSchema.safeParse({ ...child, schedules: [{ weekday: 1, arrival: "24:00", pickup: "25:00" }] }).success, false);
|
||||
assert.equal(childSchema.safeParse({ ...child, schedules: [] }).success, true);
|
||||
});
|
||||
|
||||
test("Tagesabweichungen verlangen nur bei individuellen Zeiten eine gültige Zeitspanne", () => {
|
||||
const override = { childId: "623548a9-d792-4a46-9554-543fd7f938ae", day: "2026-11-03", mode: "custom", arrival: "12:00", pickup: "16:00" };
|
||||
assert.equal(overrideSchema.safeParse(override).success, true);
|
||||
assert.equal(overrideSchema.safeParse({ ...override, pickup: "11:00" }).success, false);
|
||||
assert.equal(overrideSchema.safeParse({ ...override, arrival: "", pickup: "" }).success, false);
|
||||
assert.equal(overrideSchema.safeParse({ ...override, mode: "absent", arrival: "", pickup: "" }).success, true);
|
||||
assert.equal(overrideSchema.safeParse({ ...override, mode: "weekly", arrival: "", pickup: "" }).success, true);
|
||||
});
|
||||
|
||||
test("Passwörter werden gesalzen gehasht und nur korrektes Passwort akzeptiert", async () => {
|
||||
const password = "test-only-password-not-a-credential";
|
||||
const hash = await hashPassword(password);
|
||||
assert.notEqual(hash, password);
|
||||
assert.notEqual(hash, await hashPassword(password));
|
||||
assert.equal(await verifyPassword(password, hash), true);
|
||||
assert.equal(await verifyPassword("wrong-password", hash), false);
|
||||
assert.equal(await verifyPassword(password, "broken"), false);
|
||||
});
|
||||
|
||||
test("Korrekturen benötigen Grund, passenden Status und chronologische Zeiten", () => {
|
||||
const value = { childId: "623548a9-d792-4a46-9554-543fd7f938ae", day: "2026-11-02",
|
||||
status: "picked_up", arrival: "12:00", pickup: "15:00", pickupKind: "alone",
|
||||
reason: "Abholung vergessen", expectedVersion: "1" };
|
||||
assert.equal(correctionSchema.safeParse(value).success, true);
|
||||
for (const change of [{ reason: " " }, { pickup: "11:00" }, { arrival: "" },
|
||||
{ status: "sick" }, { status: "arrived" }, { status: "expected" }, { expectedVersion: "x" }]) {
|
||||
assert.equal(correctionSchema.safeParse({ ...value, ...change }).success, false, JSON.stringify(change));
|
||||
}
|
||||
assert.equal(correctionSchema.safeParse({ ...value, status: "expected", arrival: "", pickup: "", expectedVersion: null }).success, true);
|
||||
});
|
||||
|
||||
test("Abholkontakte und eindeutige Alleingehregeln werden validiert", () => {
|
||||
const childId = "623548a9-d792-4a46-9554-543fd7f938ae";
|
||||
assert.equal(contactSchema.safeParse({ childId, name: "Testperson", relationship: "Oma", phone: "", notes: "", authorized: false }).success, true);
|
||||
assert.equal(aloneRulesSchema.safeParse({ childId, rules: [] }).success, true);
|
||||
assert.equal(aloneRulesSchema.safeParse({ childId, rules: [{ weekday: 1, earliest: "15:00" }, { weekday: 1, earliest: "16:00" }] }).success, false);
|
||||
assert.equal(aloneRulesSchema.safeParse({ childId, rules: [{ weekday: 8, earliest: "15:00" }] }).success, false);
|
||||
});
|
||||
|
||||
test("Korrekturzeiten folgen Berlin und lehnen mehrdeutige/nicht existente DST-Zeiten ab", () => {
|
||||
assert.equal(localInstant("2026-11-02", "15:10", "Europe/Berlin").toISOString(), "2026-11-02T14:10:00.000Z");
|
||||
assert.equal(localInstant("2026-07-02", "15:10", "Europe/Berlin").toISOString(), "2026-07-02T13:10:00.000Z");
|
||||
assert.throws(() => localInstant("2026-03-29", "02:30", "Europe/Berlin"), /Zeitumstellung/);
|
||||
assert.throws(() => localInstant("2026-10-25", "02:30", "Europe/Berlin"), /Zeitumstellung/);
|
||||
});
|
||||
|
||||
test("Abholslots werden erst nach dem Fenster überfällig, Vorlauf einschließlich 15 Minuten", () => {
|
||||
assert.equal(pickupState("15:00", "14:44", "15:15"), "later");
|
||||
assert.equal(pickupState("15:00", "14:45", "15:15"), "due");
|
||||
assert.equal(pickupState("15:00", "15:00", "15:15"), "due");
|
||||
assert.equal(pickupState("15:00", "15:10", "15:15"), "due");
|
||||
assert.equal(pickupState("15:00", "15:15", "15:15"), "due");
|
||||
assert.equal(pickupState("15:00", "15:16", "15:15"), "overdue");
|
||||
assert.equal(slotSchema.safeParse({ start: "15:00", end: "15:15" }).success, true);
|
||||
assert.equal(slotSchema.safeParse({ start: "15:00", end: "15:00" }).success, false);
|
||||
assert.equal(slotSchema.safeParse({ start: "15:00", end: "14:00" }).success, false);
|
||||
});
|
||||
|
||||
test("Auslosung wählt ohne Zurücklegen exakt X und verändert Eingaben nicht", () => {
|
||||
const candidates = ["a", "b", "c", "d"];
|
||||
assert.deepEqual(chooseWinners(candidates, 2, (min) => min), ["a", "b"]);
|
||||
assert.deepEqual(chooseWinners(candidates, 2, (_min, max) => max - 1), ["d", "a"]);
|
||||
assert.deepEqual(candidates, ["a", "b", "c", "d"]);
|
||||
assert.equal(new Set(chooseWinners(candidates, 4)).size, 4);
|
||||
for (const places of [0, -1, 5, 1.5, NaN]) assert.throws(() => chooseWinners(candidates, places));
|
||||
assert.throws(() => chooseWinners([], 1));
|
||||
});
|
||||
|
||||
test("Alle Zweierauswahlen aus drei Kindern sind im Auswahlalgorithmus gleichberechtigt", () => {
|
||||
const counts = new Map<string, number>();
|
||||
for (let first = 0; first < 3; first++) {
|
||||
for (let second = 1; second < 3; second++) {
|
||||
let step = 0;
|
||||
const selected = chooseWinners(["a", "b", "c"], 2, () => step++ === 0 ? first : second).sort().join("");
|
||||
counts.set(selected, (counts.get(selected) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
assert.deepEqual([...counts.entries()].sort(), [["ab", 2], ["ac", 2], ["bc", 2]]);
|
||||
});
|
||||
|
||||
test("Einladungscode ist zufällig und Elternanmeldung benötigt vollständige Angaben und Bestätigung", () => {
|
||||
const code = newInvitation();
|
||||
assert.match(code, /^[a-f0-9]{48}$/);
|
||||
assert.notEqual(newInvitation(), code);
|
||||
assert.notEqual(invitationHash(code), code);
|
||||
const application = { code, firstName: " Test ", lastName: "Anmeldung", className: "1a",
|
||||
parentName: "Testkontakt", email: "DEMO@example.invalid", notes: "", consent: true };
|
||||
assert.equal(applicationSchema.parse(application).email, "demo@example.invalid");
|
||||
assert.equal(applicationSchema.parse(application).firstName, "Test");
|
||||
assert.equal(applicationSchema.safeParse({ ...application, consent: false }).success, false);
|
||||
assert.equal(applicationSchema.safeParse({ ...application, code: "short" }).success, false);
|
||||
});
|
||||
Reference in new issue
Block a user