/** * Unit tests for setUserPassword (admin password reset). * * Tests cover: * - Happy path: passwordHash is updated in the DB * - Audit entry "Password reset by admin" is created * - Non-existent user throws NOT_FOUND * - Short password is rejected by Zod schema before reaching the function */ import { beforeEach, describe, expect, it, vi } from "vitest"; import { setUserPassword, SetUserPasswordInputSchema } from "../router/user-procedure-support.js"; // ── Mocks ──────────────────────────────────────────────────────────────────── vi.mock("../lib/audit.js", () => ({ createAuditEntry: vi.fn(), })); const hashMock = vi.hoisted(() => vi.fn().mockResolvedValue("$argon2id$hashed")); vi.mock("@node-rs/argon2", () => ({ hash: hashMock })); // ── Helpers ────────────────────────────────────────────────────────────────── function makeCtx(userRow: Record | null = null) { return { db: { user: { findUnique: vi.fn().mockResolvedValue(userRow), update: vi.fn().mockResolvedValue({}), }, activeSession: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), }, } as never, dbUser: { id: "admin_1" }, }; } const EXISTING_USER = { id: "user_1", name: "Alice", email: "alice@example.com" }; // ── Tests ──────────────────────────────────────────────────────────────────── describe("setUserPassword — happy path", () => { beforeEach(() => { vi.clearAllMocks(); }); it("hashes the new password and updates the DB", async () => { const ctx = makeCtx(EXISTING_USER); const result = await setUserPassword(ctx, { userId: "user_1", password: "NewPassword123!" }); expect(hashMock).toHaveBeenCalledWith("NewPassword123!"); expect(ctx.db.user.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "user_1" }, data: { passwordHash: "$argon2id$hashed" }, }), ); expect(result).toEqual({ success: true }); }); it("creates an audit entry with summary 'Password reset by admin'", async () => { const { createAuditEntry } = await import("../lib/audit.js"); const ctx = makeCtx(EXISTING_USER); await setUserPassword(ctx, { userId: "user_1", password: "NewPassword123!" }); // createAuditEntry is called fire-and-forget (void), so we give microtasks a tick await Promise.resolve(); expect(createAuditEntry).toHaveBeenCalledWith( expect.objectContaining({ summary: "Password reset by admin" }), ); }); }); describe("setUserPassword — not found", () => { beforeEach(() => { vi.clearAllMocks(); }); it("throws when the user does not exist", async () => { const ctx = makeCtx(null); // findUnique returns null await expect( setUserPassword(ctx, { userId: "ghost", password: "NewPassword123!" }), ).rejects.toThrow(); }); }); describe("SetUserPasswordInputSchema — validation", () => { it("accepts a valid input", () => { const result = SetUserPasswordInputSchema.safeParse({ userId: "u1", password: "ValidPass123!", }); expect(result.success).toBe(true); }); it("rejects a password shorter than 12 characters", () => { const result = SetUserPasswordInputSchema.safeParse({ userId: "u1", password: "short" }); expect(result.success).toBe(false); }); it("rejects missing userId", () => { const result = SetUserPasswordInputSchema.safeParse({ password: "Valid123!" }); expect(result.success).toBe(false); }); });