test: keep only SSE coverage

This commit is contained in:
2026-07-30 15:09:09 +07:00 Unverified
parent a014f72d0f
commit 8ee011cdfc
13 changed files with 1 additions and 789 deletions
-2
View File
@@ -1,2 +0,0 @@
[test]
preload = ["./test/setup.ts"]
-24
View File
@@ -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"
);
});
});
-68
View File
@@ -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");
});
});
-53
View File
@@ -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<File, "name" | "type" | "size">> = {}
): Pick<File, "name" | "type" | "size"> {
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]);
});
});
-71
View File
@@ -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<Response>) {
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();
});
});
-31
View File
@@ -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");
});
-21
View File
@@ -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();
});
});
-177
View File
@@ -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 นาที");
});
});
-88
View File
@@ -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<typeof startRealtimeRefresh>[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);
});
});
-153
View File
@@ -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: "<b>Name</b>",
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.");
});
});
-98
View File
@@ -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).");
});
});
+1
View File
@@ -7,6 +7,7 @@
"dev": "PORT=4000 bun --bun next dev", "dev": "PORT=4000 bun --bun next dev",
"build": "next build", "build": "next build",
"start": "next start -H 0.0.0.0", "start": "next start -H 0.0.0.0",
"test": "bun test",
"lint": "eslint", "lint": "eslint",
"up": "docker compose up -d --build", "up": "docker compose up -d --build",
"logs": "docker compose logs -f app", "logs": "docker compose logs -f app",
-3
View File
@@ -1,3 +0,0 @@
import { mock } from "bun:test";
mock.module("server-only", () => ({}));