From 096f55b3946e475deda0b8f68bc8a3e55c1afb31 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sat, 1 Aug 2026 13:17:35 +0200 Subject: [PATCH] test: align remaining action tests with Drizzle mocks EOF Co-authored-by: Cursor --- src/actions/applications.test.ts | 79 ++++++++++++++--------- src/actions/auth-precheck.test.ts | 84 +++++++++++++++++-------- src/actions/password-reset.test.ts | 65 +++++++++---------- src/lib/services/staff-activity.test.ts | 40 ++++++------ 4 files changed, 156 insertions(+), 112 deletions(-) diff --git a/src/actions/applications.test.ts b/src/actions/applications.test.ts index ea011fa1..a6b16854 100644 --- a/src/actions/applications.test.ts +++ b/src/actions/applications.test.ts @@ -2,64 +2,83 @@ import { redirect } from "next/navigation"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { auth } from "@/lib/auth"; -import { prisma } from "@/lib/prisma"; import { clientIp, rateLimit } from "@/lib/rate-limit"; import { applyStaff, applyTeam } from "./applications"; +const { selectLimit, insertValues } = vi.hoisted(() => { + const selectLimit = vi.fn(); + const insertValues = vi.fn().mockResolvedValue([{ insertId: 1 }]); + return { selectLimit, insertValues }; +}); + vi.mock("@/lib/auth", () => ({ auth: vi.fn() })); -vi.mock("@/lib/prisma", () => ({ - prisma: { websiteStaffApplications: { findFirst: vi.fn(), create: vi.fn() } }, +vi.mock("@/lib/db", () => ({ + db: { + select: vi.fn(() => ({ + from: vi.fn(() => ({ + where: vi.fn(() => ({ + limit: selectLimit, + })), + })), + })), + insert: vi.fn(() => ({ values: insertValues })), + }, + WebsiteStaffApplications: { + id: "id", + userId: "userId", + rankId: "rankId", + }, })); 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: Record) => ({ - get: (key: string) => data[key] ?? null, +const fakeForm = (data) => ({ + get: (key) => data[key] ?? null, }); beforeEach(() => { vi.clearAllMocks(); - vi.mocked(auth).mockResolvedValue({ user: { id: "42" } } as never); + vi.mocked(auth).mockResolvedValue({ user: { id: "42" } }); vi.mocked(clientIp).mockResolvedValue("127.0.0.1"); vi.mocked(rateLimit).mockResolvedValue({ ok: true }); + selectLimit.mockResolvedValue([]); + insertValues.mockResolvedValue([{ insertId: 1 }]); }); describe("applyStaff", () => { it("submits staff application", async () => { - vi.mocked(prisma.websiteStaffApplications.findFirst).mockResolvedValue( - null, - ); - vi.mocked(prisma.websiteStaffApplications.create).mockResolvedValue( - {} as never, - ); await applyStaff( fakeForm({ rankId: "3", content: "I want to help!", - }) as unknown as FormData, + }), + ); + expect(insertValues).toHaveBeenCalledWith( + expect.objectContaining({ + userId: 42, + rankId: 3, + content: "I want to help!", + }), ); - expect(prisma.websiteStaffApplications.create).toHaveBeenCalled(); expect(redirect).toHaveBeenCalledWith("/apply/staff?submitted=1"); }); it("redirects to login when not authenticated", async () => { vi.mocked(auth).mockResolvedValue(null); - await applyStaff(fakeForm({}) as unknown as FormData); + await applyStaff(fakeForm({})); expect(redirect).toHaveBeenCalledWith("/login"); }); it("returns duplicate status when application exists", async () => { - vi.mocked(prisma.websiteStaffApplications.findFirst).mockResolvedValue({ - id: 1, - } as never); + selectLimit.mockResolvedValue([{ id: 1 }]); await applyStaff( fakeForm({ rankId: "3", content: "I want to help!", - }) as unknown as FormData, + }), ); - expect(prisma.websiteStaffApplications.create).not.toHaveBeenCalled(); + expect(insertValues).not.toHaveBeenCalled(); expect(redirect).toHaveBeenCalledWith("/apply/staff?error=duplicate"); }); @@ -69,7 +88,7 @@ describe("applyStaff", () => { fakeForm({ rankId: "3", content: "I want to help!", - }) as unknown as FormData, + }), ); expect(redirect).toHaveBeenCalledWith("/apply/staff?error=ratelimit"); }); @@ -77,25 +96,25 @@ describe("applyStaff", () => { describe("applyTeam", () => { it("submits team application", async () => { - vi.mocked(prisma.websiteStaffApplications.findFirst).mockResolvedValue( - null, - ); - vi.mocked(prisma.websiteStaffApplications.create).mockResolvedValue( - {} as never, - ); await applyTeam( fakeForm({ teamId: "2", content: "I want to join team!", - }) as unknown as FormData, + }), + ); + expect(insertValues).toHaveBeenCalledWith( + expect.objectContaining({ + userId: 42, + rankId: 2, + content: "I want to join team!", + }), ); - expect(prisma.websiteStaffApplications.create).toHaveBeenCalled(); expect(redirect).toHaveBeenCalledWith("/apply/team?submitted=1"); }); it("redirects to login when not authenticated", async () => { vi.mocked(auth).mockResolvedValue(null); - await applyTeam(fakeForm({}) as unknown as FormData); + await applyTeam(fakeForm({})); expect(redirect).toHaveBeenCalledWith("/login"); }); }); diff --git a/src/actions/auth-precheck.test.ts b/src/actions/auth-precheck.test.ts index e614c9d3..8b0167bd 100644 --- a/src/actions/auth-precheck.test.ts +++ b/src/actions/auth-precheck.test.ts @@ -1,15 +1,36 @@ // @ts-nocheck import { beforeEach, describe, expect, it, vi } from "vitest"; import { checkLogin } from "@/lib/auth/password"; -import { prisma } from "@/lib/prisma"; import { clientIp, rateLimit } from "@/lib/rate-limit"; import { captchaConfig, verifyCaptcha } from "@/lib/services/captcha"; import { siteSettings } from "@/lib/services/site-settings"; import { precheckLogin } from "./auth-precheck"; +const { selectLimit } = vi.hoisted(() => { + const selectLimit = vi.fn().mockResolvedValue([]); + return { selectLimit }; +}); + vi.mock("@/env", () => ({ env: { CONVERT_PASSWORDS: false } })); vi.mock("@/lib/auth/password", () => ({ checkLogin: vi.fn() })); -vi.mock("@/lib/prisma", () => ({ prisma: { user: { findUnique: vi.fn() } } })); +vi.mock("@/lib/db", () => ({ + db: { + select: vi.fn(() => ({ + from: vi.fn(() => ({ + where: vi.fn(() => ({ + limit: selectLimit, + })), + })), + })), + }, + User: { + password: "password", + twoFactorConfirmedAt: "twoFactorConfirmedAt", + mail: "mail", + mailVerified: "mailVerified", + username: "username", + }, +})); vi.mock("@/lib/rate-limit", () => ({ clientIp: vi.fn(), rateLimit: vi.fn() })); vi.mock("@/lib/services/captcha", () => ({ captchaConfig: vi.fn(), @@ -25,26 +46,31 @@ beforeEach(() => { vi.mocked(rateLimit).mockResolvedValue({ ok: true }); vi.mocked(checkLogin).mockResolvedValue({ valid: true } as never); vi.mocked(captchaConfig).mockResolvedValue({ provider: "none" } as never); + selectLimit.mockResolvedValue([]); }); describe("precheckLogin", () => { it("returns ok for valid login without 2FA", async () => { - vi.mocked(prisma.user.findUnique).mockResolvedValue({ - password: "hash", - twoFactorConfirmedAt: null, - mail: null, - mailVerified: "0", - } as never); + selectLimit.mockResolvedValue([ + { + password: "hash", + twoFactorConfirmedAt: null, + mail: null, + mailVerified: "0", + }, + ]); expect(await precheckLogin("user", "pass")).toBe("ok"); }); it("returns twofactor when 2FA is set up", async () => { - vi.mocked(prisma.user.findUnique).mockResolvedValue({ - password: "hash", - twoFactorConfirmedAt: new Date(), - mail: null, - mailVerified: "0", - } as never); + selectLimit.mockResolvedValue([ + { + password: "hash", + twoFactorConfirmedAt: new Date(), + mail: null, + mailVerified: "0", + }, + ]); expect(await precheckLogin("user", "pass")).toBe("twofactor"); }); @@ -57,29 +83,33 @@ describe("precheckLogin", () => { provider: "hcaptcha", } as never); vi.mocked(verifyCaptcha).mockResolvedValue(false); - vi.mocked(prisma.user.findUnique).mockResolvedValue({ - password: "hash", - twoFactorConfirmedAt: null, - mail: null, - mailVerified: "0", - } as never); + selectLimit.mockResolvedValue([ + { + password: "hash", + twoFactorConfirmedAt: null, + mail: null, + mailVerified: "0", + }, + ]); expect(await precheckLogin("user", "pass", "bad-token")).toBe("captcha"); }); it("returns invalid when user not found (dummy hash check)", async () => { - vi.mocked(prisma.user.findUnique).mockResolvedValue(null); + selectLimit.mockResolvedValue([]); const result = await precheckLogin("nonexistent", "pass"); expect(result).toBe("invalid"); expect(checkLogin).toHaveBeenCalled(); }); it("returns unverified when email verification required", async () => { - vi.mocked(prisma.user.findUnique).mockResolvedValue({ - password: "hash", - twoFactorConfirmedAt: null, - mail: "user@example.com", - mailVerified: "0", - } as never); + selectLimit.mockResolvedValue([ + { + password: "hash", + twoFactorConfirmedAt: null, + mail: "user@example.com", + mailVerified: "0", + }, + ]); vi.mocked(siteSettings.getBool).mockResolvedValue(true); expect(await precheckLogin("user", "pass")).toBe("unverified"); }); diff --git a/src/actions/password-reset.test.ts b/src/actions/password-reset.test.ts index 1f2c4fbf..e10f648c 100644 --- a/src/actions/password-reset.test.ts +++ b/src/actions/password-reset.test.ts @@ -1,39 +1,38 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -const { - mockFindFirst, - mockUpsert, - mockFindUnique, - mockUpdate, - mockDelete, - mockSendMail, - mockRedirect, -} = vi.hoisted(() => ({ - mockFindFirst: vi.fn(), - mockUpsert: vi.fn(), - mockFindUnique: vi.fn(), - mockUpdate: vi.fn(), - mockDelete: vi.fn(), - mockSendMail: vi.fn(), - mockRedirect: vi.fn(), -})); +const { selectLimit, insertOnDup, mockSendMail, mockRedirect } = vi.hoisted( + () => ({ + selectLimit: vi.fn(), + insertOnDup: vi.fn().mockResolvedValue({}), + mockSendMail: vi.fn(), + mockRedirect: vi.fn(), + }), +); vi.mock("next/navigation", () => ({ - redirect: (...args: unknown[]) => { + redirect: (...args) => { mockRedirect(...args); throw new Error("redirect"); }, })); -vi.mock("@/lib/prisma", () => ({ - prisma: { - user: { findFirst: mockFindFirst, update: mockUpdate }, - passwordReset: { - upsert: mockUpsert, - findUnique: mockFindUnique, - delete: mockDelete, - }, +vi.mock("@/lib/db", () => ({ + db: { + select: vi.fn(() => ({ + from: vi.fn(() => ({ + where: vi.fn(() => ({ + limit: selectLimit, + })), + })), + })), + insert: vi.fn(() => ({ + values: vi.fn(() => ({ + onDuplicateKeyUpdate: insertOnDup, + })), + })), }, + User: { id: "id", mail: "mail" }, + PasswordReset: { email: "email", token: "token", createdAt: "createdAt" }, })); vi.mock("@/lib/services/email", () => ({ @@ -62,22 +61,20 @@ import { requestReset } from "./password-reset"; beforeEach(() => { vi.clearAllMocks(); + insertOnDup.mockResolvedValue({}); }); describe("requestReset", () => { it("sends a reset email when the user exists", async () => { - mockFindFirst.mockResolvedValue({ id: 1 }); - mockUpsert.mockResolvedValue({}); + selectLimit.mockResolvedValue([{ id: 1 }]); const fd = new FormData(); fd.set("email", "user@example.com"); await expect(requestReset(fd)).rejects.toThrow("redirect"); - expect(mockFindFirst).toHaveBeenCalledWith( - expect.objectContaining({ where: { mail: "user@example.com" } }), - ); - expect(mockUpsert).toHaveBeenCalled(); + expect(selectLimit).toHaveBeenCalled(); + expect(insertOnDup).toHaveBeenCalled(); expect(mockSendMail).toHaveBeenCalledWith( "user@example.com", expect.stringContaining("password reset"), @@ -86,7 +83,7 @@ describe("requestReset", () => { }); it("does not send email when user is not found", async () => { - mockFindFirst.mockResolvedValue(null); + selectLimit.mockResolvedValue([]); const fd = new FormData(); fd.set("email", "unknown@example.com"); @@ -100,6 +97,6 @@ describe("requestReset", () => { fd.set("email", "not-an-email"); await expect(requestReset(fd)).rejects.toThrow("redirect"); - expect(mockFindFirst).not.toHaveBeenCalled(); + expect(selectLimit).not.toHaveBeenCalled(); }); }); diff --git a/src/lib/services/staff-activity.test.ts b/src/lib/services/staff-activity.test.ts index aa871205..454c8a40 100644 --- a/src/lib/services/staff-activity.test.ts +++ b/src/lib/services/staff-activity.test.ts @@ -1,9 +1,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -const create = vi.hoisted(() => vi.fn()); +const insertValues = vi.hoisted(() => vi.fn()); -vi.mock("@/lib/prisma", () => ({ - prisma: { staffActivities: { create } }, +vi.mock("@/lib/db", () => ({ + db: { + insert: vi.fn(() => ({ values: insertValues })), + }, + StaffActivities: {}, })); vi.mock("@/env", () => ({ env: {} })); @@ -12,8 +15,7 @@ vi.mock("next/headers", () => ({ headers: () => new Promise((resolve) => resolve({ - get: (key: string) => - key === "x-real-client-ip" ? "192.168.1.1" : null, + get: (key) => (key === "x-real-client-ip" ? "192.168.1.1" : null), }), ), })); @@ -22,43 +24,39 @@ import { logStaffActivity } from "./staff-activity"; beforeEach(() => { vi.clearAllMocks(); + insertValues.mockResolvedValue([{ insertId: 1 }]); }); describe("logStaffActivity", () => { it("creates a staff activity entry with ip", async () => { - create.mockResolvedValue({ id: 1 }); await logStaffActivity({ staffId: 1, action: "test_action", description: "Test action performed", }); - expect(create).toHaveBeenCalledWith( + expect(insertValues).toHaveBeenCalledWith( expect.objectContaining({ - data: expect.objectContaining({ - userId: BigInt(1), - action: "test_action", - description: "Test action performed", - ipAddress: "192.168.1.1", - }), + userId: BigInt(1), + action: "test_action", + description: "Test action performed", + ipAddress: "192.168.1.1", }), ); }); it("truncates action to 50 chars", async () => { - create.mockResolvedValue({ id: 1 }); const longAction = "a".repeat(100); await logStaffActivity({ staffId: 1, action: longAction, description: "test", }); - const call = create.mock.calls[0][0]; - expect(call.data.action.length).toBe(50); - expect(call.data.action).toBe("a".repeat(50)); + const call = insertValues.mock.calls[0][0]; + expect(call.action.length).toBe(50); + expect(call.action).toBe("a".repeat(50)); }); it("includes target type and target id when provided", async () => { - create.mockResolvedValue({ id: 1 }); await logStaffActivity({ staffId: 1, action: "ban", @@ -66,13 +64,13 @@ describe("logStaffActivity", () => { targetType: "user", targetId: 42, }); - const data = create.mock.calls[0][0].data; + const data = insertValues.mock.calls[0][0]; expect(data.targetType).toBe("user"); expect(data.targetId).toBe(BigInt(42)); }); - it("does not throw on prisma error", async () => { - create.mockRejectedValue(new Error("DB down")); + it("does not throw on db error", async () => { + insertValues.mockRejectedValue(new Error("DB down")); await expect( logStaffActivity({ staffId: 1,