66877d66c2
Build & Push / Pipeline Tests (push) Successful in 1m53s
Test / Type Check (all packages) (push) Successful in 54s
Build & Push / Build & Push Docker Image (push) Successful in 3m29s
Test / API Unit Tests (push) Successful in 1m14s
Test / Homepage Unit Tests (push) Successful in 48s
Test / Carplace Unit Tests (push) Successful in 42s
Test / Admin Unit Tests (push) Successful in 40s
Test / Dashboard Unit Tests (push) Successful in 44s
Test / API Integration Tests (push) Successful in 1m8s
145 lines
4.5 KiB
TypeScript
145 lines
4.5 KiB
TypeScript
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<string, string>()
|
|
|
|
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()
|
|
})
|
|
})
|