import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' import bcrypt from 'bcryptjs' vi.mock('./admin.repo', () => ({ findAdminByEmail: vi.fn(), findAdminByIdOrThrow: vi.fn(), setAdminPasswordReset: vi.fn(), updateAdminLastLogin: vi.fn(), updateAdminTotpSecret: vi.fn(), createAuditLog: vi.fn(), })) vi.mock('../../services/notificationService', () => ({ sendTransactionalEmail: vi.fn().mockResolvedValue(undefined), })) vi.mock('qrcode', () => ({ default: { toDataURL: vi.fn().mockResolvedValue('data:image/png;base64,test') }, })) const redisStore = new Map() vi.mock('../../lib/redis', () => ({ redis: { on: vi.fn(), get: vi.fn((key: string) => Promise.resolve(redisStore.get(key) ?? null)), set: vi.fn((key: string, value: string) => { redisStore.set(key, value) return Promise.resolve('OK') }), del: vi.fn((key: string) => { const deleted = redisStore.delete(key) ? 1 : 0 return Promise.resolve(deleted) }), quit: vi.fn(), duplicate: vi.fn(), }, })) import * as repo from './admin.repo' import { sendTransactionalEmail } from '../../services/notificationService' import { hashPublicAccessToken } from '../../security/publicAccessTokens' import { forgotPassword, login, setupTotp } from './admin.service' describe('admin.service forgotPassword', () => { const originalAdminUrl = process.env.ADMIN_URL const originalJwtSecret = process.env.JWT_SECRET beforeEach(() => { vi.clearAllMocks() redisStore.clear() process.env.ADMIN_URL = 'http://localhost:3000/admin' process.env.JWT_SECRET = 'test-jwt-secret' }) afterAll(() => { process.env.ADMIN_URL = originalAdminUrl process.env.JWT_SECRET = originalJwtSecret }) it('sends the reset email to the canonical stored admin address', async () => { vi.mocked(repo.findAdminByEmail).mockResolvedValue({ id: 'admin_1', email: 'admin@rentaldrivego.com', firstName: 'Samir', isActive: true, } as any) await forgotPassword('Admin@RentalDriveGo.com') expect(repo.setAdminPasswordReset).toHaveBeenCalledWith( 'admin_1', expect.any(String), expect.any(Date), ) expect(sendTransactionalEmail).toHaveBeenCalledWith( expect.objectContaining({ to: 'admin@rentaldrivego.com', }), ) }) it('signs in without an email login code when admin 2FA is enabled', async () => { vi.mocked(repo.findAdminByEmail).mockResolvedValue({ id: 'admin_2', email: 'admin@example.test', firstName: 'Amal', lastName: 'Admin', role: 'SUPER_ADMIN', isActive: true, passwordHash: await bcrypt.hash('password123', 4), totpEnabled: true, totpSecret: null, } as any) await expect(login('admin@example.test', 'password123')).resolves.toEqual(expect.objectContaining({ token: expect.any(String), admin: expect.objectContaining({ id: 'admin_2', email: 'admin@example.test', totpEnabled: true }), })) expect(sendTransactionalEmail).not.toHaveBeenCalled() }) it('accepts a previously issued emailed admin login code in the 2FA field', async () => { vi.mocked(repo.findAdminByEmail).mockResolvedValue({ id: 'admin_3', email: 'admin3@example.test', firstName: 'Mina', lastName: 'Admin', role: 'SUPER_ADMIN', isActive: true, passwordHash: await bcrypt.hash('password123', 4), totpEnabled: true, totpSecret: null, } as any) const code = '123456' redisStore.set('admin:email-otp:admin_3', hashPublicAccessToken(code)) const result = await login('admin3@example.test', 'password123', code) expect(result).toEqual(expect.objectContaining({ token: expect.any(String), admin: expect.objectContaining({ id: 'admin_3', email: 'admin3@example.test' }), })) expect(repo.updateAdminLastLogin).toHaveBeenCalledWith('admin_3') }) it('reuses a pending TOTP setup secret so duplicate dev setup calls keep codes valid', async () => { vi.mocked(repo.findAdminByIdOrThrow).mockResolvedValue({ id: 'admin_4', email: 'admin4@example.test', totpEnabled: false, totpSecret: 'JBSWY3DPEHPK3PXP', } as any) const first = await setupTotp('admin_4', 'admin4@example.test') const second = await setupTotp('admin_4', 'admin4@example.test') expect(first.secret).toBe('JBSWY3DPEHPK3PXP') expect(second.secret).toBe('JBSWY3DPEHPK3PXP') expect(repo.updateAdminTotpSecret).not.toHaveBeenCalled() }) })