diff --git a/artifacts/api-server/tests/executive-meetings-swap-times.test.mjs b/artifacts/api-server/tests/executive-meetings-swap-times.test.mjs index d8347d2f..4b5ea2ad 100644 --- a/artifacts/api-server/tests/executive-meetings-swap-times.test.mjs +++ b/artifacts/api-server/tests/executive-meetings-swap-times.test.mjs @@ -354,6 +354,19 @@ test("swap-times: viewer (no mutate role) → 403 forbidden", async () => { assert.equal(res.status, 403, "viewer must not be able to swap times"); }); +test("swap-times: malformed expectedUpdatedAt → 400 from zod, not 500", async () => { + // The route passes expectedUpdatedAt straight to new Date(...).toISOString(). + // Without a strict zod guard a non-ISO string would throw and surface as a + // 500. Lock the controlled-failure contract here. + const res = await api(cookieA, "POST", "/api/executive-meetings/swap-times", { + aId: 1, + bId: 2, + expectedUpdatedAtA: "not-a-real-timestamp", + expectedUpdatedAtB: new Date().toISOString(), + }); + assert.equal(res.status, 400, "malformed timestamp must be rejected at validation"); +}); + test("swap-times: meeting without a time window → 400 no_time_window", async () => { // Create A normally, then null out B's start/end directly so we can // exercise the guard without going through PATCH (which validates). diff --git a/lib/api-zod/src/manual.ts b/lib/api-zod/src/manual.ts index 1616a61f..d15bbd41 100644 --- a/lib/api-zod/src/manual.ts +++ b/lib/api-zod/src/manual.ts @@ -17,8 +17,12 @@ export type ExecutiveMeetingsReorderBodyT = z.infer< export const ExecutiveMeetingsSwapTimesBody = z.object({ aId: z.number().int().positive(), bId: z.number().int().positive(), - expectedUpdatedAtA: z.string().min(1), - expectedUpdatedAtB: z.string().min(1), + // Must be ISO-8601 datetime strings — the route passes them straight + // to `new Date(...).toISOString()` and any malformed input would + // bubble up as an uncontrolled 500. z.string().datetime() rejects + // garbage at the validation boundary so the client gets a 400. + expectedUpdatedAtA: z.string().datetime(), + expectedUpdatedAtB: z.string().datetime(), }); export type ExecutiveMeetingsSwapTimesBodyT = z.infer<