/** * Unit tests for `resetAdminPassword` in `src/actions/admin/reset-admin.ts`. * * The action must: * 1. Reject unauthenticated callers and non-platform-admins * 2. Look up `neon_auth.user` by email (NOT the legacy `users` table) * 3. Try `auth.admin.setUserPassword` first (instant credential) * 4. Fall back to `auth.requestPasswordReset` when the admin * endpoint returns FORBIDDEN / UNAUTHORIZED / INTERNAL_SERVER_ERROR * 5. On success, set `must_change_password = true` for the user * 6. Generate a strong server-side random password (no hard-coded * defaults like "Tuxedo2026!") * 7. Return clear errors when no Neon Auth user matches the email */ import { describe, it, expect, vi, beforeEach } from "vitest"; const { mockGetAdminUser, mockQuery, mockSetUserPassword, mockRequestPasswordReset, } = vi.hoisted(() => ({ mockGetAdminUser: vi.fn(), mockQuery: vi.fn(), mockSetUserPassword: vi.fn(), mockRequestPasswordReset: vi.fn(), })); vi.mock("server-only", () => ({})); vi.mock("@/lib/admin-permissions", () => ({ getAdminUser: mockGetAdminUser, })); vi.mock("@/lib/auth", () => ({ setUserPassword: mockSetUserPassword, requestPasswordReset: mockRequestPasswordReset, })); vi.mock("@/lib/db", () => ({ query: mockQuery, })); vi.mock("next/headers", () => ({ cookies: () => Promise.resolve({ get: () => undefined }), })); import { resetAdminPassword } from "@/actions/admin/reset-admin"; 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", brand_ids: ["brand-tux"], brand_slug: "tuxedo", }; beforeEach(() => { vi.clearAllMocks(); process.env.NEXT_PUBLIC_SITE_URL = "http://localhost:4000"; mockGetAdminUser.mockResolvedValue(PLATFORM_ADMIN); // Default: Neon Auth user lookup succeeds. mockQuery.mockImplementation(async (sql: string) => { if (sql.includes("SELECT id FROM neon_auth.user")) { return { rows: [{ id: "neon-user-1" }] }; } // UPDATE admin_users SET must_change_password — accept silently. return { rows: [], rowCount: 1 }; }); // Default: setUserPassword succeeds. mockSetUserPassword.mockResolvedValue({ data: { ok: true }, error: null }); mockRequestPasswordReset.mockResolvedValue({ data: { ok: true }, error: null }); }); describe("resetAdminPassword — authorization", () => { it("rejects unauthenticated callers", async () => { mockGetAdminUser.mockResolvedValue(null); const r = await resetAdminPassword("user@example.com"); expect(r.success).toBe(false); if (r.success) throw new Error("expected failure"); expect(r.error).toMatch(/not authenticated/i); expect(mockSetUserPassword).not.toHaveBeenCalled(); expect(mockRequestPasswordReset).not.toHaveBeenCalled(); }); it("rejects non-platform-admin callers", async () => { mockGetAdminUser.mockResolvedValue(BRAND_ADMIN); const r = await resetAdminPassword("user@example.com"); expect(r.success).toBe(false); if (r.success) throw new Error("expected failure"); expect(r.error).toMatch(/only platform admins/i); expect(mockSetUserPassword).not.toHaveBeenCalled(); expect(mockRequestPasswordReset).not.toHaveBeenCalled(); }); }); describe("resetAdminPassword — input handling", () => { it("rejects an empty email", async () => { const r = await resetAdminPassword(" "); expect(r.success).toBe(false); if (r.success) throw new Error("expected failure"); expect(r.error).toMatch(/required/i); }); it("looks up the user in neon_auth.user (not the legacy `users` table)", async () => { const r = await resetAdminPassword("User@Example.COM"); expect(r.success).toBe(true); expect(mockQuery).toHaveBeenCalledWith( "SELECT id FROM neon_auth.user WHERE email = $1 LIMIT 1", ["user@example.com"], ); }); it("returns a clear error when no Neon Auth user matches the email", async () => { mockQuery.mockImplementation(async (sql: string) => { if (sql.includes("SELECT id FROM neon_auth.user")) { return { rows: [] }; } return { rows: [], rowCount: 0 }; }); const r = await resetAdminPassword("missing@example.com"); expect(r.success).toBe(false); if (r.success) throw new Error("expected failure"); expect(r.error).toMatch(/no neon auth user found/i); expect(mockSetUserPassword).not.toHaveBeenCalled(); }); }); describe("resetAdminPassword — happy path via setUserPassword", () => { it("returns a server-generated temp password and flips must_change_password", async () => { const r = await resetAdminPassword("user@example.com"); expect(r.success).toBe(true); if (!r.success) throw new Error("expected success"); if (r.method !== "set") throw new Error("expected method=set"); // The password is server-generated — must be a non-empty strong // string, never a hard-coded constant like "Tuxedo2026!". expect(r.tempPassword.length).toBeGreaterThan(20); expect(r.tempPassword).not.toBe("Tuxedo2026!"); expect(r.tempPassword).toMatch(/[!@#$%^&*]/); // includes a symbol expect(mockSetUserPassword).toHaveBeenCalledTimes(1); const [args] = mockSetUserPassword.mock.calls[0]; expect(args.userId).toBe("neon-user-1"); expect(args.newPassword).toBe(r.tempPassword); // The must_change_password flag should be flipped. const flagUpdate = mockQuery.mock.calls.find(([sql]) => String(sql).includes("UPDATE admin_users SET must_change_password"), ); expect(flagUpdate).toBeTruthy(); }); }); describe("resetAdminPassword — FORBIDDEN fallback to requestPasswordReset", () => { it("falls back to the public reset-email endpoint when setUserPassword returns FORBIDDEN", async () => { mockSetUserPassword.mockResolvedValue({ data: null, error: { code: "FORBIDDEN", message: "Admin role required" }, }); const r = await resetAdminPassword("user@example.com"); expect(r.success).toBe(true); if (!r.success) throw new Error("expected success"); if (r.method !== "reset_link_sent") throw new Error("expected method=reset_link_sent"); expect(r.message).toMatch(/reset email/i); expect(mockRequestPasswordReset).toHaveBeenCalledWith({ email: "user@example.com", redirectTo: "http://localhost:4000/reset-password", }); // The fallback case returns method="reset_link_sent" with a // `message` describing what was sent; TypeScript guarantees // tempPassword is not on this variant. }); it("falls back when setUserPassword returns UNAUTHORIZED", async () => { mockSetUserPassword.mockResolvedValue({ data: null, error: { code: "UNAUTHORIZED", message: "no session" }, }); const r = await resetAdminPassword("user@example.com"); expect(r.success).toBe(true); if (!r.success) throw new Error("expected success"); if (r.method !== "reset_link_sent") throw new Error("expected method=reset_link_sent"); }); it("falls back when setUserPassword throws", async () => { mockSetUserPassword.mockRejectedValue(new Error("network down")); const r = await resetAdminPassword("user@example.com"); expect(r.success).toBe(true); if (!r.success) throw new Error("expected success"); if (r.method !== "reset_link_sent") throw new Error("expected method=reset_link_sent"); }); it("does NOT fall back for USER_NOT_FOUND — it surfaces the error", async () => { mockSetUserPassword.mockResolvedValue({ data: null, error: { code: "USER_NOT_FOUND", message: "no such user in neon auth" }, }); const r = await resetAdminPassword("user@example.com"); expect(r.success).toBe(false); if (r.success) throw new Error("expected failure"); expect(r.error).toMatch(/no such user in neon auth/); expect(mockRequestPasswordReset).not.toHaveBeenCalled(); }); it("propagates the public-endpoint error if BOTH paths fail", async () => { mockSetUserPassword.mockResolvedValue({ data: null, error: { code: "FORBIDDEN" }, }); mockRequestPasswordReset.mockResolvedValue({ data: null, error: { code: "RATE_LIMITED", message: "too many requests" }, }); const r = await resetAdminPassword("user@example.com"); expect(r.success).toBe(false); if (r.success) throw new Error("expected failure"); expect(r.error).toMatch(/too many requests/); }); });