test: cover share validation and migrations
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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}$/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user