test: align remaining action tests with Drizzle mocks

EOF

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-08-01 13:17:35 +02:00
1 parent ed9c23c702
commit 096f55b394
4 files changed
+156 -112

No files matched your search

+49 -30
View File
@@ -2,64 +2,83 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { clientIp, rateLimit } from "@/lib/rate-limit"; import { clientIp, rateLimit } from "@/lib/rate-limit";
import { applyStaff, applyTeam } from "./applications"; 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/auth", () => ({ auth: vi.fn() }));
vi.mock("@/lib/prisma", () => ({ vi.mock("@/lib/db", () => ({
prisma: { websiteStaffApplications: { findFirst: vi.fn(), create: vi.fn() } }, 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("@/lib/rate-limit", () => ({ clientIp: vi.fn(), rateLimit: vi.fn() }));
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() })); vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
vi.mock("next/navigation", () => ({ redirect: vi.fn() })); vi.mock("next/navigation", () => ({ redirect: vi.fn() }));
const fakeForm = (data: Record<string, string>) => ({ const fakeForm = (data) => ({
get: (key: string) => data[key] ?? null, get: (key) => data[key] ?? null,
}); });
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); 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(clientIp).mockResolvedValue("127.0.0.1");
vi.mocked(rateLimit).mockResolvedValue({ ok: true }); vi.mocked(rateLimit).mockResolvedValue({ ok: true });
selectLimit.mockResolvedValue([]);
insertValues.mockResolvedValue([{ insertId: 1 }]);
}); });
describe("applyStaff", () => { describe("applyStaff", () => {
it("submits staff application", async () => { it("submits staff application", async () => {
vi.mocked(prisma.websiteStaffApplications.findFirst).mockResolvedValue(
null,
);
vi.mocked(prisma.websiteStaffApplications.create).mockResolvedValue(
{} as never,
);
await applyStaff( await applyStaff(
fakeForm({ fakeForm({
rankId: "3", rankId: "3",
content: "I want to help!", 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"); expect(redirect).toHaveBeenCalledWith("/apply/staff?submitted=1");
}); });
it("redirects to login when not authenticated", async () => { it("redirects to login when not authenticated", async () => {
vi.mocked(auth).mockResolvedValue(null); vi.mocked(auth).mockResolvedValue(null);
await applyStaff(fakeForm({}) as unknown as FormData); await applyStaff(fakeForm({}));
expect(redirect).toHaveBeenCalledWith("/login"); expect(redirect).toHaveBeenCalledWith("/login");
}); });
it("returns duplicate status when application exists", async () => { it("returns duplicate status when application exists", async () => {
vi.mocked(prisma.websiteStaffApplications.findFirst).mockResolvedValue({ selectLimit.mockResolvedValue([{ id: 1 }]);
id: 1,
} as never);
await applyStaff( await applyStaff(
fakeForm({ fakeForm({
rankId: "3", rankId: "3",
content: "I want to help!", 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"); expect(redirect).toHaveBeenCalledWith("/apply/staff?error=duplicate");
}); });
@@ -69,7 +88,7 @@ describe("applyStaff", () => {
fakeForm({ fakeForm({
rankId: "3", rankId: "3",
content: "I want to help!", content: "I want to help!",
}) as unknown as FormData, }),
); );
expect(redirect).toHaveBeenCalledWith("/apply/staff?error=ratelimit"); expect(redirect).toHaveBeenCalledWith("/apply/staff?error=ratelimit");
}); });
@@ -77,25 +96,25 @@ describe("applyStaff", () => {
describe("applyTeam", () => { describe("applyTeam", () => {
it("submits team application", async () => { it("submits team application", async () => {
vi.mocked(prisma.websiteStaffApplications.findFirst).mockResolvedValue(
null,
);
vi.mocked(prisma.websiteStaffApplications.create).mockResolvedValue(
{} as never,
);
await applyTeam( await applyTeam(
fakeForm({ fakeForm({
teamId: "2", teamId: "2",
content: "I want to join team!", 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"); expect(redirect).toHaveBeenCalledWith("/apply/team?submitted=1");
}); });
it("redirects to login when not authenticated", async () => { it("redirects to login when not authenticated", async () => {
vi.mocked(auth).mockResolvedValue(null); vi.mocked(auth).mockResolvedValue(null);
await applyTeam(fakeForm({}) as unknown as FormData); await applyTeam(fakeForm({}));
expect(redirect).toHaveBeenCalledWith("/login"); expect(redirect).toHaveBeenCalledWith("/login");
}); });
}); });
+57 -27
View File
@@ -1,15 +1,36 @@
// @ts-nocheck // @ts-nocheck
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { checkLogin } from "@/lib/auth/password"; import { checkLogin } from "@/lib/auth/password";
import { prisma } from "@/lib/prisma";
import { clientIp, rateLimit } from "@/lib/rate-limit"; import { clientIp, rateLimit } from "@/lib/rate-limit";
import { captchaConfig, verifyCaptcha } from "@/lib/services/captcha"; import { captchaConfig, verifyCaptcha } from "@/lib/services/captcha";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
import { precheckLogin } from "./auth-precheck"; 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("@/env", () => ({ env: { CONVERT_PASSWORDS: false } }));
vi.mock("@/lib/auth/password", () => ({ checkLogin: vi.fn() })); 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/rate-limit", () => ({ clientIp: vi.fn(), rateLimit: vi.fn() }));
vi.mock("@/lib/services/captcha", () => ({ vi.mock("@/lib/services/captcha", () => ({
captchaConfig: vi.fn(), captchaConfig: vi.fn(),
@@ -25,26 +46,31 @@ beforeEach(() => {
vi.mocked(rateLimit).mockResolvedValue({ ok: true }); vi.mocked(rateLimit).mockResolvedValue({ ok: true });
vi.mocked(checkLogin).mockResolvedValue({ valid: true } as never); vi.mocked(checkLogin).mockResolvedValue({ valid: true } as never);
vi.mocked(captchaConfig).mockResolvedValue({ provider: "none" } as never); vi.mocked(captchaConfig).mockResolvedValue({ provider: "none" } as never);
selectLimit.mockResolvedValue([]);
}); });
describe("precheckLogin", () => { describe("precheckLogin", () => {
it("returns ok for valid login without 2FA", async () => { it("returns ok for valid login without 2FA", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue({ selectLimit.mockResolvedValue([
password: "hash", {
twoFactorConfirmedAt: null, password: "hash",
mail: null, twoFactorConfirmedAt: null,
mailVerified: "0", mail: null,
} as never); mailVerified: "0",
},
]);
expect(await precheckLogin("user", "pass")).toBe("ok"); expect(await precheckLogin("user", "pass")).toBe("ok");
}); });
it("returns twofactor when 2FA is set up", async () => { it("returns twofactor when 2FA is set up", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue({ selectLimit.mockResolvedValue([
password: "hash", {
twoFactorConfirmedAt: new Date(), password: "hash",
mail: null, twoFactorConfirmedAt: new Date(),
mailVerified: "0", mail: null,
} as never); mailVerified: "0",
},
]);
expect(await precheckLogin("user", "pass")).toBe("twofactor"); expect(await precheckLogin("user", "pass")).toBe("twofactor");
}); });
@@ -57,29 +83,33 @@ describe("precheckLogin", () => {
provider: "hcaptcha", provider: "hcaptcha",
} as never); } as never);
vi.mocked(verifyCaptcha).mockResolvedValue(false); vi.mocked(verifyCaptcha).mockResolvedValue(false);
vi.mocked(prisma.user.findUnique).mockResolvedValue({ selectLimit.mockResolvedValue([
password: "hash", {
twoFactorConfirmedAt: null, password: "hash",
mail: null, twoFactorConfirmedAt: null,
mailVerified: "0", mail: null,
} as never); mailVerified: "0",
},
]);
expect(await precheckLogin("user", "pass", "bad-token")).toBe("captcha"); expect(await precheckLogin("user", "pass", "bad-token")).toBe("captcha");
}); });
it("returns invalid when user not found (dummy hash check)", async () => { 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"); const result = await precheckLogin("nonexistent", "pass");
expect(result).toBe("invalid"); expect(result).toBe("invalid");
expect(checkLogin).toHaveBeenCalled(); expect(checkLogin).toHaveBeenCalled();
}); });
it("returns unverified when email verification required", async () => { it("returns unverified when email verification required", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue({ selectLimit.mockResolvedValue([
password: "hash", {
twoFactorConfirmedAt: null, password: "hash",
mail: "[email protected]", twoFactorConfirmedAt: null,
mailVerified: "0", mail: "[email protected]",
} as never); mailVerified: "0",
},
]);
vi.mocked(siteSettings.getBool).mockResolvedValue(true); vi.mocked(siteSettings.getBool).mockResolvedValue(true);
expect(await precheckLogin("user", "pass")).toBe("unverified"); expect(await precheckLogin("user", "pass")).toBe("unverified");
}); });
+31 -34
View File
@@ -1,39 +1,38 @@
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
const { const { selectLimit, insertOnDup, mockSendMail, mockRedirect } = vi.hoisted(
mockFindFirst, () => ({
mockUpsert, selectLimit: vi.fn(),
mockFindUnique, insertOnDup: vi.fn().mockResolvedValue({}),
mockUpdate, mockSendMail: vi.fn(),
mockDelete, mockRedirect: vi.fn(),
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(),
}));
vi.mock("next/navigation", () => ({ vi.mock("next/navigation", () => ({
redirect: (...args: unknown[]) => { redirect: (...args) => {
mockRedirect(...args); mockRedirect(...args);
throw new Error("redirect"); throw new Error("redirect");
}, },
})); }));
vi.mock("@/lib/prisma", () => ({ vi.mock("@/lib/db", () => ({
prisma: { db: {
user: { findFirst: mockFindFirst, update: mockUpdate }, select: vi.fn(() => ({
passwordReset: { from: vi.fn(() => ({
upsert: mockUpsert, where: vi.fn(() => ({
findUnique: mockFindUnique, limit: selectLimit,
delete: mockDelete, })),
}, })),
})),
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", () => ({ vi.mock("@/lib/services/email", () => ({
@@ -62,22 +61,20 @@ import { requestReset } from "./password-reset";
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
insertOnDup.mockResolvedValue({});
}); });
describe("requestReset", () => { describe("requestReset", () => {
it("sends a reset email when the user exists", async () => { it("sends a reset email when the user exists", async () => {
mockFindFirst.mockResolvedValue({ id: 1 }); selectLimit.mockResolvedValue([{ id: 1 }]);
mockUpsert.mockResolvedValue({});
const fd = new FormData(); const fd = new FormData();
fd.set("email", "[email protected]"); fd.set("email", "[email protected]");
await expect(requestReset(fd)).rejects.toThrow("redirect"); await expect(requestReset(fd)).rejects.toThrow("redirect");
expect(mockFindFirst).toHaveBeenCalledWith( expect(selectLimit).toHaveBeenCalled();
expect.objectContaining({ where: { mail: "[email protected]" } }), expect(insertOnDup).toHaveBeenCalled();
);
expect(mockUpsert).toHaveBeenCalled();
expect(mockSendMail).toHaveBeenCalledWith( expect(mockSendMail).toHaveBeenCalledWith(
"[email protected]", "[email protected]",
expect.stringContaining("password reset"), expect.stringContaining("password reset"),
@@ -86,7 +83,7 @@ describe("requestReset", () => {
}); });
it("does not send email when user is not found", async () => { it("does not send email when user is not found", async () => {
mockFindFirst.mockResolvedValue(null); selectLimit.mockResolvedValue([]);
const fd = new FormData(); const fd = new FormData();
fd.set("email", "[email protected]"); fd.set("email", "[email protected]");
@@ -100,6 +97,6 @@ describe("requestReset", () => {
fd.set("email", "not-an-email"); fd.set("email", "not-an-email");
await expect(requestReset(fd)).rejects.toThrow("redirect"); await expect(requestReset(fd)).rejects.toThrow("redirect");
expect(mockFindFirst).not.toHaveBeenCalled(); expect(selectLimit).not.toHaveBeenCalled();
}); });
}); });
+19 -21
View File
@@ -1,9 +1,12 @@
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
const create = vi.hoisted(() => vi.fn()); const insertValues = vi.hoisted(() => vi.fn());
vi.mock("@/lib/prisma", () => ({ vi.mock("@/lib/db", () => ({
prisma: { staffActivities: { create } }, db: {
insert: vi.fn(() => ({ values: insertValues })),
},
StaffActivities: {},
})); }));
vi.mock("@/env", () => ({ env: {} })); vi.mock("@/env", () => ({ env: {} }));
@@ -12,8 +15,7 @@ vi.mock("next/headers", () => ({
headers: () => headers: () =>
new Promise((resolve) => new Promise((resolve) =>
resolve({ resolve({
get: (key: string) => get: (key) => (key === "x-real-client-ip" ? "192.168.1.1" : null),
key === "x-real-client-ip" ? "192.168.1.1" : null,
}), }),
), ),
})); }));
@@ -22,43 +24,39 @@ import { logStaffActivity } from "./staff-activity";
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
insertValues.mockResolvedValue([{ insertId: 1 }]);
}); });
describe("logStaffActivity", () => { describe("logStaffActivity", () => {
it("creates a staff activity entry with ip", async () => { it("creates a staff activity entry with ip", async () => {
create.mockResolvedValue({ id: 1 });
await logStaffActivity({ await logStaffActivity({
staffId: 1, staffId: 1,
action: "test_action", action: "test_action",
description: "Test action performed", description: "Test action performed",
}); });
expect(create).toHaveBeenCalledWith( expect(insertValues).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
data: expect.objectContaining({ userId: BigInt(1),
userId: BigInt(1), action: "test_action",
action: "test_action", description: "Test action performed",
description: "Test action performed", ipAddress: "192.168.1.1",
ipAddress: "192.168.1.1",
}),
}), }),
); );
}); });
it("truncates action to 50 chars", async () => { it("truncates action to 50 chars", async () => {
create.mockResolvedValue({ id: 1 });
const longAction = "a".repeat(100); const longAction = "a".repeat(100);
await logStaffActivity({ await logStaffActivity({
staffId: 1, staffId: 1,
action: longAction, action: longAction,
description: "test", description: "test",
}); });
const call = create.mock.calls[0][0]; const call = insertValues.mock.calls[0][0];
expect(call.data.action.length).toBe(50); expect(call.action.length).toBe(50);
expect(call.data.action).toBe("a".repeat(50)); expect(call.action).toBe("a".repeat(50));
}); });
it("includes target type and target id when provided", async () => { it("includes target type and target id when provided", async () => {
create.mockResolvedValue({ id: 1 });
await logStaffActivity({ await logStaffActivity({
staffId: 1, staffId: 1,
action: "ban", action: "ban",
@@ -66,13 +64,13 @@ describe("logStaffActivity", () => {
targetType: "user", targetType: "user",
targetId: 42, targetId: 42,
}); });
const data = create.mock.calls[0][0].data; const data = insertValues.mock.calls[0][0];
expect(data.targetType).toBe("user"); expect(data.targetType).toBe("user");
expect(data.targetId).toBe(BigInt(42)); expect(data.targetId).toBe(BigInt(42));
}); });
it("does not throw on prisma error", async () => { it("does not throw on db error", async () => {
create.mockRejectedValue(new Error("DB down")); insertValues.mockRejectedValue(new Error("DB down"));
await expect( await expect(
logStaffActivity({ logStaffActivity({
staffId: 1, staffId: 1,