import { describe, it, expect, vi, beforeEach } from 'vitest'; import { AdminService } from '@/lib/services/admin/AdminService'; import { AdminApiClient } from '@/lib/api/admin/AdminApiClient'; import { ApiError } from '@/lib/api/base/ApiError'; import type { DashboardStats, UserDto, UserListResponse } from '@/lib/types/admin'; // Mock the API client vi.mock('@/lib/api/admin/AdminApiClient'); describe('AdminService', () => { let service: AdminService; let mockApiClient: any; beforeEach(() => { vi.clearAllMocks(); service = new AdminService(); mockApiClient = (service as any).apiClient; }); describe('happy paths', () => { it('should successfully fetch dashboard statistics', async () => { const mockStats: DashboardStats = { totalUsers: 1250, activeUsers: 1100, suspendedUsers: 50, deletedUsers: 100, systemAdmins: 5, recentLogins: 450, newUsersToday: 12, userGrowth: [ { label: 'This week', value: 45, color: '#10b981' }, { label: 'Last week', value: 38, color: '#3b82f6' }, ], roleDistribution: [ { label: 'Users', value: 1200, color: '#6b7280' }, { label: 'Admins', value: 50, color: '#8b5cf6' }, ], statusDistribution: { active: 1100, suspended: 50, deleted: 100, }, activityTimeline: [ { date: '2024-01-01', newUsers: 10, logins: 200 }, { date: '2024-01-02', newUsers: 15, logins: 220 }, ], }; mockApiClient.getDashboardStats.mockResolvedValue(mockStats); const result = await service.getDashboardStats(); expect(result.isOk()).toBe(true); expect(result.unwrap()).toEqual(mockStats); expect(mockApiClient.getDashboardStats).toHaveBeenCalledTimes(1); }); it('should successfully list users with filtering', async () => { const mockUsers: UserDto[] = [ { id: '1', email: 'admin@example.com', displayName: 'Admin User', roles: ['owner', 'admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', lastLoginAt: '2024-01-15T10:00:00.000Z', primaryDriverId: 'driver-1', }, { id: '2', email: 'user@example.com', displayName: 'Regular User', roles: ['user'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-02T00:00:00.000Z', updatedAt: '2024-01-02T00:00:00.000Z', lastLoginAt: '2024-01-14T15:00:00.000Z', }, ]; const mockResponse: UserListResponse = { users: mockUsers, total: 2, page: 1, limit: 50, totalPages: 1, }; mockApiClient.listUsers.mockResolvedValue(mockResponse); const result = await service.listUsers(); expect(result.isOk()).toBe(true); expect(result.unwrap()).toEqual(mockResponse); expect(mockApiClient.listUsers).toHaveBeenCalledTimes(1); }); it('should successfully update user status', async () => { const userId = 'user-123'; const newStatus = 'suspended'; const mockUpdatedUser: UserDto = { id: userId, email: 'test@example.com', displayName: 'Test User', roles: ['user'], status: newStatus, isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', }; mockApiClient.updateUserStatus.mockResolvedValue(mockUpdatedUser); const result = await service.updateUserStatus(userId, newStatus); expect(result.isOk()).toBe(true); expect(result.unwrap()).toEqual(mockUpdatedUser); expect(mockApiClient.updateUserStatus).toHaveBeenCalledWith(userId, newStatus); }); it('should successfully delete user', async () => { mockApiClient.deleteUser.mockResolvedValue(undefined); const result = await service.deleteUser(); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBeUndefined(); expect(mockApiClient.deleteUser).toHaveBeenCalledTimes(1); }); }); describe('failure modes', () => { it('should handle dashboard stats fetch errors', async () => { const error = new ApiError( 'Dashboard stats not found', 'NOT_FOUND', { endpoint: '/admin/dashboard/stats', method: 'GET', timestamp: new Date().toISOString(), statusCode: 404, } ); mockApiClient.getDashboardStats.mockRejectedValue(error); const result = await service.getDashboardStats(); expect(result.isErr()).toBe(true); expect(result.getError()).toEqual({ type: 'notFound', message: 'Dashboard stats not found', }); }); it('should handle user list fetch errors', async () => { const error = new ApiError( 'Failed to fetch users', 'SERVER_ERROR', { endpoint: '/admin/users', method: 'GET', timestamp: new Date().toISOString(), statusCode: 500, } ); mockApiClient.listUsers.mockRejectedValue(error); const result = await service.listUsers(); expect(result.isErr()).toBe(true); expect(result.getError()).toEqual({ type: 'serverError', message: 'Failed to fetch users', }); }); it('should handle user status update errors', async () => { const error = new ApiError( 'Invalid user ID', 'VALIDATION_ERROR', { endpoint: '/admin/users/user-123/status', method: 'PATCH', timestamp: new Date().toISOString(), statusCode: 400, } ); mockApiClient.updateUserStatus.mockRejectedValue(error); const result = await service.updateUserStatus('user-123', 'active'); expect(result.isErr()).toBe(true); expect(result.getError()).toEqual({ type: 'unknown', message: 'Invalid user ID', }); }); it('should handle user deletion errors', async () => { const error = new ApiError( 'User not found', 'NOT_FOUND', { endpoint: '/admin/users/user-123', method: 'DELETE', timestamp: new Date().toISOString(), statusCode: 404, } ); mockApiClient.deleteUser.mockRejectedValue(error); const result = await service.deleteUser(); expect(result.isErr()).toBe(true); expect(result.getError()).toEqual({ type: 'notFound', message: 'User not found', }); }); it('should handle invalid user ID', async () => { const error = new ApiError( 'Invalid user ID format', 'VALIDATION_ERROR', { endpoint: '/admin/users/invalid-id/status', method: 'PATCH', timestamp: new Date().toISOString(), statusCode: 400, } ); mockApiClient.updateUserStatus.mockRejectedValue(error); const result = await service.updateUserStatus('invalid-id', 'active'); expect(result.isErr()).toBe(true); expect(result.getError()).toEqual({ type: 'unknown', message: 'Invalid user ID format', }); }); it('should handle invalid status value', async () => { const error = new ApiError( 'Invalid status value', 'VALIDATION_ERROR', { endpoint: '/admin/users/user-123/status', method: 'PATCH', timestamp: new Date().toISOString(), statusCode: 400, } ); mockApiClient.updateUserStatus.mockRejectedValue(error); const result = await service.updateUserStatus('user-123', 'invalid-status'); expect(result.isErr()).toBe(true); expect(result.getError()).toEqual({ type: 'unknown', message: 'Invalid status value', }); }); }); describe('retries', () => { it('should retry on transient API failures', async () => { const mockStats: DashboardStats = { totalUsers: 1250, activeUsers: 1100, suspendedUsers: 50, deletedUsers: 100, systemAdmins: 5, recentLogins: 450, newUsersToday: 12, userGrowth: [], roleDistribution: [], statusDistribution: { active: 1100, suspended: 50, deleted: 100 }, activityTimeline: [], }; const error = new ApiError( 'Network error', 'NETWORK_ERROR', { endpoint: '/admin/dashboard/stats', method: 'GET', timestamp: new Date().toISOString(), } ); // First call fails, second succeeds mockApiClient.getDashboardStats .mockRejectedValueOnce(error) .mockResolvedValueOnce(mockStats); const result = await service.getDashboardStats(); expect(result.isOk()).toBe(true); expect(result.unwrap()).toEqual(mockStats); expect(mockApiClient.getDashboardStats).toHaveBeenCalledTimes(2); }); it('should retry on timeout when fetching dashboard stats', async () => { const mockStats: DashboardStats = { totalUsers: 1250, activeUsers: 1100, suspendedUsers: 50, deletedUsers: 100, systemAdmins: 5, recentLogins: 450, newUsersToday: 12, userGrowth: [], roleDistribution: [], statusDistribution: { active: 1100, suspended: 50, deleted: 100 }, activityTimeline: [], }; const error = new ApiError( 'Request timed out after 30 seconds', 'TIMEOUT_ERROR', { endpoint: '/admin/dashboard/stats', method: 'GET', timestamp: new Date().toISOString(), } ); // First call times out, second succeeds mockApiClient.getDashboardStats .mockRejectedValueOnce(error) .mockResolvedValueOnce(mockStats); const result = await service.getDashboardStats(); expect(result.isOk()).toBe(true); expect(result.unwrap()).toEqual(mockStats); expect(mockApiClient.getDashboardStats).toHaveBeenCalledTimes(2); }); }); describe('fallback logic', () => { it('should use mock data when API is unavailable', async () => { const error = new ApiError( 'Unable to connect to server', 'NETWORK_ERROR', { endpoint: '/admin/dashboard/stats', method: 'GET', timestamp: new Date().toISOString(), } ); mockApiClient.getDashboardStats.mockRejectedValue(error); const result = await service.getDashboardStats(); // The service should return the mock data from the service itself expect(result.isOk()).toBe(true); const stats = result.unwrap(); expect(stats.totalUsers).toBe(1250); expect(stats.activeUsers).toBe(1100); }); it('should handle partial user data gracefully', async () => { const mockUsers: UserDto[] = [ { id: '1', email: 'admin@example.com', displayName: 'Admin User', roles: ['owner', 'admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', lastLoginAt: '2024-01-15T10:00:00.000Z', primaryDriverId: 'driver-1', }, { id: '2', email: 'user@example.com', displayName: 'Regular User', roles: ['user'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-02T00:00:00.000Z', updatedAt: '2024-01-02T00:00:00.000Z', // Missing lastLoginAt - partial data }, ]; const mockResponse: UserListResponse = { users: mockUsers, total: 2, page: 1, limit: 50, totalPages: 1, }; mockApiClient.listUsers.mockResolvedValue(mockResponse); const result = await service.listUsers(); expect(result.isOk()).toBe(true); const response = result.unwrap(); expect(response.users).toHaveLength(2); expect(response.users[0].lastLoginAt).toBeDefined(); expect(response.users[1].lastLoginAt).toBeUndefined(); }); it('should handle empty user list', async () => { const mockResponse: UserListResponse = { users: [], total: 0, page: 1, limit: 50, totalPages: 0, }; mockApiClient.listUsers.mockResolvedValue(mockResponse); const result = await service.listUsers(); expect(result.isOk()).toBe(true); const response = result.unwrap(); expect(response.users).toHaveLength(0); expect(response.total).toBe(0); }); }); describe('aggregation logic', () => { it('should aggregate dashboard statistics correctly', async () => { const mockStats: DashboardStats = { totalUsers: 1250, activeUsers: 1100, suspendedUsers: 50, deletedUsers: 100, systemAdmins: 5, recentLogins: 450, newUsersToday: 12, userGrowth: [ { label: 'This week', value: 45, color: '#10b981' }, { label: 'Last week', value: 38, color: '#3b82f6' }, ], roleDistribution: [ { label: 'Users', value: 1200, color: '#6b7280' }, { label: 'Admins', value: 50, color: '#8b5cf6' }, ], statusDistribution: { active: 1100, suspended: 50, deleted: 100, }, activityTimeline: [ { date: '2024-01-01', newUsers: 10, logins: 200 }, { date: '2024-01-02', newUsers: 15, logins: 220 }, ], }; mockApiClient.getDashboardStats.mockResolvedValue(mockStats); const result = await service.getDashboardStats(); expect(result.isOk()).toBe(true); const stats = result.unwrap(); // Verify aggregation expect(stats.totalUsers).toBe(1250); expect(stats.activeUsers).toBe(1100); expect(stats.suspendedUsers).toBe(50); expect(stats.deletedUsers).toBe(100); expect(stats.systemAdmins).toBe(5); expect(stats.recentLogins).toBe(450); expect(stats.newUsersToday).toBe(12); // Verify user growth aggregation expect(stats.userGrowth).toHaveLength(2); expect(stats.userGrowth[0].value).toBe(45); expect(stats.userGrowth[1].value).toBe(38); // Verify role distribution aggregation expect(stats.roleDistribution).toHaveLength(2); expect(stats.roleDistribution[0].value).toBe(1200); expect(stats.roleDistribution[1].value).toBe(50); // Verify status distribution aggregation expect(stats.statusDistribution.active).toBe(1100); expect(stats.statusDistribution.suspended).toBe(50); expect(stats.statusDistribution.deleted).toBe(100); // Verify activity timeline aggregation expect(stats.activityTimeline).toHaveLength(2); expect(stats.activityTimeline[0].newUsers).toBe(10); expect(stats.activityTimeline[1].newUsers).toBe(15); }); it('should calculate user growth metrics', async () => { const mockStats: DashboardStats = { totalUsers: 1250, activeUsers: 1100, suspendedUsers: 50, deletedUsers: 100, systemAdmins: 5, recentLogins: 450, newUsersToday: 12, userGrowth: [ { label: 'This week', value: 45, color: '#10b981' }, { label: 'Last week', value: 38, color: '#3b82f6' }, ], roleDistribution: [], statusDistribution: { active: 1100, suspended: 50, deleted: 100 }, activityTimeline: [], }; mockApiClient.getDashboardStats.mockResolvedValue(mockStats); const result = await service.getDashboardStats(); expect(result.isOk()).toBe(true); const stats = result.unwrap(); // Calculate growth percentage const growth = stats.userGrowth; expect(growth).toHaveLength(2); expect(growth[0].value).toBe(45); expect(growth[1].value).toBe(38); // Verify growth trend const growthTrend = ((45 - 38) / 38) * 100; expect(growthTrend).toBeCloseTo(18.42, 1); }); it('should aggregate role distribution data', async () => { const mockStats: DashboardStats = { totalUsers: 1250, activeUsers: 1100, suspendedUsers: 50, deletedUsers: 100, systemAdmins: 5, recentLogins: 450, newUsersToday: 12, userGrowth: [], roleDistribution: [ { label: 'Users', value: 1200, color: '#6b7280' }, { label: 'Admins', value: 50, color: '#8b5cf6' }, ], statusDistribution: { active: 1100, suspended: 50, deleted: 100 }, activityTimeline: [], }; mockApiClient.getDashboardStats.mockResolvedValue(mockStats); const result = await service.getDashboardStats(); expect(result.isOk()).toBe(true); const stats = result.unwrap(); // Verify role distribution expect(stats.roleDistribution).toHaveLength(2); expect(stats.roleDistribution[0].label).toBe('Users'); expect(stats.roleDistribution[0].value).toBe(1200); expect(stats.roleDistribution[1].label).toBe('Admins'); expect(stats.roleDistribution[1].value).toBe(50); // Verify total matches const totalRoles = stats.roleDistribution.reduce((sum, role) => sum + role.value, 0); expect(totalRoles).toBe(1250); }); it('should aggregate status distribution data', async () => { const mockStats: DashboardStats = { totalUsers: 1250, activeUsers: 1100, suspendedUsers: 50, deletedUsers: 100, systemAdmins: 5, recentLogins: 450, newUsersToday: 12, userGrowth: [], roleDistribution: [], statusDistribution: { active: 1100, suspended: 50, deleted: 100, }, activityTimeline: [], }; mockApiClient.getDashboardStats.mockResolvedValue(mockStats); const result = await service.getDashboardStats(); expect(result.isOk()).toBe(true); const stats = result.unwrap(); // Verify status distribution expect(stats.statusDistribution.active).toBe(1100); expect(stats.statusDistribution.suspended).toBe(50); expect(stats.statusDistribution.deleted).toBe(100); // Verify total matches const totalStatuses = stats.statusDistribution.active + stats.statusDistribution.suspended + stats.statusDistribution.deleted; expect(totalStatuses).toBe(1250); }); it('should aggregate activity timeline data', async () => { const mockStats: DashboardStats = { totalUsers: 1250, activeUsers: 1100, suspendedUsers: 50, deletedUsers: 100, systemAdmins: 5, recentLogins: 450, newUsersToday: 12, userGrowth: [], roleDistribution: [], statusDistribution: { active: 1100, suspended: 50, deleted: 100 }, activityTimeline: [ { date: '2024-01-01', newUsers: 10, logins: 200 }, { date: '2024-01-02', newUsers: 15, logins: 220 }, { date: '2024-01-03', newUsers: 20, logins: 250 }, ], }; mockApiClient.getDashboardStats.mockResolvedValue(mockStats); const result = await service.getDashboardStats(); expect(result.isOk()).toBe(true); const stats = result.unwrap(); // Verify activity timeline expect(stats.activityTimeline).toHaveLength(3); expect(stats.activityTimeline[0].date).toBe('2024-01-01'); expect(stats.activityTimeline[0].newUsers).toBe(10); expect(stats.activityTimeline[0].logins).toBe(200); // Calculate total new users const totalNewUsers = stats.activityTimeline.reduce((sum, day) => sum + day.newUsers, 0); expect(totalNewUsers).toBe(45); // Calculate total logins const totalLogins = stats.activityTimeline.reduce((sum, day) => sum + day.logins, 0); expect(totalLogins).toBe(670); }); }); describe('decision branches', () => { it('should handle different user roles correctly', async () => { const mockUsers: UserDto[] = [ { id: '1', email: 'owner@example.com', displayName: 'Owner', roles: ['owner'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', }, { id: '2', email: 'admin@example.com', displayName: 'Admin', roles: ['admin'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-02T00:00:00.000Z', updatedAt: '2024-01-02T00:00:00.000Z', }, { id: '3', email: 'user@example.com', displayName: 'User', roles: ['user'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-03T00:00:00.000Z', updatedAt: '2024-01-03T00:00:00.000Z', }, ]; const mockResponse: UserListResponse = { users: mockUsers, total: 3, page: 1, limit: 50, totalPages: 1, }; mockApiClient.listUsers.mockResolvedValue(mockResponse); const result = await service.listUsers(); expect(result.isOk()).toBe(true); const response = result.unwrap(); // Verify different roles are handled correctly expect(response.users[0].roles).toContain('owner'); expect(response.users[0].isSystemAdmin).toBe(true); expect(response.users[1].roles).toContain('admin'); expect(response.users[1].isSystemAdmin).toBe(false); expect(response.users[2].roles).toContain('user'); expect(response.users[2].isSystemAdmin).toBe(false); }); it('should handle different user statuses', async () => { const mockUsers: UserDto[] = [ { id: '1', email: 'active@example.com', displayName: 'Active User', roles: ['user'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', }, { id: '2', email: 'suspended@example.com', displayName: 'Suspended User', roles: ['user'], status: 'suspended', isSystemAdmin: false, createdAt: '2024-01-02T00:00:00.000Z', updatedAt: '2024-01-02T00:00:00.000Z', }, { id: '3', email: 'deleted@example.com', displayName: 'Deleted User', roles: ['user'], status: 'deleted', isSystemAdmin: false, createdAt: '2024-01-03T00:00:00.000Z', updatedAt: '2024-01-03T00:00:00.000Z', }, ]; const mockResponse: UserListResponse = { users: mockUsers, total: 3, page: 1, limit: 50, totalPages: 1, }; mockApiClient.listUsers.mockResolvedValue(mockResponse); const result = await service.listUsers(); expect(result.isOk()).toBe(true); const response = result.unwrap(); // Verify different statuses are handled correctly expect(response.users[0].status).toBe('active'); expect(response.users[1].status).toBe('suspended'); expect(response.users[2].status).toBe('deleted'); }); it('should handle different pagination scenarios', async () => { const mockUsers: UserDto[] = [ { id: '1', email: 'user1@example.com', displayName: 'User 1', roles: ['user'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', }, ]; const mockResponse: UserListResponse = { users: mockUsers, total: 100, page: 2, limit: 50, totalPages: 2, }; mockApiClient.listUsers.mockResolvedValue(mockResponse); const result = await service.listUsers(); expect(result.isOk()).toBe(true); const response = result.unwrap(); // Verify pagination metadata expect(response.page).toBe(2); expect(response.limit).toBe(50); expect(response.totalPages).toBe(2); expect(response.total).toBe(100); }); it('should handle different filtering options', async () => { const mockUsers: UserDto[] = [ { id: '1', email: 'admin@example.com', displayName: 'Admin User', roles: ['admin'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', }, ]; const mockResponse: UserListResponse = { users: mockUsers, total: 1, page: 1, limit: 50, totalPages: 1, }; mockApiClient.listUsers.mockResolvedValue(mockResponse); const result = await service.listUsers(); expect(result.isOk()).toBe(true); const response = result.unwrap(); // Verify filtered results expect(response.users).toHaveLength(1); expect(response.users[0].roles).toContain('admin'); expect(response.users[0].status).toBe('active'); }); it('should handle system admin vs regular admin', async () => { const mockUsers: UserDto[] = [ { id: '1', email: 'system@example.com', displayName: 'System Admin', roles: ['owner', 'admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', }, { id: '2', email: 'regular@example.com', displayName: 'Regular Admin', roles: ['admin'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-02T00:00:00.000Z', updatedAt: '2024-01-02T00:00:00.000Z', }, ]; const mockResponse: UserListResponse = { users: mockUsers, total: 2, page: 1, limit: 50, totalPages: 1, }; mockApiClient.listUsers.mockResolvedValue(mockResponse); const result = await service.listUsers(); expect(result.isOk()).toBe(true); const response = result.unwrap(); // Verify system admin vs regular admin expect(response.users[0].isSystemAdmin).toBe(true); expect(response.users[0].roles).toContain('owner'); expect(response.users[1].isSystemAdmin).toBe(false); expect(response.users[1].roles).not.toContain('owner'); }); it('should handle soft delete vs hard delete', async () => { // Test soft delete (status change to 'deleted') const mockUpdatedUser: UserDto = { id: 'user-123', email: 'test@example.com', displayName: 'Test User', roles: ['user'], status: 'deleted', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', }; mockApiClient.updateUserStatus.mockResolvedValue(mockUpdatedUser); const result = await service.updateUserStatus('user-123', 'deleted'); expect(result.isOk()).toBe(true); const user = result.unwrap(); expect(user.status).toBe('deleted'); expect(user.id).toBe('user-123'); // Test hard delete (actual deletion) mockApiClient.deleteUser.mockResolvedValue(undefined); const deleteResult = await service.deleteUser(); expect(deleteResult.isOk()).toBe(true); expect(deleteResult.unwrap()).toBeUndefined(); }); }); });