import { describe, expect, test } from "bun:test"; import { createShareId } from "@/db/schema/share"; import { ShareHttpError } from "@/lib/share/http-error"; import { MAX_SHARE_COMMENT_LENGTH, MAX_SHARE_DESCRIPTION_LENGTH, MAX_SHARE_TEXT_BYTES, } from "@/lib/share/limits"; import { validateOptionalImage, validateShareComment, validateShareDescription, validateShareText, validateTextFile, } from "@/lib/share/validation"; describe("share validation", () => { test("preserves exact text while rejecting blank content", () => { const content = " first line\r\n\tsecond line \n"; expect(validateShareText(content)).toBe(content); try { validateShareText(" \n\t "); throw new Error("Expected blank text to fail"); } catch (error) { expect(error).toBeInstanceOf(ShareHttpError); expect((error as ShareHttpError).status).toBe(400); } expect(() => validateShareText("before\0after")).toThrow("NUL"); }); test("measures the text limit in UTF-8 bytes", () => { expect(validateShareText("a".repeat(MAX_SHARE_TEXT_BYTES))).toHaveLength( MAX_SHARE_TEXT_BYTES ); expect(() => validateShareText("ก".repeat(Math.ceil(MAX_SHARE_TEXT_BYTES / 3) + 1))) .toThrow("5 MiB"); }); test("enforces description and comment character limits", () => { expect(validateShareDescription("")).toBeNull(); expect(validateShareDescription("d".repeat(MAX_SHARE_DESCRIPTION_LENGTH))) .toHaveLength(MAX_SHARE_DESCRIPTION_LENGTH); expect(() => validateShareDescription("d".repeat(MAX_SHARE_DESCRIPTION_LENGTH + 1)) ).toThrow("5,000"); expect(validateShareComment(" comment ")).toBe(" comment "); expect(() => validateShareComment(" ")).toThrow("blank"); expect(() => validateShareComment("c".repeat(MAX_SHARE_COMMENT_LENGTH + 1)) ).toThrow("2,000"); }); test("accepts only non-empty UTF-8 .txt files", async () => { const content = "สวัสดี\nexact whitespace "; await expect( validateTextFile(new File([content], "message.txt", { type: "text/plain" })) ).resolves.toBe(content); await expect( validateTextFile(new File([content], "message.log", { type: "text/plain" })) ).rejects.toMatchObject({ status: 415 }); await expect( validateTextFile( new File([new Uint8Array([0xc3, 0x28])], "broken.txt", { type: "text/plain", }) ) ).rejects.toMatchObject({ status: 400 }); await expect( validateTextFile( new File(["before\0after"], "nul.txt", { type: "text/plain" }) ) ).rejects.toMatchObject({ status: 400 }); }); test("validates image contents instead of trusting the MIME type", async () => { const pngHeader = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]); await expect( validateOptionalImage( new File([pngHeader], "image.png", { type: "image/png" }) ) ).resolves.toMatchObject({ type: "image/png", size: pngHeader.length }); await expect( validateOptionalImage( new File(["not an image"], "image.png", { type: "image/png" }) ) ).rejects.toMatchObject({ status: 415 }); }); }); describe("share IDs", () => { test("creates collision-resistant eight-character public IDs", () => { const ids = new Set(Array.from({ length: 1_000 }, createShareId)); expect(ids.size).toBe(1_000); for (const id of ids) expect(id).toMatch(/^[a-z0-9]{8}$/); }); });