import { describe, it, expect, vi, beforeEach } from 'vitest'; import { DashboardService } from './DashboardService'; import { AnalyticsApiClient } from '../../api/analytics/AnalyticsApiClient'; import { AnalyticsDashboardPresenter } from '../../presenters/AnalyticsDashboardPresenter'; import { AnalyticsMetricsPresenter } from '../../presenters/AnalyticsMetricsPresenter'; import type { AnalyticsDashboardDto, AnalyticsMetricsDto } from '../../dtos'; import { AnalyticsDashboardViewModel, AnalyticsMetricsViewModel } from '../../view-models'; describe('DashboardService', () => { let mockApiClient: AnalyticsApiClient; let mockDashboardPresenter: AnalyticsDashboardPresenter; let mockMetricsPresenter: AnalyticsMetricsPresenter; let service: DashboardService; beforeEach(() => { mockApiClient = { recordPageView: vi.fn(), recordEngagement: vi.fn(), getDashboardData: vi.fn(), getAnalyticsMetrics: vi.fn(), } as unknown as AnalyticsApiClient; mockDashboardPresenter = { present: vi.fn(), } as unknown as AnalyticsDashboardPresenter; mockMetricsPresenter = { present: vi.fn(), } as unknown as AnalyticsMetricsPresenter; service = new DashboardService(mockApiClient, mockDashboardPresenter, mockMetricsPresenter); }); describe('getDashboardData', () => { it('should fetch dashboard data from API and transform via presenter', async () => { // Arrange const mockDto: AnalyticsDashboardDto = { totalUsers: 1000, activeUsers: 750, totalRaces: 50, totalLeagues: 25, }; const mockViewModel: AnalyticsDashboardViewModel = new AnalyticsDashboardViewModel(mockDto); vi.mocked(mockApiClient.getDashboardData).mockResolvedValue(mockDto); vi.mocked(mockDashboardPresenter.present).mockReturnValue(mockViewModel); // Act const result = await service.getDashboardData(); // Assert expect(mockApiClient.getDashboardData).toHaveBeenCalledTimes(1); expect(mockDashboardPresenter.present).toHaveBeenCalledWith(mockDto); expect(mockDashboardPresenter.present).toHaveBeenCalledTimes(1); expect(result).toBe(mockViewModel); }); it('should propagate API client errors', async () => { // Arrange const error = new Error('API Error: Failed to fetch dashboard data'); vi.mocked(mockApiClient.getDashboardData).mockRejectedValue(error); // Act & Assert await expect(service.getDashboardData()).rejects.toThrow( 'API Error: Failed to fetch dashboard data' ); expect(mockApiClient.getDashboardData).toHaveBeenCalledTimes(1); expect(mockDashboardPresenter.present).not.toHaveBeenCalled(); }); it('should propagate presenter errors', async () => { // Arrange const mockDto: AnalyticsDashboardDto = { totalUsers: 500, activeUsers: 300, totalRaces: 20, totalLeagues: 10, }; const error = new Error('Presenter Error: Invalid DTO structure'); vi.mocked(mockApiClient.getDashboardData).mockResolvedValue(mockDto); vi.mocked(mockDashboardPresenter.present).mockImplementation(() => { throw error; }); // Act & Assert await expect(service.getDashboardData()).rejects.toThrow( 'Presenter Error: Invalid DTO structure' ); expect(mockApiClient.getDashboardData).toHaveBeenCalledTimes(1); expect(mockDashboardPresenter.present).toHaveBeenCalledWith(mockDto); }); }); describe('getAnalyticsMetrics', () => { it('should fetch analytics metrics from API and transform via presenter', async () => { // Arrange const mockDto: AnalyticsMetricsDto = { pageViews: 5000, uniqueVisitors: 1200, averageSessionDuration: 180, bounceRate: 35.5, }; const mockViewModel: AnalyticsMetricsViewModel = new AnalyticsMetricsViewModel(mockDto); vi.mocked(mockApiClient.getAnalyticsMetrics).mockResolvedValue(mockDto); vi.mocked(mockMetricsPresenter.present).mockReturnValue(mockViewModel); // Act const result = await service.getAnalyticsMetrics(); // Assert expect(mockApiClient.getAnalyticsMetrics).toHaveBeenCalledTimes(1); expect(mockMetricsPresenter.present).toHaveBeenCalledWith(mockDto); expect(mockMetricsPresenter.present).toHaveBeenCalledTimes(1); expect(result).toBe(mockViewModel); }); it('should propagate API client errors', async () => { // Arrange const error = new Error('API Error: Failed to fetch analytics metrics'); vi.mocked(mockApiClient.getAnalyticsMetrics).mockRejectedValue(error); // Act & Assert await expect(service.getAnalyticsMetrics()).rejects.toThrow( 'API Error: Failed to fetch analytics metrics' ); expect(mockApiClient.getAnalyticsMetrics).toHaveBeenCalledTimes(1); expect(mockMetricsPresenter.present).not.toHaveBeenCalled(); }); it('should propagate presenter errors', async () => { // Arrange const mockDto: AnalyticsMetricsDto = { pageViews: 2500, uniqueVisitors: 600, averageSessionDuration: 120, bounceRate: 45.2, }; const error = new Error('Presenter Error: Invalid metrics data'); vi.mocked(mockApiClient.getAnalyticsMetrics).mockResolvedValue(mockDto); vi.mocked(mockMetricsPresenter.present).mockImplementation(() => { throw error; }); // Act & Assert await expect(service.getAnalyticsMetrics()).rejects.toThrow( 'Presenter Error: Invalid metrics data' ); expect(mockApiClient.getAnalyticsMetrics).toHaveBeenCalledTimes(1); expect(mockMetricsPresenter.present).toHaveBeenCalledWith(mockDto); }); }); describe('getDashboardOverview', () => { it('should delegate to getDashboardData for backward compatibility', async () => { // Arrange const mockDto: AnalyticsDashboardDto = { totalUsers: 800, activeUsers: 600, totalRaces: 40, totalLeagues: 20, }; const mockViewModel: AnalyticsDashboardViewModel = new AnalyticsDashboardViewModel(mockDto); vi.mocked(mockApiClient.getDashboardData).mockResolvedValue(mockDto); vi.mocked(mockDashboardPresenter.present).mockReturnValue(mockViewModel); // Act const result = await service.getDashboardOverview(); // Assert expect(mockApiClient.getDashboardData).toHaveBeenCalledTimes(1); expect(mockDashboardPresenter.present).toHaveBeenCalledWith(mockDto); expect(result).toBe(mockViewModel); }); }); describe('Constructor Dependency Injection', () => { it('should require apiClient, dashboardPresenter, and metricsPresenter', () => { // This test verifies the constructor signature expect(() => { new DashboardService(mockApiClient, mockDashboardPresenter, mockMetricsPresenter); }).not.toThrow(); }); it('should use injected dependencies', async () => { // Arrange const customApiClient = { recordPageView: vi.fn(), recordEngagement: vi.fn(), getDashboardData: vi.fn().mockResolvedValue({ totalUsers: 100, activeUsers: 80, totalRaces: 5, totalLeagues: 3, }), getAnalyticsMetrics: vi.fn(), } as unknown as AnalyticsApiClient; const customDashboardPresenter = { present: vi.fn().mockReturnValue({} as AnalyticsDashboardViewModel), } as unknown as AnalyticsDashboardPresenter; const customMetricsPresenter = { present: vi.fn(), } as unknown as AnalyticsMetricsPresenter; const customService = new DashboardService(customApiClient, customDashboardPresenter, customMetricsPresenter); // Act await customService.getDashboardData(); // Assert expect(customApiClient.getDashboardData).toHaveBeenCalledTimes(1); expect(customDashboardPresenter.present).toHaveBeenCalled(); }); }); });