diff --git a/maternal-app/maternal-app-backend/src/modules/analytics/report.service.spec.ts b/maternal-app/maternal-app-backend/src/modules/analytics/report.service.spec.ts new file mode 100644 index 0000000..0a4607e --- /dev/null +++ b/maternal-app/maternal-app-backend/src/modules/analytics/report.service.spec.ts @@ -0,0 +1,448 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository, Between } from 'typeorm'; +import { ReportService } from './report.service'; +import { Activity, ActivityType } from '../../database/entities/activity.entity'; +import { Child } from '../../database/entities/child.entity'; +import { PatternAnalysisService } from './pattern-analysis.service'; +import { PredictionService } from './prediction.service'; + +describe('ReportService', () => { + let service: ReportService; + let activityRepository: Repository; + let childRepository: Repository; + let patternAnalysisService: PatternAnalysisService; + let predictionService: PredictionService; + + const mockChild = { + id: 'child_123', + name: 'Baby Jane', + birthDate: new Date(Date.now() - 3 * 30 * 24 * 60 * 60 * 1000), + familyId: 'family_123', + }; + + const mockActivities = [ + { + id: '1', + type: ActivityType.SLEEP, + startedAt: new Date('2025-01-01T20:00:00'), + endedAt: new Date('2025-01-02T06:00:00'), + }, + { + id: '2', + type: ActivityType.FEEDING, + startedAt: new Date('2025-01-01T08:00:00'), + metadata: { method: 'bottle' }, + }, + { + id: '3', + type: ActivityType.DIAPER, + startedAt: new Date('2025-01-01T09:00:00'), + metadata: { type: 'wet' }, + }, + ]; + + const mockActivityRepository = { + find: jest.fn(), + }; + + const mockChildRepository = { + findOne: jest.fn(), + }; + + const mockPatternAnalysisService = { + analyzePatterns: jest.fn().mockResolvedValue({ + sleep: { averageDuration: 600, trend: 'stable' }, + feeding: { averageInterval: 3, trend: 'stable' }, + diaper: { wetDiapersPerDay: 6, isHealthy: true }, + recommendations: [], + concernsDetected: [], + }), + }; + + const mockPredictionService = { + generatePredictions: jest.fn().mockResolvedValue({ + sleep: { nextNapTime: new Date(), nextNapConfidence: 0.85 }, + feeding: { nextFeedingTime: new Date(), confidence: 0.9 }, + generatedAt: new Date(), + }), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ReportService, + { + provide: getRepositoryToken(Activity), + useValue: mockActivityRepository, + }, + { + provide: getRepositoryToken(Child), + useValue: mockChildRepository, + }, + { + provide: PatternAnalysisService, + useValue: mockPatternAnalysisService, + }, + { + provide: PredictionService, + useValue: mockPredictionService, + }, + ], + }).compile(); + + service = module.get(ReportService); + activityRepository = module.get>( + getRepositoryToken(Activity), + ); + childRepository = module.get>( + getRepositoryToken(Child), + ); + patternAnalysisService = module.get( + PatternAnalysisService, + ); + predictionService = module.get(PredictionService); + + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('generateWeeklyReport', () => { + it('should generate weekly report for a child', async () => { + mockChildRepository.findOne.mockResolvedValue(mockChild); + mockActivityRepository.find.mockResolvedValue(mockActivities); + + const result = await service.generateWeeklyReport('child_123'); + + expect(result).toHaveProperty('childId', 'child_123'); + expect(result).toHaveProperty('childName', 'Baby Jane'); + expect(result).toHaveProperty('weekStart'); + expect(result).toHaveProperty('weekEnd'); + expect(result).toHaveProperty('summary'); + expect(result).toHaveProperty('patterns'); + expect(result).toHaveProperty('predictions'); + expect(result).toHaveProperty('highlights'); + expect(result).toHaveProperty('concerns'); + }); + + it('should use custom start date when provided', async () => { + const customStart = new Date('2025-01-01'); + mockChildRepository.findOne.mockResolvedValue(mockChild); + mockActivityRepository.find.mockResolvedValue(mockActivities); + + const result = await service.generateWeeklyReport('child_123', customStart); + + expect(result.weekStart).toEqual(customStart); + expect(activityRepository.find).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ + childId: 'child_123', + startedAt: expect.any(Object), + }), + }), + ); + }); + + it('should throw error if child not found', async () => { + mockChildRepository.findOne.mockResolvedValue(null); + + await expect( + service.generateWeeklyReport('child_123'), + ).rejects.toThrow('Child not found'); + }); + + it('should include patterns and predictions', async () => { + mockChildRepository.findOne.mockResolvedValue(mockChild); + mockActivityRepository.find.mockResolvedValue(mockActivities); + + const result = await service.generateWeeklyReport('child_123'); + + expect(patternAnalysisService.analyzePatterns).toHaveBeenCalledWith( + 'child_123', + 7, + ); + expect(predictionService.generatePredictions).toHaveBeenCalledWith( + 'child_123', + ); + expect(result.patterns).toBeDefined(); + expect(result.predictions).toBeDefined(); + }); + }); + + describe('generateMonthlyReport', () => { + it('should generate monthly report for a child', async () => { + mockChildRepository.findOne.mockResolvedValue(mockChild); + mockActivityRepository.find.mockResolvedValue(mockActivities); + + const result = await service.generateMonthlyReport('child_123'); + + expect(result).toHaveProperty('childId', 'child_123'); + expect(result).toHaveProperty('childName', 'Baby Jane'); + expect(result).toHaveProperty('month'); + expect(result).toHaveProperty('summary'); + expect(result).toHaveProperty('trends'); + expect(result).toHaveProperty('weeklyBreakdown'); + expect(result.month).toMatch(/\d{4}-\d{2}/); + }); + + it('should use custom month when provided', async () => { + const customMonth = new Date('2024-06-15'); + mockChildRepository.findOne.mockResolvedValue(mockChild); + mockActivityRepository.find.mockResolvedValue(mockActivities); + + const result = await service.generateMonthlyReport( + 'child_123', + customMonth, + ); + + expect(result.month).toBe('2024-06'); + }); + + it('should throw error if child not found', async () => { + mockChildRepository.findOne.mockResolvedValue(null); + + await expect( + service.generateMonthlyReport('child_123'), + ).rejects.toThrow('Child not found'); + }); + }); + + describe('exportData', () => { + it('should export data in JSON format', async () => { + mockChildRepository.findOne.mockResolvedValue(mockChild); + mockActivityRepository.find.mockResolvedValue(mockActivities); + + const result = await service.exportData('child_123', null, null, 'json'); + + expect(result.format).toBe('json'); + expect(result.data).toEqual(mockActivities); + expect(result.contentType).toBe('application/json'); + expect(result.generatedAt).toBeInstanceOf(Date); + }); + + it('should export data in CSV format', async () => { + mockChildRepository.findOne.mockResolvedValue(mockChild); + mockActivityRepository.find.mockResolvedValue(mockActivities); + + const result = await service.exportData('child_123', null, null, 'csv'); + + expect(result.format).toBe('csv'); + expect(result.contentType).toBe('text/csv'); + expect(typeof result.data).toBe('string'); + }); + + it('should use custom date range', async () => { + const start = new Date('2025-01-01'); + const end = new Date('2025-01-31'); + mockChildRepository.findOne.mockResolvedValue(mockChild); + mockActivityRepository.find.mockResolvedValue(mockActivities); + + await service.exportData('child_123', start, end, 'json'); + + expect(activityRepository.find).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ + childId: 'child_123', + startedAt: expect.any(Object), + }), + }), + ); + }); + + it('should throw error if child not found', async () => { + mockChildRepository.findOne.mockResolvedValue(null); + + await expect( + service.exportData('child_123', null, null, 'json'), + ).rejects.toThrow('Child not found'); + }); + }); + + describe('calculateWeeklySummary', () => { + it('should calculate weekly summary statistics', () => { + const activities = [ + { + type: ActivityType.SLEEP, + startedAt: new Date('2025-01-01T20:00:00'), + endedAt: new Date('2025-01-02T06:00:00'), // 10 hours + }, + { + type: ActivityType.SLEEP, + startedAt: new Date('2025-01-02T13:00:00'), + endedAt: new Date('2025-01-02T15:00:00'), // 2 hours nap + }, + { + type: ActivityType.FEEDING, + startedAt: new Date('2025-01-01T08:00:00'), + }, + { + type: ActivityType.FEEDING, + startedAt: new Date('2025-01-01T11:00:00'), + }, + { + type: ActivityType.DIAPER, + startedAt: new Date('2025-01-01T09:00:00'), + metadata: { type: 'wet' }, + }, + ]; + + const result = (service as any).calculateWeeklySummary(activities); + + expect(result.totalSleep).toBeGreaterThan(0); + expect(result.totalFeedings).toBe(2); + expect(result.totalDiaperChanges).toBe(1); + expect(result.averageSleepPerNight).toBeGreaterThan(0); + expect(result.averageNapsPerDay).toBeGreaterThanOrEqual(0); + }); + }); + + describe('calculateMonthlySummary', () => { + it('should calculate monthly summary statistics', () => { + const activities = [ + { + type: ActivityType.SLEEP, + startedAt: new Date('2025-01-01T20:00:00'), + endedAt: new Date('2025-01-02T06:00:00'), + }, + { + type: ActivityType.FEEDING, + startedAt: new Date('2025-01-15T08:00:00'), + }, + { + type: ActivityType.DIAPER, + startedAt: new Date('2025-01-20T09:00:00'), + metadata: { type: 'wet' }, + }, + ]; + + const result = (service as any).calculateMonthlySummary(activities); + + expect(result.totalSleep).toBeGreaterThan(0); + expect(result.totalFeedings).toBe(1); + expect(result.totalDiaperChanges).toBe(1); + expect(result.weeklyAverages).toHaveProperty('sleep'); + expect(result.weeklyAverages).toHaveProperty('feedings'); + expect(result.weeklyAverages).toHaveProperty('diapers'); + }); + }); + + describe('analyzeTrends', () => { + it('should detect improving sleep trend', () => { + const activities = [ + { + type: ActivityType.SLEEP, + startedAt: new Date('2025-01-01T20:00:00'), + endedAt: new Date('2025-01-02T04:00:00'), // 8 hours + }, + { + type: ActivityType.SLEEP, + startedAt: new Date('2025-01-15T20:00:00'), + endedAt: new Date('2025-01-16T07:00:00'), // 11 hours + }, + ]; + + const result = (service as any).analyzeTrends(activities); + + expect(result.sleepTrend).toBe('improving'); + }); + + it('should detect declining sleep trend', () => { + const activities = [ + { + type: ActivityType.SLEEP, + startedAt: new Date('2025-01-01T20:00:00'), + endedAt: new Date('2025-01-02T07:00:00'), // 11 hours + }, + { + type: ActivityType.SLEEP, + startedAt: new Date('2025-01-15T20:00:00'), + endedAt: new Date('2025-01-16T04:00:00'), // 8 hours + }, + ]; + + const result = (service as any).analyzeTrends(activities); + + expect(result.sleepTrend).toBe('declining'); + }); + + it('should detect increasing feeding trend', () => { + const activities = [ + { type: ActivityType.FEEDING, startedAt: new Date('2025-01-01T08:00:00') }, + { type: ActivityType.FEEDING, startedAt: new Date('2025-01-15T08:00:00') }, + { type: ActivityType.FEEDING, startedAt: new Date('2025-01-16T08:00:00') }, + { type: ActivityType.FEEDING, startedAt: new Date('2025-01-17T08:00:00') }, + { type: ActivityType.FEEDING, startedAt: new Date('2025-01-18T08:00:00') }, + ]; + + const result = (service as any).analyzeTrends(activities); + + expect(result.feedingTrend).toBe('increasing'); + }); + + it('should count milestone activities', () => { + const activities = [ + { type: ActivityType.MILESTONE, startedAt: new Date('2025-01-01') }, + { type: ActivityType.MILESTONE, startedAt: new Date('2025-01-15') }, + ]; + + const result = (service as any).analyzeTrends(activities); + + expect(result.growthMilestones).toBe(2); + }); + }); + + describe('convertToCSV', () => { + it('should convert activities to CSV format', () => { + const activities = [ + { + id: '1', + type: ActivityType.FEEDING, + startedAt: new Date('2025-01-01T08:00:00'), + endedAt: null, + metadata: { method: 'bottle', amount: 120 }, + }, + ]; + + const csv = (service as any).convertToCSV(activities); + + expect(typeof csv).toBe('string'); + expect(csv).toContain('id,type,startedAt'); + expect(csv).toContain('FEEDING'); + }); + + it('should handle empty activities array', () => { + const csv = (service as any).convertToCSV([]); + + expect(typeof csv).toBe('string'); + expect(csv).toContain('id,type,startedAt'); + }); + }); + + describe('generateHighlights', () => { + it('should generate highlights from summary and patterns', () => { + const summary = { + totalSleep: 5000, + totalFeedings: 56, + totalDiaperChanges: 42, + averageSleepPerNight: 600, + averageNapsPerDay: 2.5, + }; + + const patterns = { + sleep: { averageDuration: 600, trend: 'improving' }, + feeding: { averageInterval: 3, trend: 'stable' }, + diaper: { wetDiapersPerDay: 6, isHealthy: true }, + recommendations: [], + concernsDetected: [], + }; + + const highlights = (service as any).generateHighlights(summary, patterns); + + expect(Array.isArray(highlights)).toBe(true); + expect(highlights.length).toBeGreaterThan(0); + }); + }); +});