import assert from "node:assert/strict"; import { test } from "node:test"; import type { Payload, PayloadRequest } from "payload"; import type { BookingRequest, Event, MeetingSetting } from "../payload-types"; import { customerBookingView } from "../lib/booking/customerView"; import { authorizeMeeting, meetingAccessVersion, meetingWindow } from "../lib/meeting/access"; import { createMeetingToken, verifyMeetingToken } from "../lib/meeting/token"; import { createLiveKitToken, getLiveKitRoomName } from "../lib/meeting/livekit"; import { allowVerificationEmail } from "../lib/auth/verificationRateLimit"; import { safeLoginRedirect } from "../lib/auth/redirect"; import { parseBerlinDate, berlinDayBounds } from "../lib/calendar/timezone"; import { getAvailableSlots } from "../lib/booking/slots"; import { handleConfirmation } from "../collections/BookingRequests"; import { Events } from "../collections/Events"; import { AvailabilityOverrides } from "../collections/AvailabilityOverrides"; process.env.MEETING_SESSION_SECRET = "test-only-meeting-secret"; process.env.LIVEKIT_API_KEY = "test-api-key"; process.env.LIVEKIT_API_SECRET = "test-only-livekit-secret-long-enough"; const settings = { hostJoinWindowMinutes: 30, participantJoinWindowMinutes: 10, meetingCloseAfterMinutes: 10 } as MeetingSetting; const event = { id: 1, slug: "private-call", title: "Fixture", date: "2027-07-01T10:00:00.000Z", startTime: "2027-07-01T10:00:00.000Z", endTime: "2027-07-01T11:00:00.000Z", isOnline: true, _status: "published", meetingPassword: "fixture" } as Event; test("customer props exclude private fields, even inside populated relationships", () => { const booking = { id: 1, internalNote: "SECRET_NOTE", user: { emailVerificationTokenHash: "SECRET_HASH" }, offer: { title: "Consultation", internalNote: "SECRET_OFFER" }, linkedEvent: { slug: "private-call", isOnline: true, meetingPassword: "SECRET_PASSWORD", bookingRequest: { internalNote: "SECRET_NESTED" } }, proposedAlternative: { date: "2027-07-01", internalNote: "SECRET_ALTERNATIVE" }, } as unknown as BookingRequest; const serialized = JSON.stringify(customerBookingView(booking)); assert.ok(!serialized.includes("SECRET")); assert.ok(serialized.includes("Consultation")); assert.ok(serialized.includes("private-call")); }); test("closed and malformed meeting windows never grant access", () => { assert.equal(meetingWindow(event, settings, "participant", new Date("2027-07-01T11:10:00Z")).allowed, false); assert.equal(meetingWindow({ ...event, endTime: "invalid" }, settings, "participant").allowed, false); assert.equal(meetingWindow(event, settings, "participant", new Date("2027-07-01T09:49:59Z")).allowed, false); assert.equal(meetingWindow(event, settings, "participant", new Date("2027-07-01T10:00:00Z")).allowed, true); }); test("cancelled private meetings are rejected using the current booking status", async () => { let queried = false; const payload = { find: async (args: { where: unknown }) => { queried = true; assert.ok(JSON.stringify(args.where).includes('"confirmed"')); return { docs: [] }; } } as unknown as Payload; assert.equal(await authorizeMeeting(payload, { ...event, isPrivateBooking: true }, "participant"), null); assert.equal(queried, true); }); test("password and schedule changes invalidate credentials and isolate the new room", () => { const version = meetingAccessVersion(event); const changed = meetingAccessVersion({ ...event, meetingPassword: "replacement" }); assert.notEqual(version, changed); assert.notEqual(version, meetingAccessVersion({ ...event, endTime: "2027-07-01T12:00:00Z" })); assert.notEqual(getLiveKitRoomName(event.slug!, version), getLiveKitRoomName(event.slug!, changed)); const issued = createMeetingToken({ eventSlug: event.slug!, name: "Guest", role: "participant", accessVersion: version }); assert.equal(verifyMeetingToken(issued.token)?.accessVersion, version); const expired = createMeetingToken({ eventSlug: event.slug!, name: "Guest", role: "participant", ttlMs: 0 }); assert.equal(verifyMeetingToken(expired.token), null); }); test("LiveKit connection credentials expire within the allowed window and one minute", async () => { const deadline = Date.now() + 30000; const jwt = await createLiveKitToken({ eventSlug: "fixture", participantId: "guest", name: "Guest", role: "participant", expiresAt: deadline }); const claims = JSON.parse(Buffer.from(jwt.split(".")[1], "base64url").toString()); assert.ok(claims.exp * 1000 <= deadline); assert.ok(claims.exp - claims.nbf <= 60); assert.equal(claims.video.roomAdmin, undefined); }); test("verification budgets apply across operations, address casing and changing IPs", () => { for (let i = 0; i < 3; i++) assert.equal(allowVerificationEmail({ headers: new Headers({ "x-forwarded-for": `fixture-${i}` }) }, "Target@example.com"), true); assert.equal(allowVerificationEmail({ headers: new Headers({ "x-forwarded-for": "fixture-new" }) }, "target@EXAMPLE.com"), false); for (let i = 0; i < 3; i++) assert.equal(allowVerificationEmail({ headers: new Headers({ "x-forwarded-for": `account-${i}` }) }, `target${i}@example.com`, 17), true); assert.equal(allowVerificationEmail({ headers: new Headers({ "x-forwarded-for": "account-new" }) }, "fresh@example.com", 17), false); }); test("login rejects external, script and backslash redirects", () => { for (const value of ["javascript:alert(1)", "https://other.invalid", "//other.invalid", "/\\other.invalid", "/\n/other.invalid", null]) assert.equal(safeLoginRedirect(value), "/konto"); assert.equal(safeLoginRedirect("/angebote/beratung?x=1"), "/angebote/beratung?x=1"); }); test("slot date parsing uses Berlin boundaries and rejects impossible calendar dates", () => { assert.equal(parseBerlinDate("2027-07-01")?.toISOString(), "2027-06-30T22:00:00.000Z"); assert.equal(parseBerlinDate("2027-02-30"), null); assert.equal(parseBerlinDate("2027-7-1"), null); assert.equal(berlinDayBounds(parseBerlinDate("2027-03-28")!).end.getTime() - parseBerlinDate("2027-03-28")!.getTime() + 1, 23 * 3600000); }); test("invalid slot durations terminate safely without querying the database", async () => { const payload = { find: () => { throw new Error("Unexpected query"); } } as unknown as Payload; for (const durationMinutes of [0, -1, NaN, Infinity, 0.5]) { assert.equal((await getAvailableSlots(payload, { durationMinutes, from: new Date(), to: new Date() })).size, 0); } }); test("public users cannot read absence reasons", () => { const read = AvailabilityOverrides.access!.read!; assert.equal(read({ req: { user: null } } as Parameters[0]), false); }); function hookArgs(data: Record, originalDoc: Record, payload: Record) { return { data, originalDoc, req: { transactionID: "fixture", payload: { ...payload, db: { sessions: { fixture: { db: { execute: async () => undefined } } } }, } } as unknown as PayloadRequest } as Parameters[0]; } test("rescheduling a confirmed booking checks conflicts instead of silently overlapping", async () => { const original = { id: 1, status: "confirmed", appointmentType: "onsite", date: event.date, startTime: event.startTime, endTime: event.endTime }; await assert.rejects(() => handleConfirmation(hookArgs({ startTime: "2027-07-01T10:30:00Z" }, original, { count: async () => ({ totalDocs: 1 }), })), { status: 409 }); }); test("cancelling a private booking disables its event", async () => { let updated = false; await handleConfirmation(hookArgs({ status: "cancelled" }, { id: 1, status: "confirmed", appointmentType: "online", date: event.date, startTime: event.startTime, endTime: event.endTime, linkedEvent: 2 }, { findByID: async () => ({ ...event, id: 2, isPrivateBooking: true }), update: async (args: { data: { isOnline: boolean } }) => { assert.equal(args.data.isOnline, false); updated = true; }, })); assert.equal(updated, true); }); test("repeated bookings for one offer get distinct private meeting slugs", async () => { const slugs: string[] = []; const payload = { count: async () => ({ totalDocs: 0 }), findByID: async () => ({ title: "Same offer" }), create: async (args: { data: { slug: string } }) => { slugs.push(args.data.slug); return { id: slugs.length }; }, }; for (let i = 0; i < 2; i++) await handleConfirmation(hookArgs({ status: "confirmed", offer: 1, appointmentType: "online", date: event.date, startTime: event.startTime, endTime: event.endTime }, {}, payload)); assert.equal(new Set(slugs).size, 2); }); test("ordinary event edits preserve the password; explicit regeneration changes it", async () => { const hook = Events.hooks!.beforeChange![0]; const unchanged = await hook({ data: { isOnline: true }, originalDoc: event } as unknown as Parameters[0]); assert.equal(unchanged.meetingPassword, undefined); const changed = await hook({ data: { regeneratePassword: true }, originalDoc: event } as unknown as Parameters[0]); assert.ok(changed.meetingPassword); assert.notEqual(changed.meetingPassword, event.meetingPassword); });