93 lines
3.2 KiB
TypeScript
93 lines
3.2 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(422);
|
|
}
|
|
});
|
|
|
|
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: 422 });
|
|
});
|
|
|
|
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}$/);
|
|
});
|
|
});
|