import { describe, it, expect, vi, Mocked, beforeEach } from 'vitest'; import { AuthService } from '@/lib/services/auth/AuthService'; import { AuthApiClient } from '@/lib/api/auth/AuthApiClient'; import { SessionViewModel } from '@/lib/view-models/SessionViewModel'; // Mock dependencies vi.mock('@/lib/config/apiBaseUrl', () => ({ getWebsiteApiBaseUrl: () => 'http://localhost:3000', })); vi.mock('@/lib/config/env', () => ({ isProductionEnvironment: () => false, })); describe('AuthService', () => { let mockApiClient: Mocked; let service: AuthService; beforeEach(() => { mockApiClient = { signup: vi.fn(), login: vi.fn(), logout: vi.fn(), forgotPassword: vi.fn(), resetPassword: vi.fn(), getSession: vi.fn(), } as Mocked; service = new AuthService(mockApiClient); }); describe('signup', () => { describe('happy paths', () => { it('should call apiClient.signup and return SessionViewModel', async () => { const params = { email: 'test@example.com', password: 'password123', displayName: 'Test User', }; const mockResponse = { token: 'jwt-token', user: { userId: 'user-123', email: 'test@example.com', displayName: 'Test User', }, }; mockApiClient.signup.mockResolvedValue(mockResponse); const result = await service.signup(params); expect(mockApiClient.signup).toHaveBeenCalledWith(params); expect(result.isOk()).toBe(true); const vm = result.unwrap(); expect(vm).toBeInstanceOf(SessionViewModel); expect(vm.userId).toBe('user-123'); expect(vm.email).toBe('test@example.com'); expect(vm.displayName).toBe('Test User'); expect(vm.isAuthenticated).toBe(true); }); }); describe('failure modes', () => { it('should handle validation errors', async () => { const params = { email: 'invalid-email', password: 'short', displayName: 'Test', }; const error = new Error('Validation failed: Invalid email format'); mockApiClient.signup.mockRejectedValue(error); const result = await service.signup(params); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('validation'); expect(result.getError().message).toBe('Validation failed: Invalid email format'); }); it('should handle duplicate email errors', async () => { const params = { email: 'existing@example.com', password: 'password123', displayName: 'Test User', }; const error = new Error('Email already exists'); mockApiClient.signup.mockRejectedValue(error); const result = await service.signup(params); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('validation'); expect(result.getError().message).toBe('Email already exists'); }); it('should handle server errors', async () => { const params = { email: 'test@example.com', password: 'password123', displayName: 'Test User', }; const error = new Error('Internal server error'); mockApiClient.signup.mockRejectedValue(error); const result = await service.signup(params); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('validation'); expect(result.getError().message).toBe('Internal server error'); }); it('should handle network errors', async () => { const params = { email: 'test@example.com', password: 'password123', displayName: 'Test User', }; const error = new Error('Network error'); mockApiClient.signup.mockRejectedValue(error); const result = await service.signup(params); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('validation'); expect(result.getError().message).toBe('Network error'); }); }); describe('decision branches', () => { it('should handle different user data structures', async () => { const params = { email: 'test@example.com', password: 'password123', displayName: 'Test User', }; const mockResponse = { token: 'jwt-token', user: { userId: 'user-123', email: 'test@example.com', displayName: 'Test User', avatarUrl: 'https://example.com/avatar.jpg', role: 'admin', }, }; mockApiClient.signup.mockResolvedValue(mockResponse); const result = await service.signup(params); expect(result.isOk()).toBe(true); const vm = result.unwrap(); expect(vm.userId).toBe('user-123'); expect(vm.email).toBe('test@example.com'); expect(vm.displayName).toBe('Test User'); expect(vm.isAuthenticated).toBe(true); }); it('should handle empty display name', async () => { const params = { email: 'test@example.com', password: 'password123', displayName: '', }; const mockResponse = { token: 'jwt-token', user: { userId: 'user-123', email: 'test@example.com', displayName: '', }, }; mockApiClient.signup.mockResolvedValue(mockResponse); const result = await service.signup(params); expect(result.isOk()).toBe(true); const vm = result.unwrap(); expect(vm.displayName).toBe(''); }); }); }); describe('login', () => { describe('happy paths', () => { it('should call apiClient.login and return SessionViewModel', async () => { const params = { email: 'test@example.com', password: 'password123', }; const mockResponse = { token: 'jwt-token', user: { userId: 'user-123', email: 'test@example.com', displayName: 'Test User', }, }; mockApiClient.login.mockResolvedValue(mockResponse); const result = await service.login(params); expect(mockApiClient.login).toHaveBeenCalledWith(params); expect(result.isOk()).toBe(true); const vm = result.unwrap(); expect(vm).toBeInstanceOf(SessionViewModel); expect(vm.userId).toBe('user-123'); expect(vm.email).toBe('test@example.com'); expect(vm.displayName).toBe('Test User'); expect(vm.isAuthenticated).toBe(true); }); }); describe('failure modes', () => { it('should handle invalid credentials', async () => { const params = { email: 'test@example.com', password: 'wrong-password', }; const error = new Error('Invalid credentials'); mockApiClient.login.mockRejectedValue(error); const result = await service.login(params); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('unauthorized'); expect(result.getError().message).toBe('Invalid credentials'); }); it('should handle account locked errors', async () => { const params = { email: 'test@example.com', password: 'password123', }; const error = new Error('Account locked due to too many failed attempts'); mockApiClient.login.mockRejectedValue(error); const result = await service.login(params); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('unauthorized'); expect(result.getError().message).toBe('Account locked due to too many failed attempts'); }); it('should handle server errors', async () => { const params = { email: 'test@example.com', password: 'password123', }; const error = new Error('Internal server error'); mockApiClient.login.mockRejectedValue(error); const result = await service.login(params); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('unauthorized'); expect(result.getError().message).toBe('Internal server error'); }); it('should handle network errors', async () => { const params = { email: 'test@example.com', password: 'password123', }; const error = new Error('Network error'); mockApiClient.login.mockRejectedValue(error); const result = await service.login(params); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('unauthorized'); expect(result.getError().message).toBe('Network error'); }); }); describe('decision branches', () => { it('should handle different user data structures', async () => { const params = { email: 'test@example.com', password: 'password123', }; const mockResponse = { token: 'jwt-token', user: { userId: 'user-123', email: 'test@example.com', displayName: 'Test User', avatarUrl: 'https://example.com/avatar.jpg', role: 'admin', permissions: ['read', 'write'], }, }; mockApiClient.login.mockResolvedValue(mockResponse); const result = await service.login(params); expect(result.isOk()).toBe(true); const vm = result.unwrap(); expect(vm.userId).toBe('user-123'); expect(vm.email).toBe('test@example.com'); expect(vm.displayName).toBe('Test User'); expect(vm.isAuthenticated).toBe(true); }); it('should handle different email formats', async () => { const emails = [ 'user@example.com', 'user+tag@example.com', 'user.name@example.com', 'user@subdomain.example.com', ]; for (const email of emails) { const params = { email, password: 'password123', }; const mockResponse = { token: 'jwt-token', user: { userId: 'user-123', email, displayName: 'Test User', }, }; mockApiClient.login.mockResolvedValue(mockResponse); const result = await service.login(params); expect(result.isOk()).toBe(true); const vm = result.unwrap(); expect(vm.email).toBe(email); } }); }); }); describe('logout', () => { describe('happy paths', () => { it('should call apiClient.logout successfully', async () => { mockApiClient.logout.mockResolvedValue(undefined); const result = await service.logout(); expect(mockApiClient.logout).toHaveBeenCalled(); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBeUndefined(); }); }); describe('failure modes', () => { it('should handle server errors', async () => { const error = new Error('Logout failed'); mockApiClient.logout.mockRejectedValue(error); const result = await service.logout(); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('serverError'); expect(result.getError().message).toBe('Logout failed'); }); it('should handle network errors', async () => { const error = new Error('Network error'); mockApiClient.logout.mockRejectedValue(error); const result = await service.logout(); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('serverError'); expect(result.getError().message).toBe('Network error'); }); }); }); describe('forgotPassword', () => { describe('happy paths', () => { it('should call apiClient.forgotPassword and return success message', async () => { const params = { email: 'test@example.com', }; const mockResponse = { message: 'Password reset link sent', magicLink: 'https://example.com/reset?token=abc123', }; mockApiClient.forgotPassword.mockResolvedValue(mockResponse); const result = await service.forgotPassword(params); expect(mockApiClient.forgotPassword).toHaveBeenCalledWith(params); expect(result.isOk()).toBe(true); const response = result.unwrap(); expect(response.message).toBe('Password reset link sent'); expect(response.magicLink).toBe('https://example.com/reset?token=abc123'); }); it('should handle response without magicLink', async () => { const params = { email: 'test@example.com', }; const mockResponse = { message: 'Password reset link sent', }; mockApiClient.forgotPassword.mockResolvedValue(mockResponse); const result = await service.forgotPassword(params); expect(result.isOk()).toBe(true); const response = result.unwrap(); expect(response.message).toBe('Password reset link sent'); expect(response.magicLink).toBeUndefined(); }); }); describe('failure modes', () => { it('should handle invalid email errors', async () => { const params = { email: 'nonexistent@example.com', }; const error = new Error('Email not found'); mockApiClient.forgotPassword.mockRejectedValue(error); const result = await service.forgotPassword(params); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('serverError'); expect(result.getError().message).toBe('Email not found'); }); it('should handle rate limiting errors', async () => { const params = { email: 'test@example.com', }; const error = new Error('Too many requests. Please try again later.'); mockApiClient.forgotPassword.mockRejectedValue(error); const result = await service.forgotPassword(params); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('serverError'); expect(result.getError().message).toBe('Too many requests. Please try again later.'); }); it('should handle server errors', async () => { const params = { email: 'test@example.com', }; const error = new Error('Internal server error'); mockApiClient.forgotPassword.mockRejectedValue(error); const result = await service.forgotPassword(params); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('serverError'); expect(result.getError().message).toBe('Internal server error'); }); }); describe('decision branches', () => { it('should handle different response formats', async () => { const params = { email: 'test@example.com', }; const mockResponse = { message: 'Password reset link sent', magicLink: 'https://example.com/reset?token=abc123', expiresAt: '2024-01-01T00:00:00.000Z', }; mockApiClient.forgotPassword.mockResolvedValue(mockResponse); const result = await service.forgotPassword(params); expect(result.isOk()).toBe(true); const response = result.unwrap(); expect(response.message).toBe('Password reset link sent'); expect(response.magicLink).toBe('https://example.com/reset?token=abc123'); }); }); }); describe('resetPassword', () => { describe('happy paths', () => { it('should call apiClient.resetPassword and return success message', async () => { const params = { token: 'reset-token-123', newPassword: 'newPassword123', }; const mockResponse = { message: 'Password reset successfully', }; mockApiClient.resetPassword.mockResolvedValue(mockResponse); const result = await service.resetPassword(params); expect(mockApiClient.resetPassword).toHaveBeenCalledWith(params); expect(result.isOk()).toBe(true); const response = result.unwrap(); expect(response.message).toBe('Password reset successfully'); }); }); describe('failure modes', () => { it('should handle invalid token errors', async () => { const params = { token: 'invalid-token', newPassword: 'newPassword123', }; const error = new Error('Invalid or expired reset token'); mockApiClient.resetPassword.mockRejectedValue(error); const result = await service.resetPassword(params); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('serverError'); expect(result.getError().message).toBe('Invalid or expired reset token'); }); it('should handle weak password errors', async () => { const params = { token: 'reset-token-123', newPassword: '123', }; const error = new Error('Password must be at least 8 characters'); mockApiClient.resetPassword.mockRejectedValue(error); const result = await service.resetPassword(params); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('serverError'); expect(result.getError().message).toBe('Password must be at least 8 characters'); }); it('should handle server errors', async () => { const params = { token: 'reset-token-123', newPassword: 'newPassword123', }; const error = new Error('Internal server error'); mockApiClient.resetPassword.mockRejectedValue(error); const result = await service.resetPassword(params); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('serverError'); expect(result.getError().message).toBe('Internal server error'); }); }); describe('decision branches', () => { it('should handle different token formats', async () => { const tokens = [ 'reset-token-123', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', 'token-with-special-chars-!@#$%', ]; for (const token of tokens) { const params = { token, newPassword: 'newPassword123', }; const mockResponse = { message: 'Password reset successfully', }; mockApiClient.resetPassword.mockResolvedValue(mockResponse); const result = await service.resetPassword(params); expect(result.isOk()).toBe(true); const response = result.unwrap(); expect(response.message).toBe('Password reset successfully'); } }); }); }); describe('getSession', () => { describe('happy paths', () => { it('should call apiClient.getSession and return session data', async () => { const mockResponse = { token: 'jwt-token', user: { userId: 'user-123', email: 'test@example.com', displayName: 'Test User', }, }; mockApiClient.getSession.mockResolvedValue(mockResponse); const result = await service.getSession(); expect(mockApiClient.getSession).toHaveBeenCalled(); expect(result.isOk()).toBe(true); expect(result.unwrap()).toEqual(mockResponse); }); it('should handle null session response', async () => { mockApiClient.getSession.mockResolvedValue(null); const result = await service.getSession(); expect(mockApiClient.getSession).toHaveBeenCalled(); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBeNull(); }); }); describe('failure modes', () => { it('should handle server errors', async () => { const error = new Error('Failed to get session'); mockApiClient.getSession.mockRejectedValue(error); const result = await service.getSession(); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('serverError'); expect(result.getError().message).toBe('Failed to get session'); }); it('should handle network errors', async () => { const error = new Error('Network error'); mockApiClient.getSession.mockRejectedValue(error); const result = await service.getSession(); expect(result.isErr()).toBe(true); expect(result.getError().type).toBe('serverError'); expect(result.getError().message).toBe('Network error'); }); }); describe('decision branches', () => { it('should handle different session data structures', async () => { const mockResponse = { token: 'jwt-token', user: { userId: 'user-123', email: 'test@example.com', displayName: 'Test User', avatarUrl: 'https://example.com/avatar.jpg', role: 'admin', permissions: ['read', 'write'], lastLogin: '2024-01-01T00:00:00.000Z', }, }; mockApiClient.getSession.mockResolvedValue(mockResponse); const result = await service.getSession(); expect(result.isOk()).toBe(true); const session = result.unwrap(); expect(session).toEqual(mockResponse); }); }); }); });