668 lines
21 KiB
TypeScript
668 lines
21 KiB
TypeScript
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<AuthApiClient>;
|
|
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<AuthApiClient>;
|
|
|
|
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);
|
|
});
|
|
});
|
|
});
|
|
});
|