add tests
Some checks failed
Contract Testing / contract-tests (push) Failing after 6m7s
Contract Testing / contract-snapshot (push) Failing after 4m46s

This commit is contained in:
2026-01-22 11:52:42 +01:00
parent 40bc15ff61
commit fb1221701d
112 changed files with 30625 additions and 1059 deletions

View File

@@ -1,119 +1,920 @@
import { describe, it, expect } from 'vitest';
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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', () => {
// TODO: Implement test
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();
});
});
});

View File

@@ -1,83 +1,741 @@
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { DashboardService } from '@/lib/services/analytics/DashboardService';
import { DashboardApiClient } from '@/lib/api/dashboard/DashboardApiClient';
import { AnalyticsApiClient } from '@/lib/api/analytics/AnalyticsApiClient';
import { ApiError } from '@/lib/api/base/ApiError';
import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
import type { GetAnalyticsMetricsOutputDTO } from '@/lib/types/generated/GetAnalyticsMetricsOutputDTO';
// Mock the API clients
vi.mock('@/lib/api/dashboard/DashboardApiClient');
vi.mock('@/lib/api/analytics/AnalyticsApiClient');
describe('DashboardService', () => {
let service: DashboardService;
let mockDashboardApiClient: any;
let mockAnalyticsApiClient: any;
beforeEach(() => {
vi.clearAllMocks();
service = new DashboardService();
mockDashboardApiClient = (service as any).apiClient;
mockAnalyticsApiClient = (service as any).analyticsApiClient;
});
describe('happy paths', () => {
it('should successfully fetch dashboard overview', () => {
// TODO: Implement test
it('should successfully fetch dashboard overview', async () => {
const mockOverview: DashboardOverviewDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
avatarUrl: 'https://example.com/avatar.jpg',
rating: 1500,
rank: 10,
},
myUpcomingRaces: [
{
id: 'race-1',
name: 'Race 1',
date: '2024-01-15T10:00:00.000Z',
track: 'Track 1',
league: 'League 1',
},
],
otherUpcomingRaces: [
{
id: 'race-2',
name: 'Race 2',
date: '2024-01-16T10:00:00.000Z',
track: 'Track 2',
league: 'League 2',
},
],
upcomingRaces: [
{
id: 'race-1',
name: 'Race 1',
date: '2024-01-15T10:00:00.000Z',
track: 'Track 1',
league: 'League 1',
},
{
id: 'race-2',
name: 'Race 2',
date: '2024-01-16T10:00:00.000Z',
track: 'Track 2',
league: 'League 2',
},
],
activeLeaguesCount: 3,
nextRace: {
id: 'race-1',
name: 'Race 1',
date: '2024-01-15T10:00:00.000Z',
track: 'Track 1',
league: 'League 1',
},
recentResults: [
{
raceId: 'race-0',
position: 5,
points: 15,
date: '2024-01-10T10:00:00.000Z',
},
],
leagueStandingsSummaries: [
{
leagueId: 'league-1',
leagueName: 'League 1',
position: 3,
points: 150,
},
],
feedSummary: {
unreadCount: 5,
latestPosts: [
{
id: 'post-1',
title: 'New Season Announcement',
date: '2024-01-14T10:00:00.000Z',
},
],
},
friends: [
{
id: 'friend-1',
name: 'Friend 1',
avatarUrl: 'https://example.com/friend1.jpg',
status: 'online',
},
],
};
mockDashboardApiClient.getDashboardOverview.mockResolvedValue(mockOverview);
const result = await service.getDashboardOverview();
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toEqual(mockOverview);
expect(mockDashboardApiClient.getDashboardOverview).toHaveBeenCalledTimes(1);
});
it('should successfully fetch analytics metrics', () => {
// TODO: Implement test
it('should successfully fetch analytics metrics', async () => {
const mockMetrics: GetAnalyticsMetricsOutputDTO = {
pageViews: 15000,
uniqueVisitors: 8500,
averageSessionDuration: 180,
bounceRate: 0.35,
};
mockAnalyticsApiClient.getAnalyticsMetrics.mockResolvedValue(mockMetrics);
const result = await service.getAnalyticsMetrics();
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toEqual(mockMetrics);
expect(mockAnalyticsApiClient.getAnalyticsMetrics).toHaveBeenCalledTimes(1);
});
});
describe('failure modes', () => {
it('should handle not found errors', () => {
// TODO: Implement test
it('should handle not found errors', async () => {
const error = new ApiError(
'Dashboard not found',
'NOT_FOUND',
{
endpoint: '/dashboard/overview',
method: 'GET',
timestamp: new Date().toISOString(),
statusCode: 404,
}
);
mockDashboardApiClient.getDashboardOverview.mockRejectedValue(error);
const result = await service.getDashboardOverview();
expect(result.isErr()).toBe(true);
expect(result.getError()).toEqual({
type: 'notFound',
message: 'Dashboard not found',
});
});
it('should handle unauthorized errors', () => {
// TODO: Implement test
it('should handle unauthorized errors', async () => {
const error = new ApiError(
'Unauthorized access',
'AUTH_ERROR',
{
endpoint: '/dashboard/overview',
method: 'GET',
timestamp: new Date().toISOString(),
statusCode: 401,
}
);
mockDashboardApiClient.getDashboardOverview.mockRejectedValue(error);
const result = await service.getDashboardOverview();
expect(result.isErr()).toBe(true);
expect(result.getError()).toEqual({
type: 'unauthorized',
message: 'Unauthorized access',
});
});
it('should handle server errors', () => {
// TODO: Implement test
it('should handle server errors', async () => {
const error = new ApiError(
'Internal server error',
'SERVER_ERROR',
{
endpoint: '/dashboard/overview',
method: 'GET',
timestamp: new Date().toISOString(),
statusCode: 500,
}
);
mockDashboardApiClient.getDashboardOverview.mockRejectedValue(error);
const result = await service.getDashboardOverview();
expect(result.isErr()).toBe(true);
expect(result.getError()).toEqual({
type: 'serverError',
message: 'Internal server error',
});
});
it('should handle network errors', () => {
// TODO: Implement test
it('should handle network errors', async () => {
const error = new ApiError(
'Network error: Unable to reach the API server',
'NETWORK_ERROR',
{
endpoint: '/dashboard/overview',
method: 'GET',
timestamp: new Date().toISOString(),
}
);
mockDashboardApiClient.getDashboardOverview.mockRejectedValue(error);
const result = await service.getDashboardOverview();
expect(result.isErr()).toBe(true);
expect(result.getError()).toEqual({
type: 'networkError',
message: 'Network error: Unable to reach the API server',
});
});
it('should handle timeout errors', () => {
// TODO: Implement test
it('should handle timeout errors', async () => {
const error = new ApiError(
'Request timed out after 30 seconds',
'TIMEOUT_ERROR',
{
endpoint: '/dashboard/overview',
method: 'GET',
timestamp: new Date().toISOString(),
}
);
mockDashboardApiClient.getDashboardOverview.mockRejectedValue(error);
const result = await service.getDashboardOverview();
expect(result.isErr()).toBe(true);
expect(result.getError()).toEqual({
type: 'networkError',
message: 'Request timed out after 30 seconds',
});
});
it('should handle unknown errors', () => {
// TODO: Implement test
it('should handle unknown errors', async () => {
const error = new Error('Something went wrong');
mockDashboardApiClient.getDashboardOverview.mockRejectedValue(error);
const result = await service.getDashboardOverview();
expect(result.isErr()).toBe(true);
expect(result.getError()).toEqual({
type: 'unknown',
message: 'Something went wrong',
});
});
});
describe('retries', () => {
it('should retry on network failure', () => {
// TODO: Implement test
it('should retry on network failure', async () => {
const mockOverview: DashboardOverviewDTO = {
currentDriver: undefined,
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 0,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: { unreadCount: 0, latestPosts: [] },
friends: [],
};
const error = new ApiError(
'Network error',
'NETWORK_ERROR',
{
endpoint: '/dashboard/overview',
method: 'GET',
timestamp: new Date().toISOString(),
}
);
// First call fails, second succeeds
mockDashboardApiClient.getDashboardOverview
.mockRejectedValueOnce(error)
.mockResolvedValueOnce(mockOverview);
const result = await service.getDashboardOverview();
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toEqual(mockOverview);
expect(mockDashboardApiClient.getDashboardOverview).toHaveBeenCalledTimes(2);
});
it('should retry on timeout', () => {
// TODO: Implement test
it('should retry on timeout', async () => {
const mockOverview: DashboardOverviewDTO = {
currentDriver: undefined,
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 0,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: { unreadCount: 0, latestPosts: [] },
friends: [],
};
const error = new ApiError(
'Request timed out after 30 seconds',
'TIMEOUT_ERROR',
{
endpoint: '/dashboard/overview',
method: 'GET',
timestamp: new Date().toISOString(),
}
);
// First call times out, second succeeds
mockDashboardApiClient.getDashboardOverview
.mockRejectedValueOnce(error)
.mockResolvedValueOnce(mockOverview);
const result = await service.getDashboardOverview();
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toEqual(mockOverview);
expect(mockDashboardApiClient.getDashboardOverview).toHaveBeenCalledTimes(2);
});
});
describe('fallback logic', () => {
it('should use fallback when primary API fails', () => {
// TODO: Implement test
it('should use fallback when primary API fails', async () => {
const error = new ApiError(
'Unable to connect to server',
'NETWORK_ERROR',
{
endpoint: '/dashboard/overview',
method: 'GET',
timestamp: new Date().toISOString(),
}
);
mockDashboardApiClient.getDashboardOverview.mockRejectedValue(error);
const result = await service.getDashboardOverview();
// The service should return an error result, not fallback data
expect(result.isErr()).toBe(true);
expect(result.getError()).toEqual({
type: 'networkError',
message: 'Unable to connect to server',
});
});
it('should handle partial data gracefully', () => {
// TODO: Implement test
it('should handle partial data gracefully', async () => {
const mockOverview: DashboardOverviewDTO = {
currentDriver: undefined, // Missing driver data
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 0,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: { unreadCount: 0, latestPosts: [] },
friends: [],
};
mockDashboardApiClient.getDashboardOverview.mockResolvedValue(mockOverview);
const result = await service.getDashboardOverview();
expect(result.isOk()).toBe(true);
const overview = result.unwrap();
expect(overview.currentDriver).toBeUndefined();
expect(overview.myUpcomingRaces).toHaveLength(0);
expect(overview.activeLeaguesCount).toBe(0);
});
});
describe('aggregation logic', () => {
it('should aggregate dashboard data correctly', () => {
// TODO: Implement test
it('should aggregate dashboard data correctly', async () => {
const mockOverview: DashboardOverviewDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
avatarUrl: 'https://example.com/avatar.jpg',
rating: 1500,
rank: 10,
},
myUpcomingRaces: [
{
id: 'race-1',
name: 'Race 1',
date: '2024-01-15T10:00:00.000Z',
track: 'Track 1',
league: 'League 1',
},
{
id: 'race-2',
name: 'Race 2',
date: '2024-01-16T10:00:00.000Z',
track: 'Track 2',
league: 'League 2',
},
],
otherUpcomingRaces: [
{
id: 'race-3',
name: 'Race 3',
date: '2024-01-17T10:00:00.000Z',
track: 'Track 3',
league: 'League 3',
},
],
upcomingRaces: [
{
id: 'race-1',
name: 'Race 1',
date: '2024-01-15T10:00:00.000Z',
track: 'Track 1',
league: 'League 1',
},
{
id: 'race-2',
name: 'Race 2',
date: '2024-01-16T10:00:00.000Z',
track: 'Track 2',
league: 'League 2',
},
{
id: 'race-3',
name: 'Race 3',
date: '2024-01-17T10:00:00.000Z',
track: 'Track 3',
league: 'League 3',
},
],
activeLeaguesCount: 3,
nextRace: {
id: 'race-1',
name: 'Race 1',
date: '2024-01-15T10:00:00.000Z',
track: 'Track 1',
league: 'League 1',
},
recentResults: [
{
raceId: 'race-0',
position: 5,
points: 15,
date: '2024-01-10T10:00:00.000Z',
},
{
raceId: 'race--1',
position: 3,
points: 20,
date: '2024-01-09T10:00:00.000Z',
},
],
leagueStandingsSummaries: [
{
leagueId: 'league-1',
leagueName: 'League 1',
position: 3,
points: 150,
},
{
leagueId: 'league-2',
leagueName: 'League 2',
position: 1,
points: 200,
},
],
feedSummary: {
unreadCount: 5,
latestPosts: [
{
id: 'post-1',
title: 'New Season Announcement',
date: '2024-01-14T10:00:00.000Z',
},
{
id: 'post-2',
title: 'Race Results Published',
date: '2024-01-13T10:00:00.000Z',
},
],
},
friends: [
{
id: 'friend-1',
name: 'Friend 1',
avatarUrl: 'https://example.com/friend1.jpg',
status: 'online',
},
{
id: 'friend-2',
name: 'Friend 2',
avatarUrl: 'https://example.com/friend2.jpg',
status: 'offline',
},
],
};
mockDashboardApiClient.getDashboardOverview.mockResolvedValue(mockOverview);
const result = await service.getDashboardOverview();
expect(result.isOk()).toBe(true);
const overview = result.unwrap();
// Verify aggregation
expect(overview.currentDriver).toBeDefined();
expect(overview.currentDriver.id).toBe('driver-123');
expect(overview.currentDriver.rating).toBe(1500);
// Verify race aggregation
expect(overview.myUpcomingRaces).toHaveLength(2);
expect(overview.otherUpcomingRaces).toHaveLength(1);
expect(overview.upcomingRaces).toHaveLength(3);
// Verify league aggregation
expect(overview.activeLeaguesCount).toBe(3);
expect(overview.leagueStandingsSummaries).toHaveLength(2);
// Verify results aggregation
expect(overview.recentResults).toHaveLength(2);
const totalPoints = overview.recentResults.reduce((sum, r) => sum + r.points, 0);
expect(totalPoints).toBe(35);
// Verify feed aggregation
expect(overview.feedSummary.unreadCount).toBe(5);
expect(overview.feedSummary.latestPosts).toHaveLength(2);
// Verify friends aggregation
expect(overview.friends).toHaveLength(2);
const onlineFriends = overview.friends.filter(f => f.status === 'online').length;
expect(onlineFriends).toBe(1);
});
it('should combine analytics metrics from multiple sources', () => {
// TODO: Implement test
it('should combine analytics metrics from multiple sources', async () => {
const mockMetrics: GetAnalyticsMetricsOutputDTO = {
pageViews: 15000,
uniqueVisitors: 8500,
averageSessionDuration: 180,
bounceRate: 0.35,
};
mockAnalyticsApiClient.getAnalyticsMetrics.mockResolvedValue(mockMetrics);
const result = await service.getAnalyticsMetrics();
expect(result.isOk()).toBe(true);
const metrics = result.unwrap();
// Verify metrics are returned correctly
expect(metrics.pageViews).toBe(15000);
expect(metrics.uniqueVisitors).toBe(8500);
expect(metrics.averageSessionDuration).toBe(180);
expect(metrics.bounceRate).toBe(0.35);
// Verify derived metrics
const pageViewsPerVisitor = metrics.pageViews / metrics.uniqueVisitors;
expect(pageViewsPerVisitor).toBeCloseTo(1.76, 2);
const bounceRatePercentage = metrics.bounceRate * 100;
expect(bounceRatePercentage).toBe(35);
});
});
describe('decision branches', () => {
it('should handle different error types correctly', () => {
// TODO: Implement test
it('should handle different error types correctly', async () => {
const errorTypes = [
{ type: 'NOT_FOUND', expectedErrorType: 'notFound' },
{ type: 'AUTH_ERROR', expectedErrorType: 'unauthorized' },
{ type: 'SERVER_ERROR', expectedErrorType: 'serverError' },
{ type: 'NETWORK_ERROR', expectedErrorType: 'networkError' },
{ type: 'TIMEOUT_ERROR', expectedErrorType: 'networkError' },
];
for (const { type, expectedErrorType } of errorTypes) {
const error = new ApiError(
`Error of type ${type}`,
type as any,
{
endpoint: '/dashboard/overview',
method: 'GET',
timestamp: new Date().toISOString(),
}
);
mockDashboardApiClient.getDashboardOverview.mockRejectedValue(error);
const result = await service.getDashboardOverview();
expect(result.isErr()).toBe(true);
expect(result.getError()).toEqual({
type: expectedErrorType,
message: `Error of type ${type}`,
});
}
});
it('should handle different API response formats', () => {
// TODO: Implement test
it('should handle different API response formats', async () => {
// Test with minimal response
const minimalOverview: DashboardOverviewDTO = {
currentDriver: undefined,
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 0,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: { unreadCount: 0, latestPosts: [] },
friends: [],
};
mockDashboardApiClient.getDashboardOverview.mockResolvedValue(minimalOverview);
const result = await service.getDashboardOverview();
expect(result.isOk()).toBe(true);
const overview = result.unwrap();
expect(overview.activeLeaguesCount).toBe(0);
expect(overview.upcomingRaces).toHaveLength(0);
// Test with full response
const fullOverview: DashboardOverviewDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
avatarUrl: 'https://example.com/avatar.jpg',
rating: 1500,
rank: 10,
},
myUpcomingRaces: [
{
id: 'race-1',
name: 'Race 1',
date: '2024-01-15T10:00:00.000Z',
track: 'Track 1',
league: 'League 1',
},
],
otherUpcomingRaces: [],
upcomingRaces: [
{
id: 'race-1',
name: 'Race 1',
date: '2024-01-15T10:00:00.000Z',
track: 'Track 1',
league: 'League 1',
},
],
activeLeaguesCount: 1,
nextRace: {
id: 'race-1',
name: 'Race 1',
date: '2024-01-15T10:00:00.000Z',
track: 'Track 1',
league: 'League 1',
},
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: { unreadCount: 0, latestPosts: [] },
friends: [],
};
mockDashboardApiClient.getDashboardOverview.mockResolvedValue(fullOverview);
const result2 = await service.getDashboardOverview();
expect(result2.isOk()).toBe(true);
const overview2 = result2.unwrap();
expect(overview2.currentDriver).toBeDefined();
expect(overview2.currentDriver.id).toBe('driver-123');
expect(overview2.activeLeaguesCount).toBe(1);
});
it('should handle different user permission levels', () => {
// TODO: Implement test
it('should handle different user permission levels', async () => {
// Test with driver data (normal user)
const driverOverview: DashboardOverviewDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
avatarUrl: 'https://example.com/avatar.jpg',
rating: 1500,
rank: 10,
},
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 0,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: { unreadCount: 0, latestPosts: [] },
friends: [],
};
mockDashboardApiClient.getDashboardOverview.mockResolvedValue(driverOverview);
const result = await service.getDashboardOverview();
expect(result.isOk()).toBe(true);
const overview = result.unwrap();
expect(overview.currentDriver).toBeDefined();
expect(overview.currentDriver.id).toBe('driver-123');
// Test without driver data (guest user or no driver assigned)
const guestOverview: DashboardOverviewDTO = {
currentDriver: undefined,
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 0,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: { unreadCount: 0, latestPosts: [] },
friends: [],
};
mockDashboardApiClient.getDashboardOverview.mockResolvedValue(guestOverview);
const result2 = await service.getDashboardOverview();
expect(result2.isOk()).toBe(true);
const overview2 = result2.unwrap();
expect(overview2.currentDriver).toBeUndefined();
});
});
});

File diff suppressed because it is too large Load Diff