refactor(db): typed query helpers, shared test FormData helper
Replace raw db.execute tuple casts with queryRows/rowsFrom/execResult/ affectedRows helpers from lib/db, drop redundant mysql2 casts on typed query builders, and centralize per-test fakeForm into test/fake-form. Update db mocks in tests so helpers resolve against mocked execute.
This commit is contained in:
1 parent
2e25b39364
commit
8638e81444
116 files changed
+866
-897
No files matched your search
@@ -6,6 +6,7 @@ import { requirePermission } from "@/lib/admin/guard";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { ActionError } from "@/lib/safe-action-shared";
|
||||
import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||
import { fakeForm } from "@/test/fake-form";
|
||||
import { createAd, deleteAd } from "./admin-ads";
|
||||
|
||||
const { insertValues, deleteWhere } = vi.hoisted(() => {
|
||||
@@ -41,10 +42,6 @@ vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||
vi.mock("next/navigation", () => ({ redirect: vi.fn() }));
|
||||
|
||||
const staff = { id: 1, rank: 7, username: "admin" };
|
||||
const fakeForm = (data: Record<string, string>) => ({
|
||||
get: (k: string) => data[k] ?? null,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(requirePermission).mockResolvedValue(staff as never);
|
||||
@@ -55,7 +52,7 @@ beforeEach(() => {
|
||||
describe("createAd", () => {
|
||||
it("creates ad and redirects", async () => {
|
||||
await createAd(
|
||||
fakeForm({ image: "https://example.com/ad.png" }) as unknown as FormData,
|
||||
fakeForm({ image: "https://example.com/ad.png" }) as FormData,
|
||||
);
|
||||
expect(insertValues).toHaveBeenCalled();
|
||||
expect(logStaffActivity).toHaveBeenCalled();
|
||||
@@ -63,13 +60,13 @@ describe("createAd", () => {
|
||||
});
|
||||
|
||||
it("returns early when image empty", async () => {
|
||||
await createAd(fakeForm({ image: "" }) as unknown as FormData);
|
||||
await createAd(fakeForm({ image: "" }) as FormData);
|
||||
expect(insertValues).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs error on db failure", async () => {
|
||||
insertValues.mockRejectedValue(new Error("db"));
|
||||
await createAd(fakeForm({ image: "x" }) as unknown as FormData);
|
||||
await createAd(fakeForm({ image: "x" }) as FormData);
|
||||
expect(logger.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { ResultSetHeader } from "mysql2";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
@@ -26,11 +25,11 @@ export async function createAd(formData: FormData): Promise<void> {
|
||||
|
||||
const now = new Date();
|
||||
try {
|
||||
const [result] = (await db.insert(WebsiteAds).values({
|
||||
const [result] = await db.insert(WebsiteAds).values({
|
||||
image,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})) as unknown as [ResultSetHeader];
|
||||
});
|
||||
await logStaffActivity({
|
||||
staffId: staff.id,
|
||||
action: "ad_create",
|
||||
@@ -95,9 +94,7 @@ export const deleteAd = adminAction(
|
||||
async (ctx) => {
|
||||
const id = ctx.data.id;
|
||||
try {
|
||||
const [result] = (await db
|
||||
.delete(WebsiteAds)
|
||||
.where(eq(WebsiteAds.id, id))) as unknown as [ResultSetHeader];
|
||||
const [result] = await db.delete(WebsiteAds).where(eq(WebsiteAds.id, id));
|
||||
if (!result.affectedRows) {
|
||||
throw new ActionError("Advertisement not found");
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { revalidatePath } from "next/cache";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
import { fakeForm } from "@/test/fake-form";
|
||||
import { sendHotelAlert } from "./admin-alerts";
|
||||
|
||||
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||
@@ -18,10 +19,6 @@ vi.mock("@/lib/db", () => ({
|
||||
vi.mock("@/lib/services/rcon", () => ({ rcon: { send: vi.fn() } }));
|
||||
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||
|
||||
const fakeForm = (data: Record<string, string>) => ({
|
||||
get: (key: string) => data[key] ?? null,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(requirePermission).mockResolvedValue({
|
||||
@@ -33,15 +30,13 @@ beforeEach(() => {
|
||||
|
||||
describe("sendHotelAlert", () => {
|
||||
it("sends hotel alert and revalidates", async () => {
|
||||
await sendHotelAlert(
|
||||
fakeForm({ message: "Hello!" }) as unknown as FormData,
|
||||
);
|
||||
await sendHotelAlert(fakeForm({ message: "Hello!" }) as FormData);
|
||||
expect(rcon.send).toHaveBeenCalledWith("hotelalert", { message: "Hello!" });
|
||||
expect(revalidatePath).toHaveBeenCalledWith("/admin/alerts");
|
||||
});
|
||||
|
||||
it("returns early when message is empty", async () => {
|
||||
await sendHotelAlert(fakeForm({ message: "" }) as unknown as FormData);
|
||||
await sendHotelAlert(fakeForm({ message: "" }) as FormData);
|
||||
expect(rcon.send).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -37,6 +37,7 @@ vi.mock("next/navigation", () => ({
|
||||
},
|
||||
}));
|
||||
vi.mock("@/lib/db", async () => {
|
||||
const { createDbHelpers } = await import("@/test/db-helpers");
|
||||
const { MySqlDialect } = await import("drizzle-orm/mysql-core");
|
||||
const dialect = new MySqlDialect();
|
||||
const database = {
|
||||
@@ -85,7 +86,11 @@ vi.mock("@/lib/db", async () => {
|
||||
transaction: async (fn: (tx: unknown) => unknown): Promise<unknown> =>
|
||||
fn(database),
|
||||
};
|
||||
return { ...(await import("@/db/schema")), db: database };
|
||||
return {
|
||||
...(await import("@/db/schema")),
|
||||
...createDbHelpers(database.execute),
|
||||
db: database,
|
||||
};
|
||||
});
|
||||
|
||||
import { articleEditToken } from "@/lib/article-edit-token";
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "@/lib/article-input";
|
||||
import {
|
||||
db,
|
||||
rowsFrom,
|
||||
WebsiteArticleComments,
|
||||
WebsiteArticleReactions,
|
||||
WebsiteArticles,
|
||||
@@ -128,11 +129,9 @@ export async function createArticle(
|
||||
updatedAt: now,
|
||||
publishedAt: fields.status === "published" ? now : null,
|
||||
});
|
||||
const [createdRows] = await tx.execute(
|
||||
sql`SELECT CAST(LAST_INSERT_ID() AS CHAR) AS id`,
|
||||
);
|
||||
const articleId = (createdRows as unknown as Array<{ id: string }>)[0]
|
||||
?.id;
|
||||
const articleId = rowsFrom<{ id: string }>(
|
||||
await tx.execute(sql`SELECT CAST(LAST_INSERT_ID() AS CHAR) AS id`),
|
||||
)[0]?.id;
|
||||
if (!articleId || !/^[1-9][0-9]*$/.test(articleId))
|
||||
throw new Error("Article identity unavailable");
|
||||
await enqueueEffect(tx, operationId, "news.refresh");
|
||||
|
||||
@@ -3,6 +3,7 @@ import { revalidatePath } from "next/cache";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { requirePermissionRateLimited } from "@/lib/admin/guard";
|
||||
import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||
import { fakeForm } from "@/test/fake-form";
|
||||
import { disbandGuild } from "./admin-guilds";
|
||||
|
||||
const { selectLimit, transactionFn, deleteWhere, updateSet } = vi.hoisted(
|
||||
@@ -42,10 +43,6 @@ vi.mock("@/lib/services/staff-activity", () => ({ logStaffActivity: vi.fn() }));
|
||||
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||
|
||||
const staff = { id: 1, rank: 7, username: "admin" };
|
||||
const fakeForm = (data: Record<string, string>) => ({
|
||||
get: (key: string) => data[key] ?? null,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(requirePermissionRateLimited).mockResolvedValue(staff as never);
|
||||
@@ -79,13 +76,13 @@ describe("disbandGuild", () => {
|
||||
await fn(tx);
|
||||
},
|
||||
);
|
||||
await disbandGuild(fakeForm({ id: "1" }) as unknown as FormData);
|
||||
await disbandGuild(fakeForm({ id: "1" }) as FormData);
|
||||
expect(logStaffActivity).toHaveBeenCalled();
|
||||
expect(revalidatePath).toHaveBeenCalledWith("/admin/guilds");
|
||||
});
|
||||
|
||||
it("returns early when id is not positive", async () => {
|
||||
await disbandGuild(fakeForm({ id: "0" }) as unknown as FormData);
|
||||
await disbandGuild(fakeForm({ id: "0" }) as FormData);
|
||||
expect(selectLimit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -60,9 +60,7 @@ export const liftBanFromHelpTicket = adminAction(
|
||||
}
|
||||
|
||||
const result = await db.delete(Ban).where(eq(Ban.userId, ticket.userId));
|
||||
const removed = Number(
|
||||
(result as unknown as [{ affectedRows: number }])[0]?.affectedRows ?? 0,
|
||||
);
|
||||
const removed = Number(result[0]?.affectedRows ?? 0);
|
||||
|
||||
const now = new Date();
|
||||
if (ticket.open) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { fakeForm } from "@/test/fake-form";
|
||||
import {
|
||||
createHelpQuestion,
|
||||
deleteHelpQuestion,
|
||||
@@ -29,10 +30,6 @@ vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||
vi.mock("next/navigation", () => ({ redirect: vi.fn() }));
|
||||
|
||||
const staff = { id: 1, rank: 7, username: "admin" };
|
||||
const fakeForm = (data: Record<string, string | null>) => ({
|
||||
get: (key: string) => (key in data ? data[key] : null),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(requirePermission).mockResolvedValue(staff as never);
|
||||
@@ -47,7 +44,7 @@ describe("createHelpQuestion", () => {
|
||||
fakeForm({
|
||||
name: "FAQ",
|
||||
content: "<p>Answer</p>",
|
||||
}) as unknown as FormData,
|
||||
}) as FormData,
|
||||
);
|
||||
expect(insertValues).toHaveBeenCalled();
|
||||
expect(redirect).toHaveBeenCalledWith("/admin/help-questions");
|
||||
@@ -61,7 +58,7 @@ describe("updateHelpQuestion", () => {
|
||||
id: "42",
|
||||
name: "Updated",
|
||||
content: "New",
|
||||
}) as unknown as FormData,
|
||||
}) as FormData,
|
||||
);
|
||||
expect(updateWhere).toHaveBeenCalled();
|
||||
expect(redirect).toHaveBeenCalledWith("/admin/help-questions");
|
||||
@@ -70,7 +67,7 @@ describe("updateHelpQuestion", () => {
|
||||
|
||||
describe("deleteHelpQuestion", () => {
|
||||
it("deletes and redirects", async () => {
|
||||
await deleteHelpQuestion(fakeForm({ id: "42" }) as unknown as FormData);
|
||||
await deleteHelpQuestion(fakeForm({ id: "42" }) as FormData);
|
||||
expect(deleteWhere).toHaveBeenCalled();
|
||||
expect(redirect).toHaveBeenCalledWith("/admin/help-questions");
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { ResultSetHeader } from "mysql2";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
@@ -34,7 +33,7 @@ export async function createHelpQuestion(formData: FormData): Promise<void> {
|
||||
sanitizeField(formData.get("buttonBorderColor"), 16) || "#facc15";
|
||||
|
||||
try {
|
||||
const [result] = (await db.insert(WebsiteHelpCenterCategories).values({
|
||||
const [result] = await db.insert(WebsiteHelpCenterCategories).values({
|
||||
name,
|
||||
content,
|
||||
position: parsePosition(formData.get("position")),
|
||||
@@ -44,7 +43,7 @@ export async function createHelpQuestion(formData: FormData): Promise<void> {
|
||||
buttonColor,
|
||||
buttonBorderColor,
|
||||
smallBox: formData.get("smallBox") != null,
|
||||
})) as unknown as [ResultSetHeader];
|
||||
});
|
||||
await logStaffActivity({
|
||||
staffId: staff.id,
|
||||
action: "help_create",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { fakeForm } from "@/test/fake-form";
|
||||
import {
|
||||
addBlacklist,
|
||||
addWhitelist,
|
||||
@@ -30,10 +31,6 @@ vi.mock("@/lib/db", () => ({
|
||||
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||
|
||||
const staff = { id: 1, rank: 7, username: "admin" };
|
||||
const fakeForm = (data: Record<string, string>) => ({
|
||||
get: (key: string) => data[key] ?? null,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(requirePermission).mockResolvedValue(staff as never);
|
||||
@@ -43,9 +40,7 @@ beforeEach(() => {
|
||||
|
||||
describe("addWhitelist", () => {
|
||||
it("creates whitelist entry", async () => {
|
||||
await addWhitelist(
|
||||
fakeForm({ ipAddress: "192.168.1.1" }) as unknown as FormData,
|
||||
);
|
||||
await addWhitelist(fakeForm({ ipAddress: "192.168.1.1" }) as FormData);
|
||||
expect(insertValues).toHaveBeenCalledWith({
|
||||
ipAddress: "192.168.1.1",
|
||||
asn: null,
|
||||
@@ -55,23 +50,21 @@ describe("addWhitelist", () => {
|
||||
});
|
||||
|
||||
it("returns early when ip is empty", async () => {
|
||||
await addWhitelist(fakeForm({ ipAddress: "" }) as unknown as FormData);
|
||||
await addWhitelist(fakeForm({ ipAddress: "" }) as FormData);
|
||||
expect(insertValues).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteWhitelist", () => {
|
||||
it("deletes whitelist entry", async () => {
|
||||
await deleteWhitelist(fakeForm({ id: "42" }) as unknown as FormData);
|
||||
await deleteWhitelist(fakeForm({ id: "42" }) as FormData);
|
||||
expect(deleteWhere).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("addBlacklist", () => {
|
||||
it("creates blacklist entry", async () => {
|
||||
await addBlacklist(
|
||||
fakeForm({ ipAddress: "203.0.113.1" }) as unknown as FormData,
|
||||
);
|
||||
await addBlacklist(fakeForm({ ipAddress: "203.0.113.1" }) as FormData);
|
||||
expect(insertValues).toHaveBeenCalledWith({
|
||||
ipAddress: "203.0.113.1",
|
||||
asn: null,
|
||||
@@ -82,7 +75,7 @@ describe("addBlacklist", () => {
|
||||
|
||||
describe("deleteBlacklist", () => {
|
||||
it("deletes blacklist entry", async () => {
|
||||
await deleteBlacklist(fakeForm({ id: "99" }) as unknown as FormData);
|
||||
await deleteBlacklist(fakeForm({ id: "99" }) as FormData);
|
||||
expect(deleteWhere).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { tryRemoveLocalPhotoFile } from "@/lib/admin/photo-files";
|
||||
import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||
import { fakeForm } from "@/test/fake-form";
|
||||
import { deletePhoto } from "./admin-photos";
|
||||
|
||||
const { select, deleteFn, limit, whereDelete } = vi.hoisted(() => {
|
||||
@@ -33,10 +34,6 @@ vi.mock("@/lib/db", () => ({
|
||||
CameraWeb: { id: "id", url: "url" },
|
||||
}));
|
||||
|
||||
const fakeForm = (data) => ({
|
||||
get: (key) => data[key] ?? null,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
limit.mockResolvedValue([{ id: 42, url: "/uploads/cam/42.png" }]);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { ResultSetHeader } from "mysql2";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
@@ -44,7 +43,7 @@ export async function createShopArticle(formData: FormData): Promise<void> {
|
||||
const now = new Date();
|
||||
const costs = reqUInt(formData, "costs");
|
||||
try {
|
||||
const [result] = (await db.insert(WebsiteShopArticles).values({
|
||||
const [result] = await db.insert(WebsiteShopArticles).values({
|
||||
name,
|
||||
info: String(formData.get("info") ?? "")
|
||||
.normalize("NFC")
|
||||
@@ -71,7 +70,7 @@ export async function createShopArticle(formData: FormData): Promise<void> {
|
||||
position: reqUInt(formData, "position"),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})) as unknown as [ResultSetHeader];
|
||||
});
|
||||
await logStaffActivity({
|
||||
staffId: staff.id,
|
||||
action: "shop_create",
|
||||
|
||||
@@ -23,13 +23,15 @@ vi.mock("@/lib/report-error", () => ({ reportError: vi.fn() }));
|
||||
vi.mock("@/lib/foundation/security", () => ({
|
||||
extractClientIpAsync: async () => "127.0.0.1",
|
||||
}));
|
||||
vi.mock("@/lib/db", () => {
|
||||
vi.mock("@/lib/db", async () => {
|
||||
const { createDbHelpers } = await import("@/test/db-helpers");
|
||||
const execute = async (sql: SQL) => {
|
||||
const query = new MySqlDialect().sqlToQuery(sql);
|
||||
state.queries.push(query);
|
||||
return [state.rows];
|
||||
};
|
||||
return {
|
||||
...createDbHelpers(execute),
|
||||
db: {
|
||||
execute,
|
||||
transaction: async (fn: (tx: { execute: typeof execute }) => unknown) =>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { namedTableView, tableViewPath } from "@/lib/admin/table-view-state";
|
||||
import { db } from "@/lib/db";
|
||||
import { db, queryRows, rowsFrom } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permission-slugs";
|
||||
import { adminAction } from "@/lib/safe-action";
|
||||
|
||||
@@ -25,11 +25,10 @@ export const listTableViews = adminAction(
|
||||
schema: z.object({ path: tableViewPath }),
|
||||
},
|
||||
async ({ session, data }) => {
|
||||
const [rows] = await db.execute(
|
||||
sql`SELECT name, state FROM website_admin_table_views WHERE user_id = ${Number(session.user.id)} AND path = ${data.path} ORDER BY name LIMIT 20`,
|
||||
);
|
||||
const views = (
|
||||
rows as unknown as { name: string; state: string }[]
|
||||
await queryRows<{ name: string; state: string }>(
|
||||
sql`SELECT name, state FROM website_admin_table_views WHERE user_id = ${Number(session.user.id)} AND path = ${data.path} ORDER BY name LIMIT 20`,
|
||||
)
|
||||
).flatMap((row) => {
|
||||
try {
|
||||
const parsed = namedTableView.safeParse({
|
||||
@@ -58,10 +57,11 @@ export const saveTableView = adminAction(
|
||||
await tx.execute(
|
||||
sql`SELECT id FROM users WHERE id = ${userId} FOR UPDATE`,
|
||||
);
|
||||
const [rows] = await tx.execute(
|
||||
sql`SELECT name FROM website_admin_table_views WHERE user_id = ${userId} AND path = ${data.path}`,
|
||||
const names = rowsFrom<{ name: string }>(
|
||||
await tx.execute(
|
||||
sql`SELECT name FROM website_admin_table_views WHERE user_id = ${userId} AND path = ${data.path}`,
|
||||
),
|
||||
);
|
||||
const names = rows as unknown as { name: string }[];
|
||||
if (
|
||||
names.length >= 20 &&
|
||||
!names.some(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { revalidatePath } from "next/cache";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||
import { fakeForm } from "@/test/fake-form";
|
||||
import { createTag, deleteTag, updateTag } from "./admin-tags";
|
||||
|
||||
const { insertValues, updateWhere, deleteWhere, transaction } = vi.hoisted(
|
||||
@@ -35,10 +36,6 @@ vi.mock("@/lib/services/staff-activity", () => ({ logStaffActivity: vi.fn() }));
|
||||
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||
|
||||
const staff = { id: 1, rank: 7, username: "admin" };
|
||||
const fakeForm = (data: Record<string, string>) => ({
|
||||
get: (key: string) => data[key] ?? null,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(requirePermission).mockResolvedValue(staff as never);
|
||||
@@ -58,7 +55,7 @@ describe("createTag", () => {
|
||||
fakeForm({
|
||||
name: "News",
|
||||
backgroundColor: "#ff0000",
|
||||
}) as unknown as FormData,
|
||||
}) as FormData,
|
||||
);
|
||||
|
||||
expect(insertValues).toHaveBeenCalledWith(
|
||||
@@ -69,12 +66,12 @@ describe("createTag", () => {
|
||||
});
|
||||
|
||||
it("returns early when name is empty", async () => {
|
||||
await createTag(fakeForm({ name: "" }) as unknown as FormData);
|
||||
await createTag(fakeForm({ name: "" }) as FormData);
|
||||
expect(insertValues).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses default color when not provided", async () => {
|
||||
await createTag(fakeForm({ name: "Test" }) as unknown as FormData);
|
||||
await createTag(fakeForm({ name: "Test" }) as FormData);
|
||||
|
||||
expect(insertValues).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ backgroundColor: "#888888" }),
|
||||
@@ -85,7 +82,7 @@ describe("createTag", () => {
|
||||
insertValues.mockRejectedValue(new Error("DB error"));
|
||||
|
||||
await expect(
|
||||
createTag(fakeForm({ name: "News" }) as unknown as FormData),
|
||||
createTag(fakeForm({ name: "News" }) as FormData),
|
||||
).resolves.toBeUndefined();
|
||||
expect(revalidatePath).toHaveBeenCalledWith("/admin/tags");
|
||||
});
|
||||
@@ -98,7 +95,7 @@ describe("updateTag", () => {
|
||||
id: "42",
|
||||
name: "Updated",
|
||||
backgroundColor: "#00ff00",
|
||||
}) as unknown as FormData,
|
||||
}) as FormData,
|
||||
);
|
||||
|
||||
expect(updateWhere).toHaveBeenCalled();
|
||||
@@ -107,19 +104,19 @@ describe("updateTag", () => {
|
||||
});
|
||||
|
||||
it("returns early when id is invalid", async () => {
|
||||
await updateTag(fakeForm({ id: "", name: "Test" }) as unknown as FormData);
|
||||
await updateTag(fakeForm({ id: "", name: "Test" }) as FormData);
|
||||
expect(updateWhere).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns early when name is empty after update", async () => {
|
||||
await updateTag(fakeForm({ id: "42", name: "" }) as unknown as FormData);
|
||||
await updateTag(fakeForm({ id: "42", name: "" }) as FormData);
|
||||
expect(updateWhere).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteTag", () => {
|
||||
it("deletes a tag and its taggables", async () => {
|
||||
await deleteTag(fakeForm({ id: "42" }) as unknown as FormData);
|
||||
await deleteTag(fakeForm({ id: "42" }) as FormData);
|
||||
|
||||
expect(transaction).toHaveBeenCalled();
|
||||
expect(deleteWhere).toHaveBeenCalled();
|
||||
@@ -128,7 +125,7 @@ describe("deleteTag", () => {
|
||||
});
|
||||
|
||||
it("returns early when id is invalid", async () => {
|
||||
await deleteTag(fakeForm({ id: "" }) as unknown as FormData);
|
||||
await deleteTag(fakeForm({ id: "" }) as FormData);
|
||||
expect(transaction).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { ResultSetHeader } from "mysql2";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { db, Taggables, Tags } from "@/lib/db";
|
||||
@@ -42,12 +41,12 @@ export async function createTag(formData: FormData): Promise<void> {
|
||||
const now = new Date();
|
||||
|
||||
try {
|
||||
const [result] = (await db.insert(Tags).values({
|
||||
const [result] = await db.insert(Tags).values({
|
||||
name,
|
||||
backgroundColor,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})) as unknown as [ResultSetHeader];
|
||||
});
|
||||
await logStaffActivity({
|
||||
staffId: staff.id,
|
||||
action: "tag_create",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { fakeForm } from "@/test/fake-form";
|
||||
import { createTeam, deleteTeam } from "./admin-teams";
|
||||
|
||||
const { insertValues, deleteWhere } = vi.hoisted(() => {
|
||||
@@ -22,10 +23,6 @@ vi.mock("@/lib/db", () => ({
|
||||
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||
|
||||
const staff = { id: 1, rank: 7, username: "admin" };
|
||||
const fakeForm = (data: Record<string, string | null>) => ({
|
||||
get: (key: string) => (key in data ? data[key] : null),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(requirePermission).mockResolvedValue(staff as never);
|
||||
@@ -35,9 +32,7 @@ beforeEach(() => {
|
||||
|
||||
describe("createTeam", () => {
|
||||
it("creates a team entry", async () => {
|
||||
await createTeam(
|
||||
fakeForm({ rankName: "Moderator" }) as unknown as FormData,
|
||||
);
|
||||
await createTeam(fakeForm({ rankName: "Moderator" }) as FormData);
|
||||
expect(insertValues).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ rankName: "Moderator" }),
|
||||
);
|
||||
@@ -45,14 +40,14 @@ describe("createTeam", () => {
|
||||
});
|
||||
|
||||
it("returns early when rankName is empty", async () => {
|
||||
await createTeam(fakeForm({ rankName: "" }) as unknown as FormData);
|
||||
await createTeam(fakeForm({ rankName: "" }) as FormData);
|
||||
expect(insertValues).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteTeam", () => {
|
||||
it("deletes a team entry", async () => {
|
||||
await deleteTeam(fakeForm({ id: "42" }) as unknown as FormData);
|
||||
await deleteTeam(fakeForm({ id: "42" }) as FormData);
|
||||
expect(deleteWhere).toHaveBeenCalled();
|
||||
expect(revalidatePath).toHaveBeenCalledWith("/admin/teams");
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { ResultSetHeader } from "mysql2";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { positiveBigInt } from "@/lib/api";
|
||||
@@ -47,7 +46,7 @@ export async function createVoucher(input: {
|
||||
const now = new Date();
|
||||
|
||||
try {
|
||||
const [result] = (await db.insert(WebsiteShopVouchers).values({
|
||||
const [result] = await db.insert(WebsiteShopVouchers).values({
|
||||
code,
|
||||
amount: Math.floor(amount),
|
||||
maxUses,
|
||||
@@ -55,7 +54,7 @@ export async function createVoucher(input: {
|
||||
expiresAt,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})) as unknown as [ResultSetHeader];
|
||||
});
|
||||
revalidatePath("/admin/vouchers");
|
||||
return actionOk({ id: String(result.insertId) });
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { redirect } from "next/navigation";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import { fakeForm } from "@/test/fake-form";
|
||||
import { saveVpn } from "./admin-vpn";
|
||||
|
||||
const { mockValues, mockOnDuplicateKeyUpdate } = vi.hoisted(() => {
|
||||
@@ -30,10 +31,6 @@ vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||
vi.mock("next/navigation", () => ({ redirect: vi.fn() }));
|
||||
|
||||
const staff = { id: 1, rank: 7, username: "admin" };
|
||||
const fakeForm = (data: Record<string, string | null>) => ({
|
||||
get: (key: string) => (key in data ? data[key] : null),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(requirePermission).mockResolvedValue(staff as never);
|
||||
@@ -50,7 +47,7 @@ describe("saveVpn", () => {
|
||||
vpn_block_enabled: "1",
|
||||
vpn_provider: "proxycheck",
|
||||
vpn_api_key: "abc123",
|
||||
}) as unknown as FormData,
|
||||
}) as FormData,
|
||||
);
|
||||
expect(mockValues).toHaveBeenCalledTimes(4);
|
||||
expect(mockOnDuplicateKeyUpdate).toHaveBeenCalledTimes(4);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { ResultSetHeader } from "mysql2";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { db, WebsiteWordfilter } from "@/lib/db";
|
||||
@@ -25,9 +24,7 @@ export async function addWord(input: {
|
||||
if (!word) return actionError("Word is required");
|
||||
|
||||
try {
|
||||
const [result] = (await db
|
||||
.insert(WebsiteWordfilter)
|
||||
.values({ word })) as unknown as [ResultSetHeader];
|
||||
const [result] = await db.insert(WebsiteWordfilter).values({ word });
|
||||
reloadWordFilter();
|
||||
await rcon.updateWordFilter();
|
||||
revalidatePath("/admin/wordfilter");
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { ResultSetHeader } from "mysql2";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { db, WebsiteWriteableBoxes } from "@/lib/db";
|
||||
@@ -53,7 +52,7 @@ export async function createBox(formData: FormData): Promise<void> {
|
||||
|
||||
const now = new Date();
|
||||
try {
|
||||
const [result] = (await db.insert(WebsiteWriteableBoxes).values({
|
||||
const [result] = await db.insert(WebsiteWriteableBoxes).values({
|
||||
title,
|
||||
icon:
|
||||
String(formData.get("icon") ?? "")
|
||||
@@ -65,7 +64,7 @@ export async function createBox(formData: FormData): Promise<void> {
|
||||
isActive: String(formData.get("isActive") ?? "").normalize("NFC") === "1",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})) as unknown as [ResultSetHeader];
|
||||
});
|
||||
await logStaffActivity({
|
||||
staffId: staff.id,
|
||||
action: "writeable_box_create",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
||||
import { fakeForm } from "@/test/fake-form";
|
||||
import { applyStaff, applyTeam } from "./applications";
|
||||
|
||||
const { selectLimit, insertValues } = vi.hoisted(() => {
|
||||
@@ -33,10 +34,6 @@ vi.mock("@/lib/rate-limit", () => ({ clientIp: vi.fn(), rateLimit: vi.fn() }));
|
||||
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||
vi.mock("next/navigation", () => ({ redirect: vi.fn() }));
|
||||
|
||||
const fakeForm = (data) => ({
|
||||
get: (key) => data[key] ?? null,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "42" } });
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { ResultSetHeader } from "mysql2";
|
||||
import { z } from "zod";
|
||||
import { db, WebsiteBanner } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
@@ -24,9 +23,7 @@ const bannerSchema = z.object({
|
||||
export const createBanner = adminAction(
|
||||
{ permission: PERMS.BANNERS_EDIT, schema: bannerSchema },
|
||||
async (ctx) => {
|
||||
const [result] = (await db
|
||||
.insert(WebsiteBanner)
|
||||
.values(ctx.data)) as unknown as [ResultSetHeader];
|
||||
const [result] = await db.insert(WebsiteBanner).values(ctx.data);
|
||||
const id = Number(result.insertId);
|
||||
logAudit({
|
||||
userId: ctx.session.user.id,
|
||||
|
||||
@@ -23,9 +23,7 @@ export async function bulkUnban({
|
||||
}): Promise<ActionResult<{ unbanned: number; total: number }>> {
|
||||
const staff = await requirePermission(PERMS.USERS_EDIT);
|
||||
const result = await db.delete(Ban).where(inArray(Ban.userId, userIds));
|
||||
const unbanned = Number(
|
||||
(result as unknown as [{ affectedRows: number }])[0]?.affectedRows ?? 0,
|
||||
);
|
||||
const unbanned = Number(result[0]?.affectedRows ?? 0);
|
||||
await logStaffActivity({
|
||||
staffId: staff.id,
|
||||
action: "bulk_unban",
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@/features/catalog/server/offer-commands";
|
||||
import { sendCatalogUpdate } from "@/features/catalog/server/sync-status";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { CatalogItems, db, ItemsBase } from "@/lib/db";
|
||||
import { CatalogItems, db, ItemsBase, queryRows } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { logAudit } from "@/lib/services/audit";
|
||||
import { withCatalogExport } from "@/lib/services/catalog-git-queue";
|
||||
@@ -145,10 +145,10 @@ export async function bulkCreateCatalogItems({
|
||||
let failed = 0;
|
||||
|
||||
// Find the next order number on the target page to avoid collisions.
|
||||
const [maxOrder] = (await db.execute(sql`
|
||||
const maxOrder = await queryRows<{ maxOrder: number }>(sql`
|
||||
SELECT COALESCE(MAX(order_number), 0) AS maxOrder FROM catalog_items WHERE page_id = ${String(pageId)}
|
||||
`)) as unknown as [{ maxOrder: number }, unknown];
|
||||
let nextOrder = Number(maxOrder?.maxOrder ?? 0) + 1;
|
||||
`);
|
||||
let nextOrder = Number(maxOrder[0]?.maxOrder ?? 0) + 1;
|
||||
|
||||
for (const row of rows) {
|
||||
const base = baseMap.get(row.baseId);
|
||||
|
||||
+11
-11
@@ -13,7 +13,7 @@ import {
|
||||
} from "@/features/catalog/server/page-commands";
|
||||
import { sendCatalogUpdate } from "@/features/catalog/server/sync-status";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { db, ItemsBase } from "@/lib/db";
|
||||
import { db, ItemsBase, queryRows } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import type { ActionResult } from "@/lib/safe-action-shared";
|
||||
import { withCatalogExport } from "@/lib/services/catalog-git-queue";
|
||||
@@ -290,10 +290,10 @@ export async function createAutoCategory(input: {
|
||||
.where(inArray(ItemsBase.id, baseIds));
|
||||
const baseMap = new Map(bases.map((b) => [b.id, b]));
|
||||
|
||||
const [maxOrder] = (await db.execute(sql`
|
||||
const maxOrder = await queryRows<{ maxOrder: number }>(sql`
|
||||
SELECT COALESCE(MAX(order_number), 0) AS maxOrder FROM catalog_items WHERE page_id = ${String(pageId)}
|
||||
`)) as unknown as [{ maxOrder: number }, unknown];
|
||||
let nextOrder = Number(maxOrder?.maxOrder ?? 0) + 1;
|
||||
`);
|
||||
let nextOrder = Number(maxOrder[0]?.maxOrder ?? 0) + 1;
|
||||
|
||||
let created = 0;
|
||||
for (const row of rows) {
|
||||
@@ -455,9 +455,9 @@ export async function organizeImportFurni(input: {
|
||||
);
|
||||
let destinationCaption: string | null = null;
|
||||
if (destinationPageId) {
|
||||
const [captionRows] = (await db.execute(sql`
|
||||
const captionRows = await queryRows<{ caption: string }>(sql`
|
||||
SELECT caption FROM catalog_pages WHERE id = ${destinationPageId}
|
||||
`)) as unknown as [Array<{ caption: string }>, unknown];
|
||||
`);
|
||||
destinationCaption = captionRows[0]?.caption ?? null;
|
||||
}
|
||||
const caption =
|
||||
@@ -465,11 +465,11 @@ export async function organizeImportFurni(input: {
|
||||
|
||||
// Existing destinations append after their current offers; freshly
|
||||
// created pages start at order 1.
|
||||
const [maxOrder] = (await db.execute(sql`
|
||||
const maxOrder = await queryRows<{ maxOrder: number }>(sql`
|
||||
SELECT COALESCE(MAX(order_number), 0) AS maxOrder
|
||||
FROM catalog_items WHERE page_id = ${pageId}
|
||||
`)) as unknown as [{ maxOrder: number }, unknown];
|
||||
let nextOrder = Number(maxOrder?.maxOrder ?? 0) + 1;
|
||||
`);
|
||||
let nextOrder = Number(maxOrder[0]?.maxOrder ?? 0) + 1;
|
||||
|
||||
const baseIds = [...new Set(group.items.map((r) => r.itemId))];
|
||||
const bases = await db
|
||||
@@ -493,12 +493,12 @@ export async function organizeImportFurni(input: {
|
||||
];
|
||||
const validMoveIds = new Set<number>();
|
||||
if (moverIds.length > 0) {
|
||||
const [hostRows] = (await db.execute(sql`
|
||||
const hostRows = await queryRows<{ id: number; page_id: number }>(sql`
|
||||
SELECT id, page_id FROM catalog_items WHERE id IN (${sql.join(
|
||||
moverIds.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})
|
||||
`)) as unknown as [Array<{ id: number; page_id: number }>, unknown];
|
||||
`);
|
||||
for (const row of hostRows) {
|
||||
if (importPageIds.has(Number(row.page_id))) {
|
||||
validMoveIds.add(Number(row.id));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
import { db, User } from "@/lib/db";
|
||||
import { db, queryRows, User } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { adminAction } from "@/lib/safe-action";
|
||||
import { ActionError, actionOk } from "@/lib/safe-action-shared";
|
||||
@@ -209,10 +209,9 @@ export const setRank = adminAction(
|
||||
|
||||
let rankExists: { id: number }[] = [];
|
||||
try {
|
||||
const [rows] = await db.execute(
|
||||
rankExists = await queryRows<{ id: number }>(
|
||||
sql`SELECT id FROM permission_ranks WHERE id = ${ctx.data.rank} LIMIT 1`,
|
||||
);
|
||||
rankExists = rows as unknown as { id: number }[];
|
||||
} catch {
|
||||
rankExists = [];
|
||||
}
|
||||
|
||||
@@ -297,10 +297,7 @@ export async function removeFriendship(formData: FormData): Promise<void> {
|
||||
),
|
||||
);
|
||||
|
||||
return Number(
|
||||
(result as unknown as [{ affectedRows: number }])[0]
|
||||
?.affectedRows ?? 0,
|
||||
);
|
||||
return Number(result[0]?.affectedRows ?? 0);
|
||||
});
|
||||
|
||||
outcome = deleted > 0 ? "removed" : "not_found";
|
||||
|
||||
@@ -16,24 +16,26 @@ vi.mock("next/navigation", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
select: vi.fn(() => ({
|
||||
from: vi.fn(() => ({
|
||||
where: vi.fn(() => ({
|
||||
limit: selectLimit,
|
||||
vi.mock("@/lib/db", () => {
|
||||
return {
|
||||
db: {
|
||||
select: vi.fn(() => ({
|
||||
from: vi.fn(() => ({
|
||||
where: vi.fn(() => ({
|
||||
limit: selectLimit,
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
insert: vi.fn(() => ({
|
||||
values: vi.fn(() => ({
|
||||
onDuplicateKeyUpdate: insertOnDup,
|
||||
insert: vi.fn(() => ({
|
||||
values: vi.fn(() => ({
|
||||
onDuplicateKeyUpdate: insertOnDup,
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
},
|
||||
User: { id: "id", mail: "mail" },
|
||||
PasswordReset: { email: "email", token: "token", createdAt: "createdAt" },
|
||||
}));
|
||||
},
|
||||
User: { id: "id", mail: "mail" },
|
||||
PasswordReset: { email: "email", token: "token", createdAt: "createdAt" },
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/lib/services/email", () => ({
|
||||
sendMail: mockSendMail,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { and, count, eq, inArray, sql } from "drizzle-orm";
|
||||
import type { ResultSetHeader } from "mysql2";
|
||||
import { revalidateTag } from "next/cache";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
@@ -254,9 +253,9 @@ export const repairAdminNavAclGrants = adminAction(
|
||||
`);
|
||||
|
||||
const inserted =
|
||||
Number((dashboardFillResult as ResultSetHeader).affectedRows) +
|
||||
Number((midRankViewsResult as ResultSetHeader).affectedRows) +
|
||||
Number((highRankToolsResult as ResultSetHeader).affectedRows);
|
||||
Number(dashboardFillResult.affectedRows) +
|
||||
Number(midRankViewsResult.affectedRows) +
|
||||
Number(highRankToolsResult.affectedRows);
|
||||
|
||||
await logStaffActivity({
|
||||
staffId: ctx.session.user.id,
|
||||
|
||||
@@ -79,32 +79,30 @@ const hoistedUpdateSet = vi.hoisted(() =>
|
||||
vi.fn(() => ({ where: hoistedUpdateWhere })),
|
||||
);
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
select: vi.fn(() => ({ from: hoistedSelectFrom })),
|
||||
insert: vi.fn(() => ({ values: hoistedInsertValues })),
|
||||
update: vi.fn(() => ({ set: hoistedUpdateSet })),
|
||||
},
|
||||
User: {
|
||||
id: "user.id",
|
||||
twoFactorSecret: "user.twoFactorSecret",
|
||||
twoFactorConfirmedAt: "user.twoFactorConfirmedAt",
|
||||
twoFactorRecoveryCodes: "user.twoFactorRecoveryCodes",
|
||||
},
|
||||
}));
|
||||
vi.mock("@/lib/db", () => {
|
||||
return {
|
||||
db: {
|
||||
select: vi.fn(() => ({ from: hoistedSelectFrom })),
|
||||
insert: vi.fn(() => ({ values: hoistedInsertValues })),
|
||||
update: vi.fn(() => ({ set: hoistedUpdateSet })),
|
||||
},
|
||||
User: {
|
||||
id: "user.id",
|
||||
twoFactorSecret: "user.twoFactorSecret",
|
||||
twoFactorConfirmedAt: "user.twoFactorConfirmedAt",
|
||||
twoFactorRecoveryCodes: "user.twoFactorRecoveryCodes",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import { db, User } from "@/lib/db";
|
||||
import { fakeForm } from "@/test/fake-form";
|
||||
import {
|
||||
beginTwoFactor,
|
||||
confirmTwoFactor,
|
||||
disableTwoFactor,
|
||||
} from "./twofactor";
|
||||
|
||||
const fakeForm = (data: Record<string, string>) =>
|
||||
({
|
||||
get: (key: string) => data[key] ?? null,
|
||||
}) as unknown as FormData;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockAuth.mockResolvedValue({ user: { id: "42" } });
|
||||
|
||||
@@ -6,6 +6,8 @@ vi.mock("next/server", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
import { fakeForm } from "@/test/fake-form";
|
||||
|
||||
vi.mock("next-auth", () => ({
|
||||
default: vi.fn(() => ({
|
||||
handlers: {},
|
||||
@@ -82,11 +84,6 @@ vi.mock("next/cache", () => ({
|
||||
import { db } from "@/lib/db";
|
||||
import { redeem } from "./voucher";
|
||||
|
||||
const fakeForm = (data: Record<string, string>) =>
|
||||
({
|
||||
get: (key: string) => data[key] ?? null,
|
||||
}) as unknown as FormData;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockAuth.mockResolvedValue({ user: { id: "1" } });
|
||||
|
||||
Reference in new issue
Block a user