veza/apps/web/src/services/2fa-service.test.ts

149 lines
4.2 KiB
TypeScript

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<typeof vi.fn>;
post: ReturnType<typeof vi.fn>;
};
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();
});
});
});