Files
erika/lib/share/validation.test.ts
T

99 lines
3.4 KiB
TypeScript

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}$/);
});
});