diff --git a/src/app/api/measurements/__tests__/group-by-day.test.ts b/src/app/api/measurements/__tests__/group-by-day.test.ts index 50f43de51..f4c91885b 100644 --- a/src/app/api/measurements/__tests__/group-by-day.test.ts +++ b/src/app/api/measurements/__tests__/group-by-day.test.ts @@ -284,4 +284,23 @@ describe("GET /api/measurements — schema rejections (W10 reconcile)", () => { expect(prisma.measurement.findMany).not.toHaveBeenCalled(); }); + // v1.4.37 W10 H2 — `2026-02-30` is a YYYY-MM-DD string but not a + // real calendar date. `new Date()` silently overflows it to March + // 2, so the drill-down would point at the wrong day. The validator + // refine must reject it at parse time. + it("rejects impossible calendar dates on dayKey (2026-02-30)", async () => { + const res = await GET( + getRequest("type=ACTIVITY_STEPS&dayKey=2026-02-30"), + ); + expect(res.status).toBe(422); + expect(prisma.measurement.findMany).not.toHaveBeenCalled(); + }); + + it("rejects impossible month on dayKey (2026-13-01)", async () => { + const res = await GET( + getRequest("type=ACTIVITY_STEPS&dayKey=2026-13-01"), + ); + expect(res.status).toBe(422); + expect(prisma.measurement.findMany).not.toHaveBeenCalled(); + }); }); diff --git a/src/lib/validations/measurement.ts b/src/lib/validations/measurement.ts index 081541700..ec6ee64ee 100644 --- a/src/lib/validations/measurement.ts +++ b/src/lib/validations/measurement.ts @@ -299,6 +299,22 @@ export const listMeasurementsSchema = z dayKey: z .string() .regex(/^\d{4}-\d{2}-\d{2}$/, "dayKey must be YYYY-MM-DD") + // v1.4.37 W10 — a `YYYY-MM-DD` string can satisfy the regex while + // still being an impossible calendar date (`2026-02-30`, + // `2026-13-01`). `new Date("2026-02-30T00:00:00Z")` silently + // overflows to March 2, so the drill-down would return rows from + // a different day than the user asked for. The same helper feeds + // the admin drain route, so a malformed CLI invocation has the + // same blast radius. Reject the impossible shapes at the + // validator instead. + .refine( + (s) => { + const parsed = new Date(`${s}T00:00:00Z`); + if (Number.isNaN(parsed.getTime())) return false; + return s === parsed.toISOString().slice(0, 10); + }, + "dayKey must be a real calendar date (YYYY-MM-DD)", + ) .optional(), }) .refine(