import { describe, it, expect, vi, beforeEach } from 'vitest'; import { AxiosError } from 'axios'; import { twoFactorService } from './2fa-service'; import { apiClient } from './api/client'; import { requireFeature } from '@/config/features'; // Mock apiClient vi.mock('./api/client', () => ({ apiClient: { get: vi.fn(), post: vi.fn(), }, })); // Mock feature config vi.mock('@/config/features', () => ({ requireFeature: vi.fn(), FEATURES: { TWO_FACTOR_AUTH: true, }, })); const mockedApiClient = apiClient as { get: ReturnType; post: ReturnType; }; describe('twoFactorService', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('getStatus', () => { it('should get 2FA status successfully', async () => { const mockStatus = { enabled: true }; mockedApiClient.get.mockResolvedValue({ data: mockStatus, }); const result = await twoFactorService.getStatus(); expect(result).toEqual(mockStatus); expect(requireFeature).toHaveBeenCalledWith('TWO_FACTOR_AUTH'); expect(mockedApiClient.get).toHaveBeenCalledWith('/auth/2fa/status'); }); it('should throw error on status fetch failure', async () => { const mockError = new AxiosError('Status fetch failed'); mockError.response = { status: 500, data: { error: 'Internal server error' }, } as any; mockedApiClient.get.mockRejectedValue(mockError); await expect(twoFactorService.getStatus()).rejects.toThrow(); }); }); describe('setup', () => { it('should setup 2FA successfully', async () => { const mockSetupResponse = { secret: 'JBSWY3DPEHPK3PXP', qr_code_url: 'https://example.com/qr.png', recovery_codes: ['code1', 'code2', 'code3'], }; mockedApiClient.post.mockResolvedValue({ data: mockSetupResponse, }); const result = await twoFactorService.setup(); expect(result).toEqual(mockSetupResponse); expect(requireFeature).toHaveBeenCalledWith('TWO_FACTOR_AUTH'); expect(mockedApiClient.post).toHaveBeenCalledWith('/auth/2fa/setup'); }); it('should throw error on setup failure', async () => { const mockError = new AxiosError('Setup failed'); mockError.response = { status: 400, data: { error: '2FA already enabled' }, } as any; mockedApiClient.post.mockRejectedValue(mockError); await expect(twoFactorService.setup()).rejects.toThrow(); }); }); describe('verify', () => { it('should verify 2FA code successfully', async () => { mockedApiClient.post.mockResolvedValue({ data: { message: '2FA enabled successfully' }, }); await twoFactorService.verify('JBSWY3DPEHPK3PXP', '123456'); expect(requireFeature).toHaveBeenCalledWith('TWO_FACTOR_AUTH'); expect(mockedApiClient.post).toHaveBeenCalledWith('/auth/2fa/verify', { secret: 'JBSWY3DPEHPK3PXP', code: '123456', }); }); it('should throw error on verification failure', async () => { const mockError = new AxiosError('Verification failed'); mockError.response = { status: 400, data: { error: 'Invalid code' }, } as any; mockedApiClient.post.mockRejectedValue(mockError); await expect( twoFactorService.verify('JBSWY3DPEHPK3PXP', 'invalid'), ).rejects.toThrow(); }); }); describe('disable', () => { it('should disable 2FA successfully', async () => { mockedApiClient.post.mockResolvedValue({ data: { message: '2FA disabled successfully' }, }); await twoFactorService.disable('password123'); expect(requireFeature).toHaveBeenCalledWith('TWO_FACTOR_AUTH'); expect(mockedApiClient.post).toHaveBeenCalledWith('/auth/2fa/disable', { password: 'password123', }); }); it('should throw error on disable failure', async () => { const mockError = new AxiosError('Disable failed'); mockError.response = { status: 401, data: { error: 'Invalid password' }, } as any; mockedApiClient.post.mockRejectedValue(mockError); await expect(twoFactorService.disable('wrongpassword')).rejects.toThrow(); }); }); });