import { describe, expect, it } from "vitest"; import { decodeCursor, encodeCursor, MAX_COMMENT_IMAGE_BYTES, parseCommentForm, parseTarget } from "./validation"; const id = "11111111-1111-4111-8111-111111111111"; function form(text = "hello", count = 0, bytes = 1, type = "image/png") { const value = new FormData(); value.set("text", text); for (let i = 0; i < count; i++) value.append("image", new File([new Uint8Array(bytes)], `${i}.png`, { type })); return value; } describe("comment form limits", () => { it("accepts text and image-only comments", () => { expect(parseCommentForm(form(" hello ")).text).toBe("hello"); expect(parseCommentForm(form("", 5)).files).toHaveLength(5); expect(() => parseCommentForm(form(""))).toThrow("empty-comment"); }); it("enforces the combined retained and new image limit", () => { expect(() => parseCommentForm(form("", 6))).toThrow("invalid-comment"); const value = form("editing", 5); value.set("version", "1"); value.append("keepImageId", id); expect(() => parseCommentForm(value, true)).toThrow("invalid-comment"); }); it("accepts exactly ten MiB and rejects one additional byte", () => { expect(parseCommentForm(form("", 1, MAX_COMMENT_IMAGE_BYTES)).files).toHaveLength(1); expect(() => parseCommentForm(form("", 1, MAX_COMMENT_IMAGE_BYTES + 1))).toThrow("image-too-large"); }); it.each(["video/mp4", "image/svg+xml", "image/gif", "application/octet-stream"])("rejects %s", (type) => { expect(() => parseCommentForm(form("", 1, 1, type))).toThrow("unsupported-image-type"); }); it("enforces text, version, reply, and retained-image validation", () => { expect(() => parseCommentForm(form("x".repeat(4001)))).toThrow("invalid-comment"); const value = form(); value.set("version", "0"); expect(() => parseCommentForm(value, true)).toThrow("invalid-comment"); value.set("version", "1"); value.set("replyToId", "bad"); expect(() => parseCommentForm(value, true)).toThrow("invalid-comment"); value.delete("replyToId"); value.append("keepImageId", id); value.append("keepImageId", id); expect(() => parseCommentForm(value, true)).toThrow("invalid-comment"); }); }); describe("discussion identities and cursors", () => { it("validates guide UUIDs and schedule IDs", () => { expect(parseTarget(`guide:${id}`)).toEqual({ kind: "guide", id }); expect(parseTarget("stygian:5269001")).toEqual({ kind: "stygian", id: 5269001 }); for (const value of ["guide:invalid", "stygian:0", "stygian:NaN", "stygian:-1", "stygian:9999999999"]) expect(() => parseTarget(value)).toThrow("invalid-target"); }); it("round-trips keyset cursors and rejects arbitrary input", () => { const cursor = { time: "2026-10-07T00:00:00.000Z", id, likes: 3 }; expect(decodeCursor(encodeCursor(cursor))).toEqual(cursor); expect(() => decodeCursor("invalid")).toThrow("invalid-cursor"); expect(() => decodeCursor(encodeCursor({ ...cursor, likes: -1 }))).toThrow("invalid-cursor"); }); });