test: cover share validation and migrations

This commit is contained in:
2026-08-16 01:00:24 +07:00 Unverified
parent 9617d67c7c
commit bcda6664e3
2 changed files with 111 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, test } from "bun:test";
describe("share migration", () => {
test("backfills Discord IDs before creating the unique index", async () => {
const migration = await Bun.file(
new URL("../../drizzle/0018_authenticated_share.sql", import.meta.url)
).text();
const backfill = migration.indexOf('SET "discord_id"');
const uniqueIndex = migration.indexOf(
'CREATE UNIQUE INDEX "user_discord_id_unique"'
);
expect(backfill).toBeGreaterThan(-1);
expect(uniqueIndex).toBeGreaterThan(backfill);
expect(migration).toContain('CREATE TABLE "share"."text"');
expect(migration).toContain('CREATE TABLE "share"."comment"');
expect(migration).toContain("ON DELETE cascade");
});
});
+92
View File
@@ -0,0 +1,92 @@
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}$/);
});
});