fix architecture and write new tests
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('bcryptjs', () => ({
|
||||
default: {
|
||||
compare: vi.fn(),
|
||||
hash: vi.fn(),
|
||||
},
|
||||
}))
|
||||
vi.mock('../../services/notificationService', () => ({ sendTransactionalEmail: vi.fn() }))
|
||||
vi.mock('./auth.employee.repo', () => ({
|
||||
findEmployeeWithCompanyById: vi.fn(),
|
||||
findEmployeeWithCompanyByEmail: vi.fn(),
|
||||
findActiveEmployeeByEmail: vi.fn(),
|
||||
findEmployeeByResetToken: vi.fn(),
|
||||
findEmployeeById: vi.fn(),
|
||||
updatePreferredLanguage: vi.fn(),
|
||||
resetPassword: vi.fn(),
|
||||
}))
|
||||
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { AppError } from '../../http/errors'
|
||||
import { sendTransactionalEmail } from '../../services/notificationService'
|
||||
import * as repo from './auth.employee.repo'
|
||||
import * as service from './auth.employee.service'
|
||||
|
||||
const employee = {
|
||||
id: 'employee_1',
|
||||
email: 'agent@example.test',
|
||||
firstName: 'Aya',
|
||||
lastName: 'Agent',
|
||||
role: 'AGENT',
|
||||
preferredLanguage: 'fr',
|
||||
companyId: 'company_1',
|
||||
isActive: true,
|
||||
passwordHash: 'hash_old',
|
||||
company: { name: 'Atlas Cars', slug: 'atlas' },
|
||||
}
|
||||
|
||||
describe('auth.employee.service edge behavior', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
process.env.JWT_SECRET = 'test-secret'
|
||||
process.env.JWT_EXPIRY = '8h'
|
||||
delete process.env.DASHBOARD_URL
|
||||
delete process.env.NEXT_PUBLIC_DASHBOARD_URL
|
||||
vi.mocked(bcrypt.compare).mockResolvedValue(true as never)
|
||||
vi.mocked(bcrypt.hash).mockResolvedValue('hash_new' as never)
|
||||
})
|
||||
|
||||
it('rejects inactive employee sessions before presenting tenant data', async () => {
|
||||
vi.mocked(repo.findEmployeeWithCompanyById).mockResolvedValue({ ...employee, isActive: false } as never)
|
||||
|
||||
await expect(service.getMe('employee_1')).rejects.toMatchObject({
|
||||
statusCode: 401,
|
||||
error: 'unauthenticated',
|
||||
})
|
||||
})
|
||||
|
||||
it('logs in active employees with a signed token and company context', async () => {
|
||||
vi.mocked(repo.findEmployeeWithCompanyByEmail).mockResolvedValue(employee as never)
|
||||
|
||||
const result = await service.login({ email: 'agent@example.test', password: 'correct-password' })
|
||||
|
||||
expect(bcrypt.compare).toHaveBeenCalledWith('correct-password', 'hash_old')
|
||||
expect(result).toMatchObject({
|
||||
token: expect.any(String),
|
||||
employee: {
|
||||
id: 'employee_1',
|
||||
companyId: 'company_1',
|
||||
companyName: 'Atlas Cars',
|
||||
companySlug: 'atlas',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('distinguishes employees without passwords from wrong credentials', async () => {
|
||||
vi.mocked(repo.findEmployeeWithCompanyByEmail).mockResolvedValue({ ...employee, passwordHash: null } as never)
|
||||
|
||||
await expect(service.login({ email: 'agent@example.test', password: 'anything' })).rejects.toMatchObject({
|
||||
statusCode: 401,
|
||||
error: 'password_not_set',
|
||||
})
|
||||
expect(bcrypt.compare).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sends reset links to active employees using the dashboard base path exactly once', async () => {
|
||||
process.env.DASHBOARD_URL = 'https://tenant.example.test/app'
|
||||
vi.mocked(repo.findActiveEmployeeByEmail).mockResolvedValue(employee as never)
|
||||
vi.mocked(sendTransactionalEmail).mockResolvedValue({ success: true } as never)
|
||||
|
||||
await expect(service.forgotPassword('agent@example.test')).resolves.toEqual({
|
||||
message: 'If that email is registered, a reset link has been sent.',
|
||||
})
|
||||
|
||||
expect(sendTransactionalEmail).toHaveBeenCalledWith(expect.objectContaining({
|
||||
to: 'agent@example.test',
|
||||
subject: expect.any(String),
|
||||
html: expect.stringContaining('https://tenant.example.test/app/dashboard/reset-password?token='),
|
||||
text: expect.stringContaining('https://tenant.example.test/app/dashboard/reset-password?token='),
|
||||
}))
|
||||
})
|
||||
|
||||
it('does not disclose whether forgot-password emails exist', async () => {
|
||||
vi.mocked(repo.findActiveEmployeeByEmail).mockResolvedValue(null)
|
||||
|
||||
await expect(service.forgotPassword('missing@example.test')).resolves.toEqual({
|
||||
message: 'If that email is registered, a reset link has been sent.',
|
||||
})
|
||||
expect(sendTransactionalEmail).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('updates employee password from a stored reset token and clears reset state through the repo', async () => {
|
||||
vi.mocked(repo.findEmployeeByResetToken).mockResolvedValue(employee as never)
|
||||
|
||||
await expect(service.resetPassword('stored-token', 'new-secure-password')).resolves.toEqual({
|
||||
message: 'Password updated successfully. You can now sign in.',
|
||||
})
|
||||
|
||||
expect(bcrypt.hash).toHaveBeenCalledWith('new-secure-password', 12)
|
||||
expect(repo.resetPassword).toHaveBeenCalledWith('employee_1', 'hash_new')
|
||||
})
|
||||
|
||||
it('rejects invalid reset tokens before hashing new passwords', async () => {
|
||||
vi.mocked(repo.findEmployeeByResetToken).mockResolvedValue(null)
|
||||
vi.mocked(repo.findEmployeeById).mockResolvedValue(null)
|
||||
|
||||
await expect(service.resetPassword('bad-token', 'new-secure-password')).rejects.toMatchObject({
|
||||
statusCode: 400,
|
||||
error: 'invalid_token',
|
||||
})
|
||||
expect(bcrypt.hash).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('delegates language preference updates and returns a stable contract', async () => {
|
||||
vi.mocked(repo.updatePreferredLanguage).mockResolvedValue({} as never)
|
||||
|
||||
await expect(service.updateLanguage('employee_1', 'ar')).resolves.toEqual({ language: 'ar' })
|
||||
expect(repo.updatePreferredLanguage).toHaveBeenCalledWith('employee_1', 'ar')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user