import bcrypt from "bcryptjs"; import { beforeEach, describe, expect, it } from "vitest"; import { createUser, validateCreateUserInput, verifyUserCredentials } from "@/lib/users"; import { prisma } from "@/lib/prisma"; // MIN_PASSWORD_LENGTH chosen in src/lib/users.ts is 8 — mirrored here as a // local constant so the "too short" test stays intentional/self-documenting // instead of a magic number. const MIN_PASSWORD_LENGTH = 8; beforeEach(async () => { await prisma.user.deleteMany(); }); describe("createUser", () => { it("hashes the password instead of storing it in plaintext", async () => { const password = "correct-horse-battery-staple"; await createUser({ email: "it-staff@example.com", name: "IT Staff", password }); const row = await prisma.user.findUnique({ where: { email: "it-staff@example.com" } }); expect(row).not.toBeNull(); expect(row?.passwordHash).not.toBe(password); expect(row?.passwordHash.length).toBeGreaterThan(0); await expect(bcrypt.compare(password, row?.passwordHash ?? "")).resolves.toBe(true); }); it("does not expose the password hash on the returned record", async () => { const created = await createUser({ email: "no-hash-leak@example.com", name: "No Hash Leak", password: "correct-horse-battery-staple", }); expect(created).not.toHaveProperty("passwordHash"); expect(created).not.toHaveProperty("password_hash"); expect(created).toMatchObject({ email: "no-hash-leak@example.com", name: "No Hash Leak" }); }); }); describe("validateCreateUserInput", () => { it("rejects an invalid email format", () => { const result = validateCreateUserInput({ email: "not-an-email", name: "Someone", password: "a-long-enough-password", }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toMatch(/email/i); } }); it(`rejects a password shorter than ${MIN_PASSWORD_LENGTH} characters`, () => { const result = validateCreateUserInput({ email: "short-pw@example.com", name: "Someone", password: "a".repeat(MIN_PASSWORD_LENGTH - 1), }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toMatch(/password/i); } }); it("accepts a valid email, non-empty name, and long-enough password", () => { const result = validateCreateUserInput({ email: "valid@example.com", name: "Valid Name", password: "a".repeat(MIN_PASSWORD_LENGTH), }); expect(result.ok).toBe(true); }); it("rejects an empty name", () => { const result = validateCreateUserInput({ email: "valid@example.com", name: " ", password: "a".repeat(MIN_PASSWORD_LENGTH), }); expect(result.ok).toBe(false); }); }); describe("verifyUserCredentials", () => { const email = "login-test@example.com"; const password = "correct-horse-battery-staple"; beforeEach(async () => { await createUser({ email, name: "Login Test", password }); }); it("succeeds with the correct email and password", async () => { const user = await verifyUserCredentials(email, password); expect(user).not.toBeNull(); expect(user).toMatchObject({ email, name: "Login Test" }); }); it("fails with the wrong password", async () => { const user = await verifyUserCredentials(email, "wrong-password"); expect(user).toBeNull(); }); it("fails with a nonexistent email", async () => { const user = await verifyUserCredentials("nobody@example.com", password); expect(user).toBeNull(); }); it("fails with non-string email/password input (e.g. from a malformed request)", async () => { expect(await verifyUserCredentials(undefined, password)).toBeNull(); expect(await verifyUserCredentials(email, undefined)).toBeNull(); expect(await verifyUserCredentials(123, password)).toBeNull(); }); });