/** * Unit tests for `sendPasswordResetEmail` in `src/actions/admin/users.ts`. * * The action must: * 1. Reject unauthenticated callers * 2. Reject non-platform-admin callers * 3. Trim + lowercase the email before calling Neon Auth * 4. Call `requestPasswordReset` with the correct `redirectTo` * 5. Return success on a clean Neon Auth response * 6. Propagate Neon Auth errors back to the UI */ import { describe, it, expect, vi, beforeEach } from "vitest"; const { mockGetAdminUser, mockRequestPasswordReset, } = vi.hoisted(() => ({ mockGetAdminUser: vi.fn(), mockRequestPasswordReset: vi.fn(), })); vi.mock("server-only", () => ({})); vi.mock("@/lib/admin-permissions", () => ({ getAdminUser: mockGetAdminUser, })); vi.mock("@/lib/auth", () => ({ requestPasswordReset: mockRequestPasswordReset, })); vi.mock("@/lib/db", () => ({ query: vi.fn(), withTx: vi.fn(), })); vi.mock("next/headers", () => ({ cookies: () => Promise.resolve({ get: () => undefined }), })); import { sendPasswordResetEmail } from "@/actions/admin/users"; const PLATFORM_ADMIN = { id: "platform-1", user_id: "platform-1", email: "root@example.com", display_name: "Root", brand_id: null, brand_ids: [], brand_slug: null, role: "platform_admin" as const, active: true, auth_provider: "neon_auth" as const, can_manage_products: true, can_manage_stops: true, can_manage_orders: true, can_manage_pickup: true, can_manage_messages: true, can_manage_refunds: true, can_manage_users: true, can_manage_water_log: true, can_manage_reports: true, can_manage_settings: true, can_manage_billing: true, can_manage_branding: true, can_manage_marketing: true, can_manage_team: true, must_change_password: false, }; const BRAND_ADMIN = { ...PLATFORM_ADMIN, role: "brand_admin" as const, brand_id: "brand-tux" }; beforeEach(() => { vi.clearAllMocks(); process.env.NEXT_PUBLIC_SITE_URL = "http://localhost:4000"; mockGetAdminUser.mockResolvedValue(PLATFORM_ADMIN); mockRequestPasswordReset.mockResolvedValue({ data: null, error: null }); }); describe("sendPasswordResetEmail — authorization", () => { it("rejects unauthenticated callers", async () => { mockGetAdminUser.mockResolvedValue(null); const r = await sendPasswordResetEmail("user@example.com"); expect(r.success).toBe(false); expect(r.error).toMatch(/not authenticated/i); expect(mockRequestPasswordReset).not.toHaveBeenCalled(); }); it("rejects non-platform-admin callers", async () => { mockGetAdminUser.mockResolvedValue(BRAND_ADMIN); const r = await sendPasswordResetEmail("user@example.com"); expect(r.success).toBe(false); expect(r.error).toMatch(/only platform admins/i); expect(mockRequestPasswordReset).not.toHaveBeenCalled(); }); }); describe("sendPasswordResetEmail — input handling", () => { it("rejects an empty email without calling Neon Auth", async () => { const r = await sendPasswordResetEmail(" "); expect(r.success).toBe(false); expect(r.error).toMatch(/required/i); expect(mockRequestPasswordReset).not.toHaveBeenCalled(); }); it("trims and lowercases the email before calling Neon Auth", async () => { await sendPasswordResetEmail(" User@Example.COM "); expect(mockRequestPasswordReset).toHaveBeenCalledWith({ email: "user@example.com", redirectTo: "http://localhost:4000/reset-password", }); }); }); describe("sendPasswordResetEmail — happy path", () => { it("returns success when Neon Auth accepts the request", async () => { mockRequestPasswordReset.mockResolvedValue({ data: { ok: true }, error: null }); const r = await sendPasswordResetEmail("user@example.com"); expect(r.success).toBe(true); expect(r.error).toBeNull(); }); }); describe("sendPasswordResetEmail — error propagation", () => { it("returns a clear error when Neon Auth returns a structured error", async () => { mockRequestPasswordReset.mockResolvedValue({ data: null, error: { code: "USER_NOT_FOUND", message: "no such user" }, }); const r = await sendPasswordResetEmail("user@example.com"); expect(r.success).toBe(false); expect(r.error).toMatch(/no such user/); }); it("falls back to the error code if the message is missing", async () => { mockRequestPasswordReset.mockResolvedValue({ data: null, error: { code: "RATE_LIMITED" }, }); const r = await sendPasswordResetEmail("user@example.com"); expect(r.success).toBe(false); expect(r.error).toMatch(/RATE_LIMITED/); }); it("catches thrown exceptions and surfaces them as a string", async () => { mockRequestPasswordReset.mockRejectedValue(new Error("network down")); const r = await sendPasswordResetEmail("user@example.com"); expect(r.success).toBe(false); expect(r.error).toMatch(/network down/); }); });