diff --git a/bunfig.toml b/bunfig.toml deleted file mode 100644 index 8755352..0000000 --- a/bunfig.toml +++ /dev/null @@ -1,2 +0,0 @@ -[test] -preload = ["./test/setup.ts"] diff --git a/lib/admin-dashboard.test.ts b/lib/admin-dashboard.test.ts deleted file mode 100644 index f7bb049..0000000 --- a/lib/admin-dashboard.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { getDashboardDateWindows } from "@/lib/admin-dashboard"; - -describe("admin dashboard date windows", () => { - test("uses Bangkok-local midnight for today", () => { - const windows = getDashboardDateWindows( - new Date("2026-07-27T18:30:00.000Z") - ); - - expect(windows.todayStart.toISOString()).toBe("2026-07-27T17:00:00.000Z"); - expect(windows.tomorrowStart.toISOString()).toBe("2026-07-28T17:00:00.000Z"); - }); - - test("includes today and the previous six Bangkok calendar days", () => { - const windows = getDashboardDateWindows( - new Date("2026-07-28T05:00:00.000Z") - ); - - expect(windows.todayStart.toISOString()).toBe("2026-07-27T17:00:00.000Z"); - expect(windows.last7DaysStart.toISOString()).toBe( - "2026-07-21T17:00:00.000Z" - ); - }); -}); diff --git a/lib/admin-form-ux.test.ts b/lib/admin-form-ux.test.ts deleted file mode 100644 index fcd6fb2..0000000 --- a/lib/admin-form-ux.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { - isValidDiscordWebhookUrl, - previewWebhookTemplate, -} from "@/lib/discord-webhook-settings"; -import { - parseResultFilters, - resultFiltersToSearchParams, -} from "@/lib/admin-form-results"; - -describe("Discord webhook settings", () => { - test("accepts only Discord webhook endpoints", () => { - expect( - isValidDiscordWebhookUrl( - "https://discord.com/api/webhooks/123/secret-token" - ) - ).toBe(true); - expect(isValidDiscordWebhookUrl("https://example.com/api/webhooks/1/2")).toBe( - false - ); - expect(isValidDiscordWebhookUrl("not a URL")).toBe(false); - }); - - test("previews known variables without removing unknown variables", () => { - expect( - previewWebhookTemplate("{username}: {1} {unknown}") - ).toBe("Example User: Example answer 1 {unknown}"); - expect(previewWebhookTemplate("{update}", { update: true })).toContain( - "Previous answer" - ); - }); -}); - -describe("response filters", () => { - test("normalizes unsupported and unsafe query values", () => { - expect( - parseResultFilters({ - page: "-5", - sort: "other", - direction: "sideways", - edited: "maybe", - dateFrom: "27/07/2026", - }) - ).toEqual({ - q: "", - page: 1, - sort: "submittedAt", - direction: "desc", - edited: "all", - dateFrom: "", - dateTo: "", - }); - }); - - test("preserves active filters when changing page", () => { - const filters = parseResultFilters({ - q: "Erika", - edited: "edited", - dateFrom: "2026-07-01", - }); - const params = new URLSearchParams( - resultFiltersToSearchParams(filters, { page: 2 }) - ); - expect(params.get("q")).toBe("Erika"); - expect(params.get("edited")).toBe("edited"); - expect(params.get("page")).toBe("2"); - }); -}); diff --git a/lib/cdn-images.test.ts b/lib/cdn-images.test.ts deleted file mode 100644 index 2056378..0000000 --- a/lib/cdn-images.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { - cdnImageResponse, - MAX_FORM_ANSWER_IMAGE_BYTES, - validateImageUploadFile, -} from "@/lib/cdn-images"; - -function fileLike( - overrides: Partial> = {} -): Pick { - return { - name: "answer.png", - type: "image/png", - size: 1024, - ...overrides, - }; -} - -describe("form answer image uploads", () => { - test("accepts supported images", () => { - expect(() => validateImageUploadFile(fileLike())).not.toThrow(); - }); - - test("rejects non-images, invalid extensions, and over-50MB files", () => { - expect(() => - validateImageUploadFile(fileLike({ type: "text/plain", name: "answer.txt" })) - ).toThrow("Only PNG, JPG, WebP, and GIF images are allowed"); - expect(() => - validateImageUploadFile(fileLike({ name: "answer.svg" })) - ).toThrow("Invalid image extension"); - expect(() => - validateImageUploadFile(fileLike({ size: MAX_FORM_ANSWER_IMAGE_BYTES + 1 })) - ).toThrow("Image must be 50MB or smaller"); - }); -}); - -describe("CDN image responses", () => { - test("returns stored bytes with immutable image headers", async () => { - const response = cdnImageResponse({ - data: Buffer.from([1, 2, 3]), - type: "image/png", - size: 3, - }); - - expect(response.headers.get("Content-Type")).toBe("image/png"); - expect(response.headers.get("Content-Length")).toBe("3"); - expect(response.headers.get("Cache-Control")).toBe( - "public, max-age=31536000, immutable" - ); - expect(response.headers.get("X-Content-Type-Options")).toBe("nosniff"); - expect([...new Uint8Array(await response.arrayBuffer())]).toEqual([1, 2, 3]); - }); -}); diff --git a/lib/discord.test.ts b/lib/discord.test.ts deleted file mode 100644 index 89f7114..0000000 --- a/lib/discord.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { afterEach, describe, expect, test } from "bun:test"; -import { addDiscordGuildMember } from "@/lib/discord"; - -const originalFetch = globalThis.fetch; -const originalBotToken = process.env.DISCORD_BOT_TOKEN; -const originalGuildId = process.env.DISCORD_GUILD_ID; - -afterEach(() => { - globalThis.fetch = originalFetch; - if (originalBotToken === undefined) { - delete process.env.DISCORD_BOT_TOKEN; - } else { - process.env.DISCORD_BOT_TOKEN = originalBotToken; - } - if (originalGuildId === undefined) { - delete process.env.DISCORD_GUILD_ID; - } else { - process.env.DISCORD_GUILD_ID = originalGuildId; - } -}); - -function mockFetch(handler: (input: RequestInfo | URL, init?: RequestInit) => Promise) { - globalThis.fetch = handler as unknown as typeof fetch; -} - -describe("Discord guild member join", () => { - test("adds a member with the OAuth access token", async () => { - process.env.DISCORD_BOT_TOKEN = "bot-token"; - process.env.DISCORD_GUILD_ID = "guild-1"; - - mockFetch(async (input, init) => { - expect(input).toBe("https://discord.com/api/v10/guilds/guild-1/members/user-1"); - expect(init?.method).toBe("PUT"); - expect(init?.headers).toEqual({ - Authorization: "Bot bot-token", - "Content-Type": "application/json", - }); - expect(init?.body).toBe(JSON.stringify({ access_token: "oauth-token" })); - - return new Response(null, { status: 204 }); - }); - - await expect(addDiscordGuildMember("user-1", "oauth-token")).resolves.toBeTrue(); - }); - - test("uses the configured Erika guild by default", async () => { - process.env.DISCORD_BOT_TOKEN = "bot-token"; - delete process.env.DISCORD_GUILD_ID; - - mockFetch(async (input) => { - expect(input).toBe("https://discord.com/api/v10/guilds/1302977523310923847/members/user-1"); - return new Response(null, { status: 201 }); - }); - - await expect(addDiscordGuildMember("user-1", "oauth-token")).resolves.toBeTrue(); - }); - - test("does not call Discord when tokens are missing", async () => { - delete process.env.DISCORD_BOT_TOKEN; - process.env.DISCORD_GUILD_ID = "guild-1"; - - let called = false; - mockFetch(async () => { - called = true; - return new Response(null, { status: 204 }); - }); - - await expect(addDiscordGuildMember("user-1", "oauth-token")).resolves.toBeFalse(); - expect(called).toBeFalse(); - }); -}); diff --git a/lib/form-answer-values.test.ts b/lib/form-answer-values.test.ts deleted file mode 100644 index 3c4e4d4..0000000 --- a/lib/form-answer-values.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { - answerCellValue, - decodeCheckboxAnswer, - encodeAnswerValue, - formatStoredAnswer, -} from "@/lib/form-answer-values"; - -describe("form answer values", () => { - test("round-trips checkbox choices containing commas", () => { - const choices = ["Alpha, beta", "Gamma"]; - const stored = encodeAnswerValue(choices); - - expect(decodeCheckboxAnswer(stored)).toEqual(choices); - expect(formatStoredAnswer(stored, "checkbox")).toBe("Alpha, beta, Gamma"); - }); - - test("continues to read legacy comma-separated checkbox answers", () => { - expect(decodeCheckboxAnswer("Alpha, Beta")).toEqual(["Alpha", "Beta"]); - }); - - test("does not decode JSON-looking text answers", () => { - expect(formatStoredAnswer('["plain text"]', "text")).toBe('["plain text"]'); - }); -}); - -test("formats CSV answer cells with image URLs", () => { - expect(answerCellValue("", "text", "img-1")).toBe("/cdn/img-1"); - expect(answerCellValue("Details", "text", "img-1")).toBe("Details\n/cdn/img-1"); -}); diff --git a/lib/leaderboard/discord.test.ts b/lib/leaderboard/discord.test.ts deleted file mode 100644 index ba81bf0..0000000 --- a/lib/leaderboard/discord.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { parseCurrentVoiceChannels } from "@/lib/leaderboard/discord"; - -describe("Discord current voice channels", () => { - test("keeps voice and Stage channels in stable position order", () => { - expect(parseCurrentVoiceChannels([ - { id: "text", name: "general", type: 0, position: 0 }, - { id: "stage", name: "Stage", type: 13, position: 3 }, - { id: "voice-b", name: "Beta", type: 2, position: 2 }, - { id: "voice-a", name: "Alpha", type: 2, position: 2 }, - ])).toEqual([ - { id: "voice-a", name: "Alpha", type: 2, position: 2 }, - { id: "voice-b", name: "Beta", type: 2, position: 2 }, - { id: "stage", name: "Stage", type: 13, position: 3 }, - ]); - }); - - test("rejects malformed top-level data", () => { - expect(() => parseCurrentVoiceChannels({ channels: [] })).toThrow(); - }); -}); diff --git a/lib/leaderboard/ranking.test.ts b/lib/leaderboard/ranking.test.ts deleted file mode 100644 index f30b4a9..0000000 --- a/lib/leaderboard/ranking.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { - buildVoiceLeaderboard, - buildXpLeaderboard, - formatInteger, - formatVoiceDuration, - normalizeRequestedPage, - shortenDiscordId, - type VoiceSourceRow, - type XpSourceRow, -} from "@/lib/leaderboard/ranking"; - -function xpRow( - userId: string, - totalXp: string, - level = 1, - displayName = userId, -): XpSourceRow { - return { - userId, - username: `${userId}-username`, - displayName, - level, - totalXp, - }; -} - -function voiceRow( - userId: string, - channelId: string, - totalSeconds: string, - displayName: string | null = userId, -): VoiceSourceRow { - return { - userId, - channelId, - totalSeconds, - displayName, - username: displayName ? `${userId}-username` : null, - }; -} - -describe("XP leaderboard", () => { - test("filters level zero and assigns shared competition ranks", () => { - const result = buildXpLeaderboard([ - xpRow("b", "900", 2, "Beta"), - xpRow("a", "1000", 3, "Alpha"), - xpRow("c", "900", 2, "Charlie"), - xpRow("hidden", "9999", 0), - xpRow("d", "800", 1, "Delta"), - ], 1); - - expect(result.rows.map(({ userId, rank }) => [userId, rank])).toEqual([ - ["a", 1], - ["b", 2], - ["c", 2], - ["d", 4], - ]); - }); - - test("returns ten rows and normalizes pages beyond the result", () => { - const rows = Array.from({ length: 101 }, (_, index) => - xpRow(String(index).padStart(3, "0"), String(1_000 - index))); - - const middle = buildXpLeaderboard(rows, 2); - expect(middle.rows).toHaveLength(10); - expect(middle.rows[0]?.rank).toBe(11); - expect(middle.totalPages).toBe(11); - - const last = buildXpLeaderboard(rows, 99); - expect(last.page).toBe(11); - expect(last.rows).toHaveLength(1); - }); - - test("uses stable name then user ID ordering for ties", () => { - const result = buildXpLeaderboard([ - xpRow("3", "100", 1, "Zulu"), - xpRow("2", "100", 1, "Alpha"), - xpRow("1", "100", 1, "Alpha"), - ], 1); - - expect(result.rows.map((row) => row.userId)).toEqual(["1", "2", "3"]); - }); - - test("searches identities while preserving overall ranks", () => { - const result = buildXpLeaderboard([ - xpRow("top", "1000", 4, "Alpha"), - xpRow("target", "900", 3, "Beta"), - xpRow("third", "800", 2, "Gamma"), - ], 1, "target-user"); - - expect(result.totalRows).toBe(1); - expect(result.rows.map(({ userId, rank }) => [userId, rank])).toEqual([ - ["target", 2], - ]); - }); -}); - -describe("voice leaderboard", () => { - const rows = [ - voiceRow("a", "voice-1", "100"), - voiceRow("a", "voice-2", "200"), - voiceRow("a", "deleted-channel", "9_999"), - voiceRow("b", "voice-1", "300"), - voiceRow("missing", "voice-2", "60", null), - ]; - - test("aggregates current channels and excludes historical channels", () => { - const result = buildVoiceLeaderboard(rows, ["voice-1", "voice-2"], 1); - - expect(result.rows.map(({ userId, totalSeconds, rank }) => ({ - userId, - totalSeconds, - rank, - }))).toEqual([ - { userId: "a", totalSeconds: "300", rank: 1 }, - { userId: "b", totalSeconds: "300", rank: 1 }, - { userId: "missing", totalSeconds: "60", rank: 3 }, - ]); - }); - - test("filters to a selected current channel", () => { - const result = buildVoiceLeaderboard( - rows, - ["voice-1", "voice-2"], - 1, - "voice-1", - ); - - expect(result.rows.map(({ userId, totalSeconds }) => [ - userId, - totalSeconds, - ])).toEqual([ - ["b", "300"], - ["a", "100"], - ]); - }); - - test("keeps unresolved identities and formats their shortened ID", () => { - const result = buildVoiceLeaderboard(rows, ["voice-2"], 1); - const unresolved = result.rows.find((row) => row.userId === "missing"); - - expect(unresolved?.displayName).toBeNull(); - expect(unresolved?.username).toBeNull(); - expect(shortenDiscordId("123456789012345678")).toBe("12345…5678"); - }); - - test("searches aggregated voice rows", () => { - const result = buildVoiceLeaderboard( - rows, - ["voice-1", "voice-2"], - 1, - undefined, - "missing", - ); - - expect(result.rows.map(({ userId, rank }) => [userId, rank])).toEqual([ - ["missing", 3], - ]); - }); -}); - -describe("leaderboard formatting", () => { - test("normalizes invalid page values", () => { - expect(normalizeRequestedPage(undefined)).toBe(1); - expect(normalizeRequestedPage("0")).toBe(1); - expect(normalizeRequestedPage("2.5")).toBe(1); - expect(normalizeRequestedPage(["2", "3"])).toBe(1); - expect(normalizeRequestedPage("3")).toBe(3); - }); - - test("formats large integer and voice duration values", () => { - expect(formatInteger("1234567")).toBe("1,234,567"); - expect(formatVoiceDuration("90061")).toBe("1 วัน 1 ชม. 1 นาที"); - expect(formatVoiceDuration("30")).toBe("น้อยกว่า 1 นาที"); - }); -}); diff --git a/lib/realtime-refresh.test.ts b/lib/realtime-refresh.test.ts deleted file mode 100644 index 552640c..0000000 --- a/lib/realtime-refresh.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { afterEach, describe, expect, mock, test } from "bun:test"; -import { - startRealtimeRefresh, - type RealtimeRefreshHandlers, -} from "@/lib/realtime-refresh"; - -const cleanups: Array<() => void> = []; - -afterEach(() => { - for (const clean of cleanups.splice(0)) clean(); -}); - -function setup( - filter: Parameters[0]["filter"], - debounceMs = 10, -) { - let handlers: RealtimeRefreshHandlers | undefined; - const refresh = mock(() => {}); - const cleanSubscription = mock(() => {}); - const clean = startRealtimeRefresh({ - filter, - debounceMs, - refresh, - subscribe(nextHandlers) { - handlers = nextHandlers; - return { clean: cleanSubscription }; - }, - }); - cleanups.push(clean); - return { - get handlers() { - if (!handlers) throw new Error("Handlers were not registered."); - return handlers; - }, - refresh, - clean, - cleanSubscription, - }; -} - -describe("leaderboard realtime refresh", () => { - test("filters XP and VC events by leaderboard scope", async () => { - const xp = setup({ kind: "xp" }); - xp.handlers.onVc({ channelId: "1" }); - xp.handlers.onXp(); - await Bun.sleep(20); - expect(xp.refresh).toHaveBeenCalledTimes(1); - - const selectedVc = setup({ kind: "vc", channelId: "2" }); - selectedVc.handlers.onVc({ channelId: "1" }); - await Bun.sleep(20); - expect(selectedVc.refresh).not.toHaveBeenCalled(); - selectedVc.handlers.onVc({ channelId: "2" }); - await Bun.sleep(20); - expect(selectedVc.refresh).toHaveBeenCalledTimes(1); - - const allVc = setup({ kind: "vc" }); - allVc.handlers.onVc({ channelId: "any" }); - await Bun.sleep(20); - expect(allVc.refresh).toHaveBeenCalledTimes(1); - }); - - test("refreshes after reconnect but not the initial connection", async () => { - const subscription = setup({ kind: "xp" }); - subscription.handlers.onOpen(); - await Bun.sleep(20); - expect(subscription.refresh).not.toHaveBeenCalled(); - - subscription.handlers.onOpen(); - await Bun.sleep(20); - expect(subscription.refresh).toHaveBeenCalledTimes(1); - }); - - test("debounces event bursts and cleans timers and subscriptions", async () => { - const subscription = setup({ kind: "xp" }, 20); - subscription.handlers.onXp(); - subscription.handlers.onXp(); - subscription.handlers.onXp(); - await Bun.sleep(30); - expect(subscription.refresh).toHaveBeenCalledTimes(1); - - subscription.handlers.onXp(); - subscription.clean(); - await Bun.sleep(30); - expect(subscription.refresh).toHaveBeenCalledTimes(1); - expect(subscription.cleanSubscription).toHaveBeenCalledTimes(1); - }); -}); diff --git a/lib/submission-intake.test.ts b/lib/submission-intake.test.ts deleted file mode 100644 index 5971cf6..0000000 --- a/lib/submission-intake.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { - answersByTemplateIndex, - normalizeSubmissionAnswers, - submissionUpdateText, - type SubmissionQuestion, -} from "@/lib/submission-intake"; - -const questions: SubmissionQuestion[] = [ - { - id: "q-name", - type: "text", - label: "Name", - required: true, - options: [], - allowOther: false, - }, - { - id: "q-skill", - type: "radio", - label: "Skill", - required: true, - options: ["Build", "Fight"], - allowOther: true, - }, - { - id: "q-tags", - type: "checkbox", - label: "Tags", - required: false, - options: ["Alpha", "Beta"], - allowOther: true, - }, -]; - -describe("Submission intake", () => { - test("normalizes answers into stored and display values", () => { - const normalized = normalizeSubmissionAnswers(questions, [ - { questionId: "q-name", value: "Erika" }, - { questionId: "q-skill", value: "Custom skill" }, - { questionId: "q-tags", value: ["Alpha", "Gamma"] }, - ]); - - expect(normalized).toEqual([ - { questionId: "q-name", value: "Erika", imageCdnId: null, displayValue: "Erika" }, - { questionId: "q-skill", value: "Custom skill", imageCdnId: null, displayValue: "Custom skill" }, - { questionId: "q-tags", value: '["Alpha","Gamma"]', imageCdnId: null, displayValue: "Alpha, Gamma" }, - ]); - }); - - test("validates required image-only and text plus image answers", () => { - const imageQuestions: SubmissionQuestion[] = [ - { - id: "q-image", - type: "text", - label: "Screenshot", - required: false, - options: [], - allowOther: false, - imageAnswerMode: "required", - }, - { - id: "q-text-image", - type: "textarea", - label: "Proof", - required: true, - options: [], - allowOther: false, - imageAnswerMode: "required", - }, - ]; - - expect(() => normalizeSubmissionAnswers(imageQuestions, [ - { questionId: "q-image", value: "" }, - { questionId: "q-text-image", value: "Details", imageCdnId: "img-2" }, - ])).toThrow("จำเป็นต้องอัปโหลดรูปสำหรับ “Screenshot”"); - - expect(normalizeSubmissionAnswers(imageQuestions, [ - { questionId: "q-image", value: "", imageCdnId: "img-1" }, - { questionId: "q-text-image", value: "Details", imageCdnId: "img-2" }, - ])).toEqual([ - { questionId: "q-image", value: "", imageCdnId: "img-1", displayValue: "" }, - { questionId: "q-text-image", value: "Details", imageCdnId: "img-2", displayValue: "Details" }, - ]); - }); - - test("rejects answers for questions outside the Form", () => { - expect(() => normalizeSubmissionAnswers(questions, [ - { questionId: "q-name", value: "Erika" }, - { questionId: "missing", value: "Injected" }, - ])).toThrow("ข้อมูลคำตอบไม่ตรงกับแบบฟอร์ม"); - }); - - test("rejects duplicate answers for one Question", () => { - expect(() => normalizeSubmissionAnswers(questions, [ - { questionId: "q-name", value: "Erika" }, - { questionId: "q-name", value: "Duplicate" }, - ])).toThrow("ข้อมูลคำตอบไม่ตรงกับแบบฟอร์ม"); - }); - - test("rejects unresolved Other answer placeholders", () => { - expect(() => normalizeSubmissionAnswers(questions, [ - { questionId: "q-name", value: "Erika" }, - { questionId: "q-skill", value: "__OTHER__" }, - ])).toThrow("โปรดระบุคำตอบสำหรับ “Skill”"); - }); - - test("maps answers by Question order for webhook templates", () => { - const normalized = normalizeSubmissionAnswers(questions, [ - { questionId: "q-name", value: "Erika" }, - { questionId: "q-skill", value: "Build" }, - { questionId: "q-tags", value: ["Beta"] }, - ]); - - expect(answersByTemplateIndex(questions, normalized)).toEqual({ - "1": "Erika", - "2": "Build", - "3": "Beta", - }); - }); - - test("renders Update text from stored old answers", () => { - const normalized = normalizeSubmissionAnswers(questions, [ - { questionId: "q-name", value: "Erika" }, - { questionId: "q-skill", value: "Fight" }, - { questionId: "q-tags", value: ["Alpha", "Gamma"] }, - ]); - - expect(submissionUpdateText(questions, normalized, { - "q-name": "Erika", - "q-skill": "Build", - "q-tags": '["Alpha"]', - })).toBe([ - "**Skill**: `Build` ➡️ `Fight`", - "**Tags**: `Alpha` ➡️ `Alpha, Gamma`", - ].join("\n")); - }); - - test("renders a stable no-change Update message", () => { - const normalized = normalizeSubmissionAnswers(questions, [ - { questionId: "q-name", value: "Erika" }, - { questionId: "q-skill", value: "Build" }, - { questionId: "q-tags", value: ["Alpha"] }, - ]); - - expect(submissionUpdateText(questions, normalized, { - "q-name": "Erika", - "q-skill": "Build", - "q-tags": '["Alpha"]', - })).toBe("No changes made."); - }); -}); diff --git a/lib/submission-webhook.test.ts b/lib/submission-webhook.test.ts deleted file mode 100644 index 1ec3fa8..0000000 --- a/lib/submission-webhook.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { submissionWebhookMessage } from "@/lib/submission-webhook"; -import type { - NormalizedSubmissionAnswer, - SubmissionQuestion, -} from "@/lib/submission-intake"; - -const questions: SubmissionQuestion[] = [ - { - id: "name", - type: "text", - label: "Character name", - required: true, - options: [], - allowOther: false, - }, -]; - -const answers: NormalizedSubmissionAnswer[] = [ - { - questionId: "name", - value: "Erika", - imageCdnId: null, - displayValue: "Erika", - }, -]; - -test("uses an image URL for image-only placeholders", () => { - expect(submissionWebhookMessage({ - template: "Image: {1}", - isUpdate: false, - userName: "Gunshiz", - discordId: "123", - submissionLink: "https://erika.sudloh.com/admin/form/form-1/result/submission-1", - questions, - answers: [{ - questionId: "name", - value: "", - imageCdnId: "img-1", - displayValue: "", - }], - oldAnswersByQuestionId: {}, - })).toBe("Image: /cdn/img-1"); -}); - -test("uses the configured template", () => { - expect(submissionWebhookMessage({ - template: "New answer: {1} by {username} ({id})", - isUpdate: false, - userName: "Gunshiz", - discordId: "123", - submissionLink: "https://erika.sudloh.com/admin/form/form-1/result/submission-1", - questions, - answers, - oldAnswersByQuestionId: {}, - })).toBe("New answer: Erika by Gunshiz (123)"); -}); - -test("uses a submission result link placeholder", () => { - expect(submissionWebhookMessage({ - template: "Review: {link}", - isUpdate: false, - userName: "Gunshiz", - discordId: "123", - submissionLink: "https://erika.sudloh.com/admin/form/form-1/result/submission-1", - questions, - answers, - oldAnswersByQuestionId: {}, - })).toBe("Review: https://erika.sudloh.com/admin/form/form-1/result/submission-1"); -}); - -describe("default templates", () => { - test("sends a new-submission message when its template is blank", () => { - expect(submissionWebhookMessage({ - template: null, - isUpdate: false, - userName: "Gunshiz", - discordId: "123", - submissionLink: "https://erika.sudloh.com/admin/form/form-1/result/submission-1", - questions, - answers, - oldAnswersByQuestionId: {}, - })).toBe("New submission from Gunshiz (123)."); - }); - - test("sends an update message when its template is blank", () => { - expect(submissionWebhookMessage({ - template: " ", - isUpdate: true, - userName: "Gunshiz", - discordId: "123", - submissionLink: "https://erika.sudloh.com/admin/form/form-1/result/submission-1", - questions, - answers, - oldAnswersByQuestionId: { name: "Old name" }, - })).toContain("Submission updated by Gunshiz (123)."); - }); -}); diff --git a/package.json b/package.json index 02b92d5..b129d0a 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev": "PORT=4000 bun --bun next dev", "build": "next build", "start": "next start -H 0.0.0.0", + "test": "bun test", "lint": "eslint", "up": "docker compose up -d --build", "logs": "docker compose logs -f app", diff --git a/test/setup.ts b/test/setup.ts deleted file mode 100644 index e24e390..0000000 --- a/test/setup.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { mock } from "bun:test"; - -mock.module("server-only", () => ({}));