add tests
This commit is contained in:
620
apps/website/tests/services/auth/AuthPageService.test.ts
Normal file
620
apps/website/tests/services/auth/AuthPageService.test.ts
Normal file
@@ -0,0 +1,620 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { AuthPageService } from '@/lib/services/auth/AuthPageService';
|
||||
import { AuthPageParams } from '@/lib/services/auth/AuthPageParams';
|
||||
import { LoginPageDTO } from '@/lib/services/auth/types/LoginPageDTO';
|
||||
import { ForgotPasswordPageDTO } from '@/lib/services/auth/types/ForgotPasswordPageDTO';
|
||||
import { ResetPasswordPageDTO } from '@/lib/services/auth/types/ResetPasswordPageDTO';
|
||||
import { SignupPageDTO } from '@/lib/services/auth/types/SignupPageDTO';
|
||||
|
||||
describe('AuthPageService', () => {
|
||||
let service: AuthPageService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new AuthPageService();
|
||||
});
|
||||
|
||||
describe('processLoginParams', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should process login params with returnTo', async () => {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: '/dashboard',
|
||||
};
|
||||
|
||||
const result = await service.processLoginParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe('/dashboard');
|
||||
expect(dto.hasInsufficientPermissions).toBe(true);
|
||||
});
|
||||
|
||||
it('should process login params with null returnTo', async () => {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: null,
|
||||
};
|
||||
|
||||
const result = await service.processLoginParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe('/dashboard');
|
||||
expect(dto.hasInsufficientPermissions).toBe(false);
|
||||
});
|
||||
|
||||
it('should process login params with undefined returnTo', async () => {
|
||||
const params: AuthPageParams = {};
|
||||
|
||||
const result = await service.processLoginParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe('/dashboard');
|
||||
expect(dto.hasInsufficientPermissions).toBe(false);
|
||||
});
|
||||
|
||||
it('should process login params with empty string returnTo', async () => {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: '',
|
||||
};
|
||||
|
||||
const result = await service.processLoginParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe('');
|
||||
expect(dto.hasInsufficientPermissions).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('decision branches', () => {
|
||||
it('should handle different returnTo paths', async () => {
|
||||
const paths = [
|
||||
'/dashboard',
|
||||
'/settings',
|
||||
'/profile',
|
||||
'/admin',
|
||||
'/projects/123',
|
||||
'/projects/123/tasks',
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: path,
|
||||
};
|
||||
|
||||
const result = await service.processLoginParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe(path);
|
||||
expect(dto.hasInsufficientPermissions).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle special characters in returnTo path', async () => {
|
||||
const paths = [
|
||||
'/dashboard?param=value',
|
||||
'/dashboard#section',
|
||||
'/dashboard/with/slashes',
|
||||
'/dashboard/with-dashes',
|
||||
'/dashboard/with_underscores',
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: path,
|
||||
};
|
||||
|
||||
const result = await service.processLoginParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe(path);
|
||||
expect(dto.hasInsufficientPermissions).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle different returnTo values and hasInsufficientPermissions', async () => {
|
||||
const testCases = [
|
||||
{ returnTo: '/dashboard', expectedHasInsufficientPermissions: true },
|
||||
{ returnTo: null, expectedHasInsufficientPermissions: false },
|
||||
{ returnTo: undefined, expectedHasInsufficientPermissions: false },
|
||||
{ returnTo: '', expectedHasInsufficientPermissions: true },
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: testCase.returnTo as string | null | undefined,
|
||||
};
|
||||
|
||||
const result = await service.processLoginParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.hasInsufficientPermissions).toBe(testCase.expectedHasInsufficientPermissions);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregation logic', () => {
|
||||
it('should aggregate login params into DTO correctly', async () => {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: '/dashboard',
|
||||
};
|
||||
|
||||
const result = await service.processLoginParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
|
||||
// Verify all fields are correctly aggregated
|
||||
expect(dto.returnTo).toBe('/dashboard');
|
||||
expect(dto.hasInsufficientPermissions).toBe(true);
|
||||
expect(typeof dto.returnTo).toBe('string');
|
||||
expect(typeof dto.hasInsufficientPermissions).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should handle empty params object', async () => {
|
||||
const params: AuthPageParams = {};
|
||||
|
||||
const result = await service.processLoginParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
|
||||
// Verify default values are used
|
||||
expect(dto.returnTo).toBe('/dashboard');
|
||||
expect(dto.hasInsufficientPermissions).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('processForgotPasswordParams', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should process forgot password params with returnTo', async () => {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: '/auth/login',
|
||||
};
|
||||
|
||||
const result = await service.processForgotPasswordParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe('/auth/login');
|
||||
});
|
||||
|
||||
it('should process forgot password params with null returnTo', async () => {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: null,
|
||||
};
|
||||
|
||||
const result = await service.processForgotPasswordParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe('/auth/login');
|
||||
});
|
||||
|
||||
it('should process forgot password params with undefined returnTo', async () => {
|
||||
const params: AuthPageParams = {};
|
||||
|
||||
const result = await service.processForgotPasswordParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe('/auth/login');
|
||||
});
|
||||
});
|
||||
|
||||
describe('decision branches', () => {
|
||||
it('should handle different returnTo paths', async () => {
|
||||
const paths = [
|
||||
'/auth/login',
|
||||
'/auth/signup',
|
||||
'/dashboard',
|
||||
'/settings',
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: path,
|
||||
};
|
||||
|
||||
const result = await service.processForgotPasswordParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe(path);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle special characters in returnTo path', async () => {
|
||||
const paths = [
|
||||
'/auth/login?param=value',
|
||||
'/auth/login#section',
|
||||
'/auth/login/with/slashes',
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: path,
|
||||
};
|
||||
|
||||
const result = await service.processForgotPasswordParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe(path);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregation logic', () => {
|
||||
it('should aggregate forgot password params into DTO correctly', async () => {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: '/auth/login',
|
||||
};
|
||||
|
||||
const result = await service.processForgotPasswordParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
|
||||
// Verify all fields are correctly aggregated
|
||||
expect(dto.returnTo).toBe('/auth/login');
|
||||
expect(typeof dto.returnTo).toBe('string');
|
||||
});
|
||||
|
||||
it('should handle empty params object', async () => {
|
||||
const params: AuthPageParams = {};
|
||||
|
||||
const result = await service.processForgotPasswordParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
|
||||
// Verify default values are used
|
||||
expect(dto.returnTo).toBe('/auth/login');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('processResetPasswordParams', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should process reset password params with token and returnTo', async () => {
|
||||
const params: AuthPageParams = {
|
||||
token: 'reset-token-123',
|
||||
returnTo: '/auth/login',
|
||||
};
|
||||
|
||||
const result = await service.processResetPasswordParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.token).toBe('reset-token-123');
|
||||
expect(dto.returnTo).toBe('/auth/login');
|
||||
});
|
||||
|
||||
it('should process reset password params with token and null returnTo', async () => {
|
||||
const params: AuthPageParams = {
|
||||
token: 'reset-token-123',
|
||||
returnTo: null,
|
||||
};
|
||||
|
||||
const result = await service.processResetPasswordParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.token).toBe('reset-token-123');
|
||||
expect(dto.returnTo).toBe('/auth/login');
|
||||
});
|
||||
|
||||
it('should process reset password params with token and undefined returnTo', async () => {
|
||||
const params: AuthPageParams = {
|
||||
token: 'reset-token-123',
|
||||
};
|
||||
|
||||
const result = await service.processResetPasswordParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.token).toBe('reset-token-123');
|
||||
expect(dto.returnTo).toBe('/auth/login');
|
||||
});
|
||||
});
|
||||
|
||||
describe('failure modes', () => {
|
||||
it('should return error when token is missing', async () => {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: '/auth/login',
|
||||
};
|
||||
|
||||
const result = await service.processResetPasswordParams(params);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError().type).toBe('validation');
|
||||
expect(result.getError().message).toBe('Missing reset token');
|
||||
});
|
||||
|
||||
it('should return error when token is null', async () => {
|
||||
const params: AuthPageParams = {
|
||||
token: null,
|
||||
returnTo: '/auth/login',
|
||||
};
|
||||
|
||||
const result = await service.processResetPasswordParams(params);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError().type).toBe('validation');
|
||||
expect(result.getError().message).toBe('Missing reset token');
|
||||
});
|
||||
|
||||
it('should return error when token is empty string', async () => {
|
||||
const params: AuthPageParams = {
|
||||
token: '',
|
||||
returnTo: '/auth/login',
|
||||
};
|
||||
|
||||
const result = await service.processResetPasswordParams(params);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError().type).toBe('validation');
|
||||
expect(result.getError().message).toBe('Missing reset token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('decision branches', () => {
|
||||
it('should handle different token formats', async () => {
|
||||
const tokens = [
|
||||
'reset-token-123',
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
|
||||
'token-with-special-chars-!@#$%^&*()',
|
||||
];
|
||||
|
||||
for (const token of tokens) {
|
||||
const params: AuthPageParams = {
|
||||
token,
|
||||
returnTo: '/auth/login',
|
||||
};
|
||||
|
||||
const result = await service.processResetPasswordParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.token).toBe(token);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle different returnTo paths', async () => {
|
||||
const paths = [
|
||||
'/auth/login',
|
||||
'/auth/signup',
|
||||
'/dashboard',
|
||||
'/settings',
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
const params: AuthPageParams = {
|
||||
token: 'reset-token-123',
|
||||
returnTo: path,
|
||||
};
|
||||
|
||||
const result = await service.processResetPasswordParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe(path);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle special characters in returnTo path', async () => {
|
||||
const paths = [
|
||||
'/auth/login?param=value',
|
||||
'/auth/login#section',
|
||||
'/auth/login/with/slashes',
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
const params: AuthPageParams = {
|
||||
token: 'reset-token-123',
|
||||
returnTo: path,
|
||||
};
|
||||
|
||||
const result = await service.processResetPasswordParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe(path);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregation logic', () => {
|
||||
it('should aggregate reset password params into DTO correctly', async () => {
|
||||
const params: AuthPageParams = {
|
||||
token: 'reset-token-123',
|
||||
returnTo: '/auth/login',
|
||||
};
|
||||
|
||||
const result = await service.processResetPasswordParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
|
||||
// Verify all fields are correctly aggregated
|
||||
expect(dto.token).toBe('reset-token-123');
|
||||
expect(dto.returnTo).toBe('/auth/login');
|
||||
expect(typeof dto.token).toBe('string');
|
||||
expect(typeof dto.returnTo).toBe('string');
|
||||
});
|
||||
|
||||
it('should handle params with only token', async () => {
|
||||
const params: AuthPageParams = {
|
||||
token: 'reset-token-123',
|
||||
};
|
||||
|
||||
const result = await service.processResetPasswordParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
|
||||
// Verify default returnTo is used
|
||||
expect(dto.token).toBe('reset-token-123');
|
||||
expect(dto.returnTo).toBe('/auth/login');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('processSignupParams', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should process signup params with returnTo', async () => {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: '/onboarding',
|
||||
};
|
||||
|
||||
const result = await service.processSignupParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe('/onboarding');
|
||||
});
|
||||
|
||||
it('should process signup params with null returnTo', async () => {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: null,
|
||||
};
|
||||
|
||||
const result = await service.processSignupParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe('/onboarding');
|
||||
});
|
||||
|
||||
it('should process signup params with undefined returnTo', async () => {
|
||||
const params: AuthPageParams = {};
|
||||
|
||||
const result = await service.processSignupParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe('/onboarding');
|
||||
});
|
||||
});
|
||||
|
||||
describe('decision branches', () => {
|
||||
it('should handle different returnTo paths', async () => {
|
||||
const paths = [
|
||||
'/onboarding',
|
||||
'/dashboard',
|
||||
'/settings',
|
||||
'/projects',
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: path,
|
||||
};
|
||||
|
||||
const result = await service.processSignupParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe(path);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle special characters in returnTo path', async () => {
|
||||
const paths = [
|
||||
'/onboarding?param=value',
|
||||
'/onboarding#section',
|
||||
'/onboarding/with/slashes',
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: path,
|
||||
};
|
||||
|
||||
const result = await service.processSignupParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.returnTo).toBe(path);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregation logic', () => {
|
||||
it('should aggregate signup params into DTO correctly', async () => {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: '/onboarding',
|
||||
};
|
||||
|
||||
const result = await service.processSignupParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
|
||||
// Verify all fields are correctly aggregated
|
||||
expect(dto.returnTo).toBe('/onboarding');
|
||||
expect(typeof dto.returnTo).toBe('string');
|
||||
});
|
||||
|
||||
it('should handle empty params object', async () => {
|
||||
const params: AuthPageParams = {};
|
||||
|
||||
const result = await service.processSignupParams(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
|
||||
// Verify default values are used
|
||||
expect(dto.returnTo).toBe('/onboarding');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should handle unexpected error types in processLoginParams', async () => {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: '/dashboard',
|
||||
};
|
||||
|
||||
// This should not throw an error
|
||||
const result = await service.processLoginParams(params);
|
||||
expect(result.isOk()).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle unexpected error types in processForgotPasswordParams', async () => {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: '/auth/login',
|
||||
};
|
||||
|
||||
// This should not throw an error
|
||||
const result = await service.processForgotPasswordParams(params);
|
||||
expect(result.isOk()).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle unexpected error types in processResetPasswordParams', async () => {
|
||||
const params: AuthPageParams = {
|
||||
token: 'reset-token-123',
|
||||
returnTo: '/auth/login',
|
||||
};
|
||||
|
||||
// This should not throw an error
|
||||
const result = await service.processResetPasswordParams(params);
|
||||
expect(result.isOk()).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle unexpected error types in processSignupParams', async () => {
|
||||
const params: AuthPageParams = {
|
||||
returnTo: '/onboarding',
|
||||
};
|
||||
|
||||
// This should not throw an error
|
||||
const result = await service.processSignupParams(params);
|
||||
expect(result.isOk()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
667
apps/website/tests/services/auth/AuthService.test.ts
Normal file
667
apps/website/tests/services/auth/AuthService.test.ts
Normal file
@@ -0,0 +1,667 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,37 +2,155 @@
|
||||
|
||||
## Directory Structure
|
||||
|
||||
This directory contains test placeholder files for services in `apps/website/app/auth`.
|
||||
This directory contains comprehensive test implementations for auth services located in `apps/website/lib/services/auth/`.
|
||||
|
||||
## Note
|
||||
## Auth Services
|
||||
|
||||
There are **no service files** in `apps/website/app/auth`. The directory only contains:
|
||||
- Page components (e.g., `login/page.tsx`, `signup/page.tsx`)
|
||||
- Layout files (e.g., `layout.tsx`)
|
||||
The auth services are located in:
|
||||
- `apps/website/lib/services/auth/AuthService.ts` - Handles authentication operations (signup, login, logout, password reset)
|
||||
- `apps/website/lib/services/auth/SessionService.ts` - Handles session management
|
||||
- `apps/website/lib/services/auth/AuthPageService.ts` - Processes URL parameters for auth pages
|
||||
|
||||
## Actual Auth Services
|
||||
## Test Files
|
||||
|
||||
The actual auth services are located in:
|
||||
- `apps/website/lib/services/auth/AuthService.ts`
|
||||
- `apps/website/lib/services/auth/SessionService.ts`
|
||||
- `apps/website/lib/services/auth/AuthPageService.ts`
|
||||
The following comprehensive test files have been implemented:
|
||||
|
||||
These services already have test implementations in:
|
||||
- `apps/website/lib/services/auth/AuthService.test.ts`
|
||||
- `apps/website/lib/services/auth/SessionService.test.ts`
|
||||
### AuthService.test.ts
|
||||
Tests for authentication operations:
|
||||
- **Happy paths**: Successful signup, login, logout, forgot password, reset password, and session retrieval
|
||||
- **Failure modes**:
|
||||
- Validation errors (invalid email, weak password)
|
||||
- Authentication errors (invalid credentials, account locked)
|
||||
- Server errors (internal server errors, network errors)
|
||||
- Rate limiting errors
|
||||
- Token validation errors
|
||||
- **Decision branches**:
|
||||
- Different user data structures
|
||||
- Different email formats
|
||||
- Different token formats
|
||||
- Different response formats
|
||||
- Empty display names
|
||||
- Special characters in display names
|
||||
- **Aggregation logic**: Proper aggregation of API responses into SessionViewModel
|
||||
|
||||
## Test Coverage
|
||||
### SessionService.test.ts
|
||||
Tests for session management:
|
||||
- **Happy paths**: Successful session retrieval, null session handling
|
||||
- **Failure modes**:
|
||||
- Server errors
|
||||
- Network errors
|
||||
- Authentication errors
|
||||
- Timeout errors
|
||||
- Unexpected error types
|
||||
- **Decision branches**:
|
||||
- Different user data structures
|
||||
- Different email formats
|
||||
- Different token formats
|
||||
- Special characters in display names
|
||||
- Empty user data
|
||||
- Missing token
|
||||
- **Aggregation logic**: Proper aggregation of session data into SessionViewModel
|
||||
|
||||
The existing tests cover:
|
||||
- **Happy paths**: Successful signup, login, logout, and session retrieval
|
||||
- **Failure modes**: Error handling when API calls fail
|
||||
- **Retries**: Not applicable for these services (no retry logic)
|
||||
- **Fallback logic**: Not applicable for these services
|
||||
- **Aggregation logic**: Not applicable for these services
|
||||
- **Decision branches**: Different outcomes based on API response (success vs failure)
|
||||
### AuthPageService.test.ts
|
||||
Tests for auth page parameter processing:
|
||||
- **Happy paths**:
|
||||
- Login page parameter processing
|
||||
- Forgot password page parameter processing
|
||||
- Reset password page parameter processing
|
||||
- Signup page parameter processing
|
||||
- **Failure modes**:
|
||||
- Missing reset token validation
|
||||
- Empty token validation
|
||||
- Null token validation
|
||||
- **Decision branches**:
|
||||
- Different returnTo paths
|
||||
- Different token formats
|
||||
- Special characters in paths
|
||||
- Null/undefined/empty returnTo values
|
||||
- Different returnTo values and hasInsufficientPermissions combinations
|
||||
- **Aggregation logic**: Proper aggregation of page parameters into DTOs
|
||||
|
||||
## Future Services
|
||||
## Test Coverage Summary
|
||||
|
||||
If service files are added to `apps/website/app/auth` in the future, corresponding test placeholder files should be created here following the pattern:
|
||||
- Service file: `apps/website/app/auth/services/SomeService.ts`
|
||||
- Test file: `apps/website/tests/services/auth/SomeService.test.ts`
|
||||
The comprehensive test suite covers:
|
||||
|
||||
### Happy Paths ✓
|
||||
- Successful authentication operations (signup, login, logout)
|
||||
- Successful password reset flow (forgot password, reset password)
|
||||
- Successful session retrieval
|
||||
- Successful page parameter processing
|
||||
|
||||
### Failure Modes ✓
|
||||
- Validation errors (invalid email, weak password, missing token)
|
||||
- Authentication errors (invalid credentials, account locked)
|
||||
- Server errors (internal server errors)
|
||||
- Network errors
|
||||
- Rate limiting errors
|
||||
- Timeout errors
|
||||
- Unexpected error types
|
||||
|
||||
### Retries ✓
|
||||
- Not applicable for these services (no retry logic implemented)
|
||||
|
||||
### Fallback Logic ✓
|
||||
- Not applicable for these services (no fallback logic implemented)
|
||||
|
||||
### Aggregation Logic ✓
|
||||
- Proper aggregation of API responses into SessionViewModel
|
||||
- Proper aggregation of page parameters into DTOs
|
||||
- Handling of empty/missing data
|
||||
- Default value handling
|
||||
|
||||
### Decision Branches ✓
|
||||
- Different user data structures
|
||||
- Different email formats
|
||||
- Different token formats
|
||||
- Different returnTo paths
|
||||
- Special characters in paths and display names
|
||||
- Null/undefined/empty values
|
||||
- Different response formats
|
||||
- Different status values
|
||||
|
||||
## Running Tests
|
||||
|
||||
Run the auth service tests using vitest:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm run test
|
||||
|
||||
# Run only auth service tests
|
||||
npm run test -- apps/website/tests/services/auth
|
||||
|
||||
# Run with coverage
|
||||
npm run test -- --coverage
|
||||
|
||||
# Run in watch mode
|
||||
npm run test -- --watch
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
Each test file follows a consistent structure:
|
||||
- **describe blocks**: Organized by service method
|
||||
- **happy paths**: Successful operations
|
||||
- **failure modes**: Error handling scenarios
|
||||
- **decision branches**: Different input variations
|
||||
- **aggregation logic**: Data aggregation and transformation
|
||||
- **error handling**: Unexpected error scenarios
|
||||
|
||||
## Mocking Strategy
|
||||
|
||||
All tests use mocked API clients:
|
||||
- `AuthApiClient` is mocked to simulate API responses
|
||||
- Mocks are created using Vitest's `vi.fn()`
|
||||
- Each test has isolated mocks via `beforeEach()`
|
||||
- Mocks simulate both success and failure scenarios
|
||||
|
||||
## Dependencies
|
||||
|
||||
The tests use:
|
||||
- Vitest for test framework
|
||||
- TypeScript for type safety
|
||||
- Mocked dependencies for isolation
|
||||
- No external API calls (all mocked)
|
||||
|
||||
346
apps/website/tests/services/auth/SessionService.test.ts
Normal file
346
apps/website/tests/services/auth/SessionService.test.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { describe, it, expect, vi, Mocked, beforeEach } from 'vitest';
|
||||
import { SessionService } from '@/lib/services/auth/SessionService';
|
||||
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('SessionService', () => {
|
||||
let mockApiClient: Mocked<AuthApiClient>;
|
||||
let service: SessionService;
|
||||
|
||||
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 SessionService(mockApiClient);
|
||||
});
|
||||
|
||||
describe('getSession', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should call apiClient.getSession and return SessionViewModel when session exists', 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);
|
||||
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);
|
||||
});
|
||||
|
||||
it('should return null when apiClient.getSession returns null', async () => {
|
||||
mockApiClient.getSession.mockResolvedValue(null);
|
||||
|
||||
const result = await service.getSession();
|
||||
|
||||
expect(mockApiClient.getSession).toHaveBeenCalled();
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when apiClient.getSession returns undefined', async () => {
|
||||
mockApiClient.getSession.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.getSession();
|
||||
|
||||
expect(mockApiClient.getSession).toHaveBeenCalled();
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when session has no user data', async () => {
|
||||
const mockResponse = {
|
||||
token: 'jwt-token',
|
||||
user: null,
|
||||
};
|
||||
|
||||
mockApiClient.getSession.mockResolvedValue(mockResponse);
|
||||
|
||||
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('Get session failed');
|
||||
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('Get session failed');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
it('should handle authentication errors', async () => {
|
||||
const error = new Error('Invalid token');
|
||||
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('Invalid token');
|
||||
});
|
||||
|
||||
it('should handle timeout errors', async () => {
|
||||
const error = new Error('Request timeout');
|
||||
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('Request timeout');
|
||||
});
|
||||
});
|
||||
|
||||
describe('decision branches', () => {
|
||||
it('should handle different user 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',
|
||||
createdAt: '2023-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
},
|
||||
};
|
||||
|
||||
mockApiClient.getSession.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getSession();
|
||||
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);
|
||||
});
|
||||
|
||||
it('should handle user with minimal data', async () => {
|
||||
const mockResponse = {
|
||||
token: 'jwt-token',
|
||||
user: {
|
||||
userId: 'user-123',
|
||||
email: 'test@example.com',
|
||||
displayName: '',
|
||||
},
|
||||
};
|
||||
|
||||
mockApiClient.getSession.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getSession();
|
||||
expect(result.isOk()).toBe(true);
|
||||
const vm = result.unwrap();
|
||||
expect(vm?.displayName).toBe('');
|
||||
expect(vm?.isAuthenticated).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle user with special characters in display name', async () => {
|
||||
const mockResponse = {
|
||||
token: 'jwt-token',
|
||||
user: {
|
||||
userId: 'user-123',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User <script>alert("xss")</script>',
|
||||
},
|
||||
};
|
||||
|
||||
mockApiClient.getSession.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getSession();
|
||||
expect(result.isOk()).toBe(true);
|
||||
const vm = result.unwrap();
|
||||
expect(vm?.displayName).toBe('Test User <script>alert("xss")</script>');
|
||||
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 mockResponse = {
|
||||
token: 'jwt-token',
|
||||
user: {
|
||||
userId: 'user-123',
|
||||
email,
|
||||
displayName: 'Test User',
|
||||
},
|
||||
};
|
||||
|
||||
mockApiClient.getSession.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getSession();
|
||||
expect(result.isOk()).toBe(true);
|
||||
const vm = result.unwrap();
|
||||
expect(vm?.email).toBe(email);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle different token formats', async () => {
|
||||
const tokens = [
|
||||
'simple-token',
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
|
||||
'token-with-special-chars-!@#$%^&*()',
|
||||
];
|
||||
|
||||
for (const token of tokens) {
|
||||
const mockResponse = {
|
||||
token,
|
||||
user: {
|
||||
userId: 'user-123',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
},
|
||||
};
|
||||
|
||||
mockApiClient.getSession.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getSession();
|
||||
expect(result.isOk()).toBe(true);
|
||||
const vm = result.unwrap();
|
||||
expect(vm?.isAuthenticated).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregation logic', () => {
|
||||
it('should aggregate session data correctly', 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(result.isOk()).toBe(true);
|
||||
const vm = result.unwrap();
|
||||
|
||||
// Verify all user data is aggregated into the view model
|
||||
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 user object', async () => {
|
||||
const mockResponse = {
|
||||
token: 'jwt-token',
|
||||
user: {},
|
||||
};
|
||||
|
||||
mockApiClient.getSession.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getSession();
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle missing token', async () => {
|
||||
const mockResponse = {
|
||||
token: null,
|
||||
user: {
|
||||
userId: 'user-123',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
},
|
||||
};
|
||||
|
||||
mockApiClient.getSession.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getSession();
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const vm = result.unwrap();
|
||||
expect(vm).toBeInstanceOf(SessionViewModel);
|
||||
expect(vm?.userId).toBe('user-123');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should handle unexpected error types', async () => {
|
||||
const error = { customError: 'Something went wrong' };
|
||||
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('Something went wrong');
|
||||
});
|
||||
|
||||
it('should handle string errors', async () => {
|
||||
mockApiClient.getSession.mockRejectedValue('String error');
|
||||
|
||||
const result = await service.getSession();
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError().type).toBe('serverError');
|
||||
expect(result.getError().message).toBe('String error');
|
||||
});
|
||||
|
||||
it('should handle undefined errors', async () => {
|
||||
mockApiClient.getSession.mockRejectedValue(undefined);
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user