/** * CM: admin user creation can defer the password to the new user. * * Two modes: * - setup-email mode (default): no password is supplied at creation. The * account is provisioned (profile + port role) and a set-password link is * dispatched via better-auth. (The welcome-vs-reset framing of that email * is covered by tests/unit/email/account-setup-email-routing.test.ts.) * - manual mode: the admin supplies a password inline; no email is sent. */ import { afterAll, describe, expect, it, vi } from 'vitest'; import { eq } from 'drizzle-orm'; import { db } from '@/lib/db'; import { account, roles, user, userProfiles } from '@/lib/db/schema'; import { auth } from '@/lib/auth'; import { createUser } from '@/lib/services/users.service'; import { makePort, makeAuditMeta } from '../helpers/factories'; describe('createUser - set-password email flow', () => { const createdUserIds: string[] = []; afterAll(async () => { // user_port_roles are port-scoped and cleaned by global teardown; the // auth user + profile + account rows are global, so purge them here. for (const id of createdUserIds) { await db.delete(account).where(eq(account.userId, id)); await db.delete(userProfiles).where(eq(userProfiles.userId, id)); await db.delete(user).where(eq(user.id, id)); } }); async function salesRoleId(): Promise { const r = await db.query.roles.findFirst({ where: eq(roles.name, 'sales_manager') }); if (!r) throw new Error('sales_manager role not seeded — run pnpm db:seed'); return r.id; } it('provisions the user without a password and emails a set-password link', async () => { const port = await makePort(); const roleId = await salesRoleId(); const resetSpy = vi .spyOn(auth.api, 'requestPasswordReset') .mockResolvedValue({ status: true } as never); try { const email = `setup-test-${Date.now()}-a@example.test`; const result = await createUser( port.id, { email, name: 'Jane Doe', displayName: 'Jane Doe', roleId, sendSetupEmail: true, residentialAccess: false, }, makeAuditMeta(), ); createdUserIds.push(result.userId); // Provisioned with the assigned role, ready to sign in once they set a password. expect(result.role.name).toBe('sales_manager'); // A set-password email was dispatched to their address. expect(resetSpy).toHaveBeenCalledTimes(1); expect(resetSpy.mock.calls[0]?.[0]?.body?.email).toBe(email); } finally { resetSpy.mockRestore(); } }); it('uses the supplied password and sends no email in manual mode', async () => { const port = await makePort(); const roleId = await salesRoleId(); const resetSpy = vi .spyOn(auth.api, 'requestPasswordReset') .mockResolvedValue({ status: true } as never); try { const email = `setup-test-${Date.now()}-b@example.test`; const result = await createUser( port.id, { email, name: 'John Roe', displayName: 'John Roe', roleId, password: 'manual-secret-1234', sendSetupEmail: false, residentialAccess: false, }, makeAuditMeta(), ); createdUserIds.push(result.userId); expect(resetSpy).not.toHaveBeenCalled(); } finally { resetSpy.mockRestore(); } }); });