/* @vitest-environment node */ import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn(), })); vi.mock("@/lib/db", () => ({ getDb: vi.fn(), })); vi.mock("@/models/user", () => { const USER_ROLES = Object.freeze({ BRANCH: "branch", ADMIN: "admin", SUPERADMIN: "superadmin", DEV: "dev", }); return { default: { find: vi.fn(), findOne: vi.fn(), create: vi.fn(), }, USER_ROLES, }; }); vi.mock("bcryptjs", () => { const hash = vi.fn(); return { default: { hash }, hash, }; }); import { getSession } from "@/lib/auth/session"; import { getDb } from "@/lib/db"; import User from "@/models/user"; import { hash as bcryptHash } from "bcryptjs"; import { GET, POST, dynamic } from "./route.js"; function buildCursor(lastId) { return Buffer.from(JSON.stringify({ v: 1, lastId }), "utf8").toString( "base64url", ); } function createRequestStub(body) { return { async json() { return body; }, }; } describe("GET /api/admin/users", () => { beforeEach(() => { vi.clearAllMocks(); getDb.mockResolvedValue({}); }); it('exports dynamic="force-dynamic"', () => { expect(dynamic).toBe("force-dynamic"); }); it("returns 401 when unauthenticated", async () => { getSession.mockResolvedValue(null); const res = await GET(new Request("http://localhost/api/admin/users")); expect(res.status).toBe(401); expect(await res.json()).toEqual({ error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" }, }); }); it("returns 403 when authenticated but not allowed (admin)", async () => { getSession.mockResolvedValue({ userId: "u1", role: "admin", branchId: null, email: "admin@example.com", }); const res = await GET(new Request("http://localhost/api/admin/users")); expect(res.status).toBe(403); expect(await res.json()).toEqual({ error: { message: "Forbidden", code: "AUTH_FORBIDDEN_USER_MANAGEMENT", }, }); expect(User.find).not.toHaveBeenCalled(); }); it("returns 200 with items and nextCursor (superadmin, limit + cursor)", async () => { getSession.mockResolvedValue({ userId: "u2", role: "superadmin", branchId: null, email: "superadmin@example.com", }); const d1 = { _id: "507f1f77bcf86cd799439013", username: "u3", email: "u3@example.com", role: "admin", branchId: null, mustChangePassword: false, createdAt: new Date("2026-02-01T10:00:00.000Z"), updatedAt: new Date("2026-02-02T10:00:00.000Z"), }; const d2 = { _id: "507f1f77bcf86cd799439012", username: "u2", email: "u2@example.com", role: "branch", branchId: "NL01", mustChangePassword: true, createdAt: new Date("2026-02-01T09:00:00.000Z"), updatedAt: new Date("2026-02-02T09:00:00.000Z"), }; const d3 = { _id: "507f1f77bcf86cd799439011", username: "u1", email: "u1@example.com", role: "dev", branchId: null, mustChangePassword: false, createdAt: new Date("2026-02-01T08:00:00.000Z"), updatedAt: new Date("2026-02-02T08:00:00.000Z"), }; const chain = { sort: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), select: vi.fn().mockReturnThis(), exec: vi.fn().mockResolvedValue([d1, d2, d3]), }; User.find.mockReturnValue(chain); const res = await GET( new Request("http://localhost/api/admin/users?limit=2"), ); expect(res.status).toBe(200); expect(chain.sort).toHaveBeenCalledWith({ _id: -1 }); expect(chain.limit).toHaveBeenCalledWith(3); // limit + 1 const body = await res.json(); expect(body.items).toHaveLength(2); expect(body.nextCursor).toBe(buildCursor("507f1f77bcf86cd799439012")); }); }); describe("POST /api/admin/users", () => { beforeEach(() => { vi.clearAllMocks(); getDb.mockResolvedValue({}); bcryptHash.mockResolvedValue("hashed"); }); it("returns 401 when unauthenticated", async () => { getSession.mockResolvedValue(null); const res = await POST(createRequestStub({})); expect(res.status).toBe(401); expect(await res.json()).toEqual({ error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" }, }); }); it("returns 403 when authenticated but not allowed (admin)", async () => { getSession.mockResolvedValue({ userId: "u1", role: "admin", branchId: null, email: "admin@example.com", }); const res = await POST(createRequestStub({})); expect(res.status).toBe(403); expect(await res.json()).toEqual({ error: { message: "Forbidden", code: "AUTH_FORBIDDEN_USER_MANAGEMENT", }, }); }); it("returns 400 when JSON parsing fails", async () => { getSession.mockResolvedValue({ userId: "u2", role: "superadmin", branchId: null, email: "superadmin@example.com", }); const req = { json: vi.fn().mockRejectedValue(new Error("invalid json")) }; const res = await POST(req); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: { message: "Invalid request body", code: "VALIDATION_INVALID_JSON", }, }); }); it("returns 400 when body is not an object", async () => { getSession.mockResolvedValue({ userId: "u2", role: "superadmin", branchId: null, email: "superadmin@example.com", }); const res = await POST(createRequestStub("nope")); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: { message: "Invalid request body", code: "VALIDATION_INVALID_BODY", }, }); }); it("returns 400 when fields are missing", async () => { getSession.mockResolvedValue({ userId: "u2", role: "dev", branchId: null, email: "dev@example.com", }); const res = await POST(createRequestStub({})); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: { message: "Missing required fields", code: "VALIDATION_MISSING_FIELD", details: { fields: ["username", "email", "role", "initialPassword"] }, }, }); }); it("returns 400 for invalid role", async () => { getSession.mockResolvedValue({ userId: "u2", role: "superadmin", branchId: null, email: "superadmin@example.com", }); const res = await POST( createRequestStub({ username: "newuser", email: "new@example.com", role: "nope", initialPassword: "StrongPassword123", }), ); expect(res.status).toBe(400); const body = await res.json(); expect(body.error.code).toBe("VALIDATION_INVALID_FIELD"); expect(body.error.details.field).toBe("role"); }); it("returns 400 for invalid branchId when role=branch", async () => { getSession.mockResolvedValue({ userId: "u2", role: "superadmin", branchId: null, email: "superadmin@example.com", }); const res = await POST( createRequestStub({ username: "newuser", email: "new@example.com", role: "branch", branchId: "XX1", initialPassword: "StrongPassword123", }), ); expect(res.status).toBe(400); const body = await res.json(); expect(body.error.code).toBe("VALIDATION_INVALID_FIELD"); expect(body.error.details.field).toBe("branchId"); }); it("returns 400 for weak initialPassword", async () => { getSession.mockResolvedValue({ userId: "u2", role: "dev", branchId: null, email: "dev@example.com", }); const res = await POST( createRequestStub({ username: "newuser", email: "new@example.com", role: "admin", initialPassword: "short1", }), ); expect(res.status).toBe(400); const body = await res.json(); expect(body.error.code).toBe("VALIDATION_WEAK_PASSWORD"); expect(body.error.details).toMatchObject({ minLength: 8, requireLetter: true, requireNumber: true, }); expect(Array.isArray(body.error.details.reasons)).toBe(true); }); it("returns 200 and creates user with hashed password + mustChangePassword=true", async () => { getSession.mockResolvedValue({ userId: "u2", role: "superadmin", branchId: null, email: "superadmin@example.com", }); User.findOne.mockImplementation((query) => { return { select: vi.fn().mockReturnThis(), exec: vi.fn().mockResolvedValue(null), }; }); User.create.mockResolvedValue({ _id: "507f1f77bcf86cd799439099", username: "newuser", email: "new@example.com", role: "branch", branchId: "NL01", mustChangePassword: true, createdAt: new Date("2026-02-06T10:00:00.000Z"), updatedAt: new Date("2026-02-06T10:00:00.000Z"), }); const res = await POST( createRequestStub({ username: "NewUser", email: "NEW@EXAMPLE.COM", role: "branch", branchId: "nl01", initialPassword: "StrongPassword123", }), ); expect(res.status).toBe(200); expect(getDb).toHaveBeenCalledTimes(1); expect(bcryptHash).toHaveBeenCalledWith("StrongPassword123", 12); expect(User.findOne).toHaveBeenCalledWith({ username: "newuser" }); expect(User.findOne).toHaveBeenCalledWith({ email: "new@example.com" }); expect(User.create).toHaveBeenCalledWith( expect.objectContaining({ username: "newuser", email: "new@example.com", role: "branch", branchId: "NL01", passwordHash: "hashed", mustChangePassword: true, }), ); const body = await res.json(); expect(body).toMatchObject({ ok: true, user: { id: "507f1f77bcf86cd799439099", username: "newuser", email: "new@example.com", role: "branch", branchId: "NL01", mustChangePassword: true, }, }); }); });