From 5c9b2b9d8f37f0afab1f8bc27aaba23dc8aaf924 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 21 Nov 2025 14:56:16 +0100 Subject: [PATCH] test(JiraSync): enhance test coverage for change detection logic and preserved fields --- .../analytics/__tests__/analytics.test.ts | 278 ++++++++++ .../__tests__/deadline-analytics.test.ts | 380 +++++++++++++ .../__tests__/manager-summary.test.ts | 314 +++++++++++ .../analytics/__tests__/metrics.test.ts | 234 ++++++++ .../analytics/__tests__/tag-analytics.test.ts | 502 ++++++++++++++++++ 5 files changed, 1708 insertions(+) create mode 100644 src/services/analytics/__tests__/analytics.test.ts create mode 100644 src/services/analytics/__tests__/deadline-analytics.test.ts create mode 100644 src/services/analytics/__tests__/manager-summary.test.ts create mode 100644 src/services/analytics/__tests__/metrics.test.ts create mode 100644 src/services/analytics/__tests__/tag-analytics.test.ts diff --git a/src/services/analytics/__tests__/analytics.test.ts b/src/services/analytics/__tests__/analytics.test.ts new file mode 100644 index 0000000..198450e --- /dev/null +++ b/src/services/analytics/__tests__/analytics.test.ts @@ -0,0 +1,278 @@ +/** + * Tests unitaires pour AnalyticsService + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { AnalyticsService } from '../analytics'; +import { TaskStatus, TaskPriority, TaskSource } from '@/lib/types'; +import { prisma } from '@/services/core/database'; + +// Mock de prisma +vi.mock('@/services/core/database', () => ({ + prisma: { + task: { + findMany: vi.fn(), + }, + }, +})); + +describe('AnalyticsService', () => { + const userId = 'test-user-id'; + const mockDate = new Date('2024-01-15T12:00:00Z'); + + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + vi.setSystemTime(mockDate); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe('getProductivityMetrics', () => { + it('devrait calculer les métriques de productivité avec des tâches', async () => { + const mockTasks = [ + { + id: '1', + title: 'Tâche 1', + description: 'Description 1', + status: 'done' as TaskStatus, + priority: 'high' as TaskPriority, + source: 'manual' as TaskSource, + sourceId: null, + ownerId: userId, + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-12'), + completedAt: new Date('2024-01-12'), + dueDate: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + assignee: null, + taskTags: [], + }, + { + id: '2', + title: 'Tâche 2', + description: 'Description 2', + status: 'todo' as TaskStatus, + priority: 'medium' as TaskPriority, + source: 'jira' as TaskSource, + sourceId: 'JIRA-123', + ownerId: userId, + createdAt: new Date('2024-01-13'), + updatedAt: new Date('2024-01-13'), + completedAt: null, + dueDate: null, + jiraProject: 'PROJ', + jiraKey: 'JIRA-123', + jiraType: 'Task', + assignee: null, + taskTags: [], + }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + + const result = await AnalyticsService.getProductivityMetrics(userId); + + expect(result).toHaveProperty('completionTrend'); + expect(result).toHaveProperty('velocityData'); + expect(result).toHaveProperty('priorityDistribution'); + expect(result).toHaveProperty('statusFlow'); + expect(result).toHaveProperty('weeklyStats'); + + expect(result.completionTrend.length).toBeGreaterThan(0); + expect(result.priorityDistribution.length).toBeGreaterThan(0); + expect(result.statusFlow.length).toBeGreaterThan(0); + }); + + it('devrait gérer une liste vide de tâches', async () => { + vi.mocked(prisma.task.findMany).mockResolvedValue([]); + + const result = await AnalyticsService.getProductivityMetrics(userId); + + expect(result.completionTrend.length).toBeGreaterThan(0); + expect(result.velocityData).toEqual([]); + expect(result.priorityDistribution).toEqual([]); + expect(result.statusFlow).toEqual([]); + }); + + it('devrait filtrer par sources si spécifié', async () => { + const mockTasks = [ + { + id: '1', + title: 'Tâche Jira', + status: 'done' as TaskStatus, + priority: 'high' as TaskPriority, + source: 'jira' as TaskSource, + sourceId: 'JIRA-1', + ownerId: userId, + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-10'), + completedAt: new Date('2024-01-10'), + dueDate: null, + description: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + assignee: null, + taskTags: [], + }, + { + id: '2', + title: 'Tâche Manual', + status: 'todo' as TaskStatus, + priority: 'medium' as TaskPriority, + source: 'manual' as TaskSource, + sourceId: null, + ownerId: userId, + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-10'), + completedAt: null, + dueDate: null, + description: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + assignee: null, + taskTags: [], + }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + + const result = await AnalyticsService.getProductivityMetrics( + userId, + undefined, + ['jira'] + ); + + // Vérifier que seules les tâches Jira sont incluses dans les calculs + const allStatuses = result.statusFlow.map((s) => s.status); + expect(allStatuses).toContain('Terminé'); // Seule la tâche Jira est done + }); + + it('devrait utiliser une période personnalisée si fournie', async () => { + const startDate = new Date('2024-01-01'); + const endDate = new Date('2024-01-10'); + + vi.mocked(prisma.task.findMany).mockResolvedValue([]); + + await AnalyticsService.getProductivityMetrics(userId, { + start: startDate, + end: endDate, + }); + + expect(prisma.task.findMany).toHaveBeenCalled(); + }); + + it('devrait calculer correctement la tendance de completion', async () => { + const mockTasks = [ + { + id: '1', + title: 'Tâche complétée', + status: 'done' as TaskStatus, + priority: 'high' as TaskPriority, + source: 'manual' as TaskSource, + sourceId: null, + ownerId: userId, + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-12'), + completedAt: new Date('2024-01-12'), + dueDate: null, + description: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + assignee: null, + taskTags: [], + }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + + const result = await AnalyticsService.getProductivityMetrics(userId); + + const completionDay = result.completionTrend.find( + (day) => day.date === '2024-01-12' + ); + expect(completionDay).toBeDefined(); + expect(completionDay?.completed).toBe(1); + }); + + it('devrait calculer correctement la distribution des priorités', async () => { + const mockTasks = [ + { + id: '1', + title: 'Tâche haute priorité', + status: 'todo' as TaskStatus, + priority: 'high' as TaskPriority, + source: 'manual' as TaskSource, + sourceId: null, + ownerId: userId, + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-10'), + completedAt: null, + dueDate: null, + description: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + assignee: null, + taskTags: [], + }, + { + id: '2', + title: 'Tâche moyenne priorité', + status: 'todo' as TaskStatus, + priority: 'medium' as TaskPriority, + source: 'manual' as TaskSource, + sourceId: null, + ownerId: userId, + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-10'), + completedAt: null, + dueDate: null, + description: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + assignee: null, + taskTags: [], + }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + + const result = await AnalyticsService.getProductivityMetrics(userId); + + const highPriority = result.priorityDistribution.find( + (p) => p.priority === 'Élevée' + ); + const mediumPriority = result.priorityDistribution.find( + (p) => p.priority === 'Moyenne' + ); + + expect(highPriority).toBeDefined(); + expect(highPriority?.count).toBe(1); + expect(highPriority?.percentage).toBe(50); + + expect(mediumPriority).toBeDefined(); + expect(mediumPriority?.count).toBe(1); + expect(mediumPriority?.percentage).toBe(50); + }); + + it('devrait gérer les erreurs de base de données', async () => { + vi.mocked(prisma.task.findMany).mockRejectedValue( + new Error('Database error') + ); + + await expect( + AnalyticsService.getProductivityMetrics(userId) + ).rejects.toThrow('Impossible de calculer les métriques de productivité'); + }); + }); +}); diff --git a/src/services/analytics/__tests__/deadline-analytics.test.ts b/src/services/analytics/__tests__/deadline-analytics.test.ts new file mode 100644 index 0000000..f1e578b --- /dev/null +++ b/src/services/analytics/__tests__/deadline-analytics.test.ts @@ -0,0 +1,380 @@ +/** + * Tests unitaires pour DeadlineAnalyticsService + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { DeadlineAnalyticsService } from '../deadline-analytics'; +import { TaskStatus } from '@/lib/types'; +import { prisma } from '@/services/core/database'; + +// Mock de prisma +vi.mock('@/services/core/database', () => ({ + prisma: { + task: { + findMany: vi.fn(), + }, + }, +})); + +describe('DeadlineAnalyticsService', () => { + const userId = 'test-user-id'; + const mockDate = new Date('2024-01-15T12:00:00Z'); + + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + vi.setSystemTime(mockDate); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe('getDeadlineMetrics', () => { + it('devrait catégoriser les tâches par urgence', async () => { + const overdueDate = new Date('2024-01-10'); // 5 jours en retard + const criticalDate = new Date('2024-01-16'); // 1 jour restant + const warningDate = new Date('2024-01-20'); // 5 jours restants + const upcomingDate = new Date('2024-01-25'); // 10 jours restants + + const mockTasks = [ + { + id: '1', + title: 'Tâche en retard', + status: 'todo' as TaskStatus, + priority: 'high', + dueDate: overdueDate, + source: 'manual', + ownerId: userId, + taskTags: [], + }, + { + id: '2', + title: 'Tâche critique', + status: 'in_progress' as TaskStatus, + priority: 'urgent', + dueDate: criticalDate, + source: 'jira', + ownerId: userId, + taskTags: [], + }, + { + id: '3', + title: 'Tâche warning', + status: 'todo' as TaskStatus, + priority: 'medium', + dueDate: warningDate, + source: 'manual', + ownerId: userId, + taskTags: [], + }, + { + id: '4', + title: 'Tâche à venir', + status: 'todo' as TaskStatus, + priority: 'low', + dueDate: upcomingDate, + source: 'manual', + ownerId: userId, + taskTags: [], + }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + + const result = await DeadlineAnalyticsService.getDeadlineMetrics(userId); + + expect(result.overdue).toHaveLength(1); + expect(result.overdue[0].id).toBe('1'); + expect(result.critical).toHaveLength(1); + expect(result.critical[0].id).toBe('2'); + expect(result.warning).toHaveLength(1); + expect(result.warning[0].id).toBe('3'); + expect(result.upcoming).toHaveLength(1); + expect(result.upcoming[0].id).toBe('4'); + + expect(result.summary.overdueCount).toBe(1); + expect(result.summary.criticalCount).toBe(1); + expect(result.summary.warningCount).toBe(1); + expect(result.summary.upcomingCount).toBe(1); + expect(result.summary.totalWithDeadlines).toBe(4); + }); + + it('devrait exclure les tâches terminées ou annulées', async () => { + // Le service filtre déjà les tâches terminées dans la requête Prisma + // donc le mock devrait retourner une liste vide + vi.mocked(prisma.task.findMany).mockResolvedValue([]); + + const result = await DeadlineAnalyticsService.getDeadlineMetrics(userId); + + expect(result.summary.totalWithDeadlines).toBe(0); + }); + + it('devrait filtrer par sources si spécifié', async () => { + const mockTasks = [ + { + id: '1', + title: 'Tâche Jira', + status: 'todo' as TaskStatus, + priority: 'high', + dueDate: new Date('2024-01-16'), + source: 'jira', + ownerId: userId, + taskTags: [], + }, + { + id: '2', + title: 'Tâche Manual', + status: 'todo' as TaskStatus, + priority: 'medium', + dueDate: new Date('2024-01-17'), + source: 'manual', + ownerId: userId, + taskTags: [], + }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + + const result = await DeadlineAnalyticsService.getDeadlineMetrics(userId, [ + 'jira', + ]); + + const allTaskIds = [ + ...result.overdue, + ...result.critical, + ...result.warning, + ...result.upcoming, + ].map((t) => t.id); + + expect(allTaskIds).toContain('1'); + expect(allTaskIds).not.toContain('2'); + }); + + it('devrait calculer correctement les jours restants', async () => { + // Utiliser une date normalisée pour éviter les problèmes de timezone + // La date mockée est le 15 janvier à 12:00, donc le 20 janvier à 00:00 + // donne environ 4-5 jours selon le calcul avec Math.ceil + const dueDate = new Date('2024-01-20T00:00:00Z'); // 5 jours dans le futur + const mockTasks = [ + { + id: '1', + title: 'Tâche test', + status: 'todo' as TaskStatus, + priority: 'medium', + dueDate, + source: 'manual', + ownerId: userId, + taskTags: [], + }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + + const result = await DeadlineAnalyticsService.getDeadlineMetrics(userId); + + // Le calcul utilise Math.ceil, donc avec les heures ça peut donner 5 ou 6 jours + // Vérifions que c'est dans la plage warning (3-7 jours) + expect(result.warning[0].daysRemaining).toBeGreaterThanOrEqual(5); + expect(result.warning[0].daysRemaining).toBeLessThanOrEqual(6); + expect(result.warning[0].urgencyLevel).toBe('warning'); + }); + + it('devrait gérer les erreurs de base de données', async () => { + vi.mocked(prisma.task.findMany).mockRejectedValue( + new Error('Database error') + ); + + await expect( + DeadlineAnalyticsService.getDeadlineMetrics(userId) + ).rejects.toThrow("Impossible d'analyser les échéances"); + }); + }); + + describe('getCriticalDeadlines', () => { + it('devrait retourner les 10 tâches les plus critiques', async () => { + const mockTasks = Array.from({ length: 15 }, (_, i) => ({ + id: `${i + 1}`, + title: `Tâche ${i + 1}`, + status: 'todo' as TaskStatus, + priority: 'high', + dueDate: new Date(`2024-01-${10 + i}`), // Dates en retard + source: 'manual', + ownerId: userId, + taskTags: [], + })); + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + + const result = + await DeadlineAnalyticsService.getCriticalDeadlines(userId); + + expect(result.length).toBeLessThanOrEqual(10); + }); + }); + + describe('analyzeImpactByPriority', () => { + it("devrait analyser l'impact par priorité", () => { + const tasks = [ + { + id: '1', + title: 'Tâche haute priorité en retard', + status: 'todo' as TaskStatus, + priority: 'high', + dueDate: new Date('2024-01-10'), + daysRemaining: -5, + urgencyLevel: 'overdue' as const, + source: 'manual', + tags: [], + }, + { + id: '2', + title: 'Tâche haute priorité critique', + status: 'todo' as TaskStatus, + priority: 'high', + dueDate: new Date('2024-01-16'), + daysRemaining: 1, + urgencyLevel: 'critical' as const, + source: 'manual', + tags: [], + }, + { + id: '3', + title: 'Tâche moyenne priorité', + status: 'todo' as TaskStatus, + priority: 'medium', + dueDate: new Date('2024-01-20'), + daysRemaining: 5, + urgencyLevel: 'warning' as const, + source: 'manual', + tags: [], + }, + ]; + + const result = DeadlineAnalyticsService.analyzeImpactByPriority(tasks); + + expect(result.length).toBeGreaterThan(0); + const highPriority = result.find((p) => p.priority === 'high'); + expect(highPriority).toBeDefined(); + expect(highPriority?.overdueCount).toBe(1); + expect(highPriority?.criticalCount).toBe(1); + }); + + it('devrait trier par impact décroissant', () => { + const tasks = [ + { + id: '1', + title: 'Tâche haute priorité', + status: 'todo' as TaskStatus, + priority: 'high', + dueDate: new Date('2024-01-10'), + daysRemaining: -5, + urgencyLevel: 'overdue' as const, + source: 'manual', + tags: [], + }, + { + id: '2', + title: 'Tâche basse priorité', + status: 'todo' as TaskStatus, + priority: 'low', + dueDate: new Date('2024-01-20'), + daysRemaining: 5, + urgencyLevel: 'warning' as const, + source: 'manual', + tags: [], + }, + ]; + + const result = DeadlineAnalyticsService.analyzeImpactByPriority(tasks); + + expect(result[0].priority).toBe('high'); // Plus d'impact + }); + }); + + describe('calculateRiskMetrics', () => { + it('devrait calculer un score de risque faible', () => { + const metrics = { + overdue: [], + critical: [], + warning: [], + upcoming: [], + summary: { + overdueCount: 0, + criticalCount: 0, + warningCount: 1, + upcomingCount: 2, + totalWithDeadlines: 3, + }, + }; + + const result = DeadlineAnalyticsService.calculateRiskMetrics(metrics); + + expect(result.riskScore).toBeLessThan(25); + expect(result.riskLevel).toBe('low'); + }); + + it('devrait calculer un score de risque critique', () => { + const metrics = { + overdue: [], + critical: [], + warning: [], + upcoming: [], + summary: { + overdueCount: 3, + criticalCount: 2, + warningCount: 0, + upcomingCount: 0, + totalWithDeadlines: 5, + }, + }; + + const result = DeadlineAnalyticsService.calculateRiskMetrics(metrics); + + expect(result.riskScore).toBeGreaterThanOrEqual(75); + expect(result.riskLevel).toBe('critical'); + }); + + it('devrait limiter le score à 100', () => { + const metrics = { + overdue: [], + critical: [], + warning: [], + upcoming: [], + summary: { + overdueCount: 10, + criticalCount: 10, + warningCount: 10, + upcomingCount: 10, + totalWithDeadlines: 40, + }, + }; + + const result = DeadlineAnalyticsService.calculateRiskMetrics(metrics); + + expect(result.riskScore).toBeLessThanOrEqual(100); + }); + + it('devrait fournir des recommandations appropriées', () => { + const metrics = { + overdue: [], + critical: [], + warning: [], + upcoming: [], + summary: { + overdueCount: 2, + criticalCount: 1, + warningCount: 2, + upcomingCount: 1, + totalWithDeadlines: 6, + }, + }; + + const result = DeadlineAnalyticsService.calculateRiskMetrics(metrics); + + expect(result.recommendation).toBeDefined(); + expect(result.recommendation.length).toBeGreaterThan(0); + }); + }); +}); diff --git a/src/services/analytics/__tests__/manager-summary.test.ts b/src/services/analytics/__tests__/manager-summary.test.ts new file mode 100644 index 0000000..d78c379 --- /dev/null +++ b/src/services/analytics/__tests__/manager-summary.test.ts @@ -0,0 +1,314 @@ +/** + * Tests unitaires pour ManagerSummaryService + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { ManagerSummaryService } from '../manager-summary'; +import { prisma } from '@/services/core/database'; + +// Mock de prisma +vi.mock('@/services/core/database', () => ({ + prisma: { + task: { + findMany: vi.fn(), + }, + dailyCheckbox: { + findMany: vi.fn(), + count: vi.fn(), + }, + }, +})); + +describe('ManagerSummaryService', () => { + const userId = 'test-user-id'; + const mockDate = new Date('2024-01-15T12:00:00Z'); + + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + vi.setSystemTime(mockDate); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe('getManagerSummary', () => { + it('devrait générer un résumé pour les 7 derniers jours', async () => { + const mockTasks = [ + { + id: '1', + title: 'Tâche complétée', + description: 'Description', + priority: 'high', + completedAt: new Date('2024-01-12'), + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-12'), + taskTags: [], + }, + ]; + + const mockCheckboxes = [ + { + id: '1', + text: 'Todo complété', + isChecked: true, + type: 'task', + date: new Date('2024-01-13'), + createdAt: new Date('2024-01-13'), + task: null, + }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + vi.mocked(prisma.dailyCheckbox.findMany).mockResolvedValue( + mockCheckboxes as any + ); + vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0); + + const result = await ManagerSummaryService.getManagerSummary(userId); + + expect(result).toHaveProperty('period'); + expect(result).toHaveProperty('keyAccomplishments'); + expect(result).toHaveProperty('upcomingChallenges'); + expect(result).toHaveProperty('metrics'); + expect(result).toHaveProperty('narrative'); + + expect(result.period.start).toBeInstanceOf(Date); + expect(result.period.end).toBeInstanceOf(Date); + expect(result.keyAccomplishments.length).toBeGreaterThan(0); + }); + + it('devrait extraire les accomplissements clés des tâches', async () => { + const mockTasks = [ + { + id: '1', + title: 'Tâche haute priorité', + description: 'Description importante', + priority: 'high', + completedAt: new Date('2024-01-12'), + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-12'), + taskTags: [ + { + tag: { + name: 'important', + }, + }, + ], + }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + vi.mocked(prisma.dailyCheckbox.findMany).mockResolvedValue([]); + vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0); + + const result = await ManagerSummaryService.getManagerSummary(userId); + + const accomplishment = result.keyAccomplishments.find( + (a) => a.id === 'task-1' + ); + expect(accomplishment).toBeDefined(); + expect(accomplishment?.impact).toBe('high'); + expect(accomplishment?.tags).toContain('important'); + }); + + it('devrait inclure les todos standalone comme accomplissements', async () => { + const mockCheckboxes = [ + { + id: '1', + text: 'Réunion importante', + isChecked: true, + type: 'meeting', + date: new Date('2024-01-13'), + createdAt: new Date('2024-01-13'), + task: null, + }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue([]); + vi.mocked(prisma.dailyCheckbox.findMany).mockResolvedValue( + mockCheckboxes as any + ); + + const result = await ManagerSummaryService.getManagerSummary(userId); + + const accomplishment = result.keyAccomplishments.find( + (a) => a.id === 'todo-1' + ); + expect(accomplishment).toBeDefined(); + expect(accomplishment?.title).toContain('Réunion importante'); + expect(accomplishment?.impact).toBe('low'); + }); + + it('devrait identifier les défis à venir', async () => { + const mockUpcomingTasks = [ + { + id: '1', + title: 'Tâche à venir', + description: 'Description', + priority: 'high', + createdAt: new Date('2024-01-10'), + taskTags: [], + }, + ]; + + const mockUpcomingCheckboxes = [ + { + id: '1', + text: 'Todo à venir', + isChecked: false, + type: 'task', + date: new Date('2024-01-20'), + createdAt: new Date('2024-01-10'), + task: { + id: '1', + title: 'Tâche à venir', + priority: 'high', + taskTags: [], + }, + }, + ]; + + // Mock pour getCompletedTasks + vi.mocked(prisma.task.findMany) + .mockResolvedValueOnce([]) // Tâches complétées + .mockResolvedValueOnce(mockUpcomingTasks as any); // Tâches à venir + + vi.mocked(prisma.dailyCheckbox.findMany) + .mockResolvedValueOnce([]) // Checkboxes complétées + .mockResolvedValueOnce(mockUpcomingCheckboxes as any); // Checkboxes à venir + + vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0); + + const result = await ManagerSummaryService.getManagerSummary(userId); + + expect(result.upcomingChallenges.length).toBeGreaterThan(0); + const challenge = result.upcomingChallenges.find( + (c) => c.id === 'task-1' + ); + expect(challenge).toBeDefined(); + expect(challenge?.priority).toBe('high'); + }); + + it('devrait calculer les métriques correctement', async () => { + const mockTasks = [ + { + id: '1', + title: 'Tâche haute priorité', + description: 'Description', + priority: 'high', + completedAt: new Date('2024-01-12'), + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-12'), + taskTags: [], + }, + ]; + + const mockCheckboxes = [ + { + id: '1', + text: 'Réunion', + isChecked: true, + type: 'meeting', + date: new Date('2024-01-13'), + createdAt: new Date('2024-01-13'), + task: null, + }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + vi.mocked(prisma.dailyCheckbox.findMany).mockResolvedValue( + mockCheckboxes as any + ); + vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0); + + const result = await ManagerSummaryService.getManagerSummary(userId); + + expect(result.metrics.totalTasksCompleted).toBe(1); + expect(result.metrics.totalCheckboxesCompleted).toBe(1); + expect(result.metrics.highPriorityTasksCompleted).toBe(1); + expect(result.metrics.meetingCheckboxesCompleted).toBe(1); + }); + + it('devrait générer un narratif', async () => { + const mockTasks = [ + { + id: '1', + title: 'Tâche importante', + description: 'Description', + priority: 'high', + completedAt: new Date('2024-01-12'), + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-12'), + taskTags: [ + { + tag: { + name: 'important', + }, + }, + ], + }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + vi.mocked(prisma.dailyCheckbox.findMany).mockResolvedValue([]); + vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0); + + const result = await ManagerSummaryService.getManagerSummary(userId); + + expect(result.narrative).toHaveProperty('weekHighlight'); + expect(result.narrative).toHaveProperty('mainChallenges'); + expect(result.narrative).toHaveProperty('nextWeekFocus'); + expect(result.narrative.weekHighlight.length).toBeGreaterThan(0); + }); + + it('devrait utiliser une date personnalisée si fournie', async () => { + const customDate = new Date('2024-02-01'); + + vi.mocked(prisma.task.findMany).mockResolvedValue([]); + vi.mocked(prisma.dailyCheckbox.findMany).mockResolvedValue([]); + + const result = await ManagerSummaryService.getManagerSummary( + userId, + customDate + ); + + expect(result.period.end).toEqual(customDate); + }); + + it('devrait trier les accomplissements par impact', async () => { + const mockTasks = [ + { + id: '1', + title: 'Tâche haute priorité', + description: 'Description', + priority: 'high', + completedAt: new Date('2024-01-12'), + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-12'), + taskTags: [], + }, + { + id: '2', + title: 'Tâche basse priorité', + description: 'Description', + priority: 'low', + completedAt: new Date('2024-01-13'), + createdAt: new Date('2024-01-11'), + updatedAt: new Date('2024-01-13'), + taskTags: [], + }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + vi.mocked(prisma.dailyCheckbox.findMany).mockResolvedValue([]); + vi.mocked(prisma.dailyCheckbox.count).mockResolvedValue(0); + + const result = await ManagerSummaryService.getManagerSummary(userId); + + expect(result.keyAccomplishments[0].impact).toBe('high'); + }); + }); +}); diff --git a/src/services/analytics/__tests__/metrics.test.ts b/src/services/analytics/__tests__/metrics.test.ts new file mode 100644 index 0000000..8b41708 --- /dev/null +++ b/src/services/analytics/__tests__/metrics.test.ts @@ -0,0 +1,234 @@ +/** + * Tests unitaires pour MetricsService + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { MetricsService } from '../metrics'; +import { prisma } from '@/services/core/database'; + +// Mock de prisma +vi.mock('@/services/core/database', () => ({ + prisma: { + task: { + count: vi.fn(), + groupBy: vi.fn(), + }, + }, +})); + +describe('MetricsService', () => { + const userId = 'test-user-id'; + const mockDate = new Date('2024-01-15T12:00:00Z'); + + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + vi.setSystemTime(mockDate); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe('getWeeklyMetrics', () => { + it('devrait retourner les métriques hebdomadaires', async () => { + // Mock des comptages pour chaque jour + vi.mocked(prisma.task.count) + .mockResolvedValueOnce(5) // completed + .mockResolvedValueOnce(3) // inProgress + .mockResolvedValueOnce(1) // blocked + .mockResolvedValueOnce(2) // pending + .mockResolvedValueOnce(2) // newTasks + .mockResolvedValueOnce(10) // totalTasks + .mockResolvedValueOnce(5) // completed jour 2 + .mockResolvedValueOnce(3) // inProgress jour 2 + .mockResolvedValueOnce(1) // blocked jour 2 + .mockResolvedValueOnce(2) // pending jour 2 + .mockResolvedValueOnce(2) // newTasks jour 2 + .mockResolvedValueOnce(10); // totalTasks jour 2 + + // Mock pour les autres jours (répéter pour 7 jours) + for (let i = 0; i < 5; i++) { + vi.mocked(prisma.task.count) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0); + } + + // Mock pour statusDistribution + vi.mocked(prisma.task.groupBy).mockResolvedValueOnce([ + { + status: 'done', + _count: { status: 5 }, + }, + { + status: 'in_progress', + _count: { status: 3 }, + }, + ] as any); + + // Mock pour priorityBreakdown (3 priorités × 1 count each) + vi.mocked(prisma.task.count) + .mockResolvedValueOnce(2) // high completed + .mockResolvedValueOnce(1) // high inProgress + .mockResolvedValueOnce(2) // medium completed + .mockResolvedValueOnce(1) // medium inProgress + .mockResolvedValueOnce(1) // low completed + .mockResolvedValueOnce(1); // low inProgress + + const result = await MetricsService.getWeeklyMetrics(userId); + + expect(result).toHaveProperty('period'); + expect(result).toHaveProperty('dailyBreakdown'); + expect(result).toHaveProperty('summary'); + expect(result).toHaveProperty('statusDistribution'); + expect(result).toHaveProperty('priorityBreakdown'); + + expect(result.dailyBreakdown).toHaveLength(7); + expect(result.summary).toHaveProperty('totalTasksCompleted'); + expect(result.summary).toHaveProperty('totalTasksCreated'); + expect(result.summary).toHaveProperty('averageCompletionRate'); + }); + + it('devrait calculer correctement le taux de completion', async () => { + // Mock pour un jour avec des tâches complétées + vi.mocked(prisma.task.count) + .mockResolvedValueOnce(5) // completed + .mockResolvedValueOnce(0) // inProgress + .mockResolvedValueOnce(0) // blocked + .mockResolvedValueOnce(0) // pending + .mockResolvedValueOnce(0) // newTasks + .mockResolvedValueOnce(10); // totalTasks + + // Mock pour les autres jours + for (let i = 0; i < 6; i++) { + vi.mocked(prisma.task.count) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0); + } + + vi.mocked(prisma.task.groupBy).mockResolvedValueOnce([] as any); + + // Mock priorityBreakdown + vi.mocked(prisma.task.count) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0); + + const result = await MetricsService.getWeeklyMetrics(userId); + + expect(result.dailyBreakdown[0].completionRate).toBe(50); + }); + + it('devrait identifier le jour de pic de productivité', async () => { + // Jour 1: 10 complétées + vi.mocked(prisma.task.count) + .mockResolvedValueOnce(10) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(10); + + // Jour 2-7: 0 complétées + for (let i = 0; i < 6; i++) { + vi.mocked(prisma.task.count) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0); + } + + vi.mocked(prisma.task.groupBy).mockResolvedValueOnce([] as any); + + // Mock priorityBreakdown + vi.mocked(prisma.task.count) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0) + .mockResolvedValueOnce(0); + + const result = await MetricsService.getWeeklyMetrics(userId); + + expect(result.summary.totalTasksCompleted).toBe(10); + expect(result.summary.peakProductivityDay).toBeDefined(); + }); + + it('devrait utiliser une date personnalisée si fournie', async () => { + const customDate = new Date('2024-02-01'); + + // Mock minimal pour éviter les erreurs + vi.mocked(prisma.task.count).mockResolvedValue(0); + vi.mocked(prisma.task.groupBy).mockResolvedValue([]); + + await MetricsService.getWeeklyMetrics(userId, customDate); + + expect(prisma.task.count).toHaveBeenCalled(); + }); + }); + + describe('getVelocityTrends', () => { + it('devrait retourner les tendances de vélocité', async () => { + vi.mocked(prisma.task.count) + .mockResolvedValueOnce(5) // completed semaine 1 + .mockResolvedValueOnce(3) // created semaine 1 + .mockResolvedValueOnce(7) // completed semaine 2 + .mockResolvedValueOnce(4) // created semaine 2 + .mockResolvedValueOnce(6) // completed semaine 3 + .mockResolvedValueOnce(5) // created semaine 3 + .mockResolvedValueOnce(8) // completed semaine 4 + .mockResolvedValueOnce(6); // created semaine 4 + + const result = await MetricsService.getVelocityTrends(userId, 4); + + expect(result).toHaveLength(4); + expect(result[0]).toHaveProperty('date'); + expect(result[0]).toHaveProperty('completed'); + expect(result[0]).toHaveProperty('created'); + expect(result[0]).toHaveProperty('velocity'); + }); + + it('devrait calculer correctement la vélocité', async () => { + vi.mocked(prisma.task.count) + .mockResolvedValueOnce(10) // completed + .mockResolvedValueOnce(5); // created + + const result = await MetricsService.getVelocityTrends(userId, 1); + + expect(result[0].velocity).toBe(200); // 10/5 * 100 + }); + + it("devrait gérer le cas où aucune tâche n'est créée", async () => { + vi.mocked(prisma.task.count) + .mockResolvedValueOnce(5) // completed + .mockResolvedValueOnce(0); // created + + const result = await MetricsService.getVelocityTrends(userId, 1); + + expect(result[0].velocity).toBe(0); + }); + + it('devrait utiliser le nombre de semaines personnalisé', async () => { + vi.mocked(prisma.task.count).mockResolvedValue(0); + + const result = await MetricsService.getVelocityTrends(userId, 8); + + expect(result).toHaveLength(8); + }); + }); +}); diff --git a/src/services/analytics/__tests__/tag-analytics.test.ts b/src/services/analytics/__tests__/tag-analytics.test.ts new file mode 100644 index 0000000..cd6fbd4 --- /dev/null +++ b/src/services/analytics/__tests__/tag-analytics.test.ts @@ -0,0 +1,502 @@ +/** + * Tests unitaires pour TagAnalyticsService + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { TagAnalyticsService } from '../tag-analytics'; +import { TaskStatus, TaskPriority, TaskSource } from '@/lib/types'; +import { prisma } from '@/services/core/database'; + +// Mock de prisma +vi.mock('@/services/core/database', () => ({ + prisma: { + task: { + findMany: vi.fn(), + }, + tag: { + findMany: vi.fn(), + }, + }, +})); + +describe('TagAnalyticsService', () => { + const userId = 'test-user-id'; + const mockDate = new Date('2024-01-15T12:00:00Z'); + + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + vi.setSystemTime(mockDate); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe('getTagDistributionMetrics', () => { + it('devrait calculer la distribution des tags', async () => { + const mockTasks = [ + { + id: '1', + title: 'Tâche avec tag', + description: 'Description', + status: 'done' as TaskStatus, + priority: 'high' as TaskPriority, + source: 'manual' as TaskSource, + sourceId: null, + ownerId: userId, + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-12'), + completedAt: new Date('2024-01-12'), + dueDate: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + assignee: null, + taskTags: [ + { + tag: { + name: 'important', + color: '#ff0000', + }, + }, + ], + }, + { + id: '2', + title: 'Autre tâche', + description: 'Description', + status: 'todo' as TaskStatus, + priority: 'medium' as TaskPriority, + source: 'manual' as TaskSource, + sourceId: null, + ownerId: userId, + createdAt: new Date('2024-01-11'), + updatedAt: new Date('2024-01-11'), + completedAt: null, + dueDate: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + assignee: null, + taskTags: [ + { + tag: { + name: 'important', + color: '#ff0000', + }, + }, + { + tag: { + name: 'urgent', + color: '#00ff00', + }, + }, + ], + }, + ]; + + const mockTags = [ + { name: 'important', color: '#ff0000' }, + { name: 'urgent', color: '#00ff00' }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + vi.mocked(prisma.tag.findMany).mockResolvedValue(mockTags as any); + + const result = + await TagAnalyticsService.getTagDistributionMetrics(userId); + + expect(result).toHaveProperty('tagDistribution'); + expect(result).toHaveProperty('topTags'); + expect(result).toHaveProperty('tagTrends'); + expect(result).toHaveProperty('tagStats'); + + expect(result.tagDistribution.length).toBeGreaterThan(0); + const importantTag = result.tagDistribution.find( + (t) => t.tagName === 'important' + ); + expect(importantTag).toBeDefined(); + expect(importantTag?.count).toBe(2); + expect(importantTag?.percentage).toBe(100); // 2 tâches sur 2 + }); + + it('devrait calculer les top tags avec métriques', async () => { + const mockTasks = [ + { + id: '1', + title: 'Tâche complétée', + status: 'done' as TaskStatus, + priority: 'high' as TaskPriority, + source: 'manual' as TaskSource, + sourceId: null, + ownerId: userId, + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-12'), + completedAt: new Date('2024-01-12'), + dueDate: null, + description: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + assignee: null, + taskTags: [ + { + tag: { + name: 'important', + color: '#ff0000', + }, + }, + ], + }, + { + id: '2', + title: 'Tâche non complétée', + status: 'todo' as TaskStatus, + priority: 'medium' as TaskPriority, + source: 'manual' as TaskSource, + sourceId: null, + ownerId: userId, + createdAt: new Date('2024-01-11'), + updatedAt: new Date('2024-01-11'), + completedAt: null, + dueDate: null, + description: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + assignee: null, + taskTags: [ + { + tag: { + name: 'important', + color: '#ff0000', + }, + }, + ], + }, + ]; + + const mockTags = [{ name: 'important', color: '#ff0000' }]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + vi.mocked(prisma.tag.findMany).mockResolvedValue(mockTags as any); + + const result = + await TagAnalyticsService.getTagDistributionMetrics(userId); + + const importantTag = result.topTags.find( + (t) => t.tagName === 'important' + ); + expect(importantTag).toBeDefined(); + expect(importantTag?.usage).toBe(2); + expect(importantTag?.completionRate).toBe(50); // 1 complétée sur 2 + }); + + it('devrait calculer les tendances des tags', async () => { + const mockTasks = [ + { + id: '1', + title: 'Tâche', + status: 'todo' as TaskStatus, + priority: 'medium' as TaskPriority, + source: 'manual' as TaskSource, + sourceId: null, + ownerId: userId, + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-10'), + completedAt: null, + dueDate: null, + description: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + assignee: null, + taskTags: [ + { + tag: { + name: 'important', + color: '#ff0000', + }, + }, + ], + }, + ]; + + const mockTags = [{ name: 'important', color: '#ff0000' }]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + vi.mocked(prisma.tag.findMany).mockResolvedValue(mockTags as any); + + const result = + await TagAnalyticsService.getTagDistributionMetrics(userId); + + expect(result.tagTrends.length).toBeGreaterThan(0); + const trend = result.tagTrends.find((t) => t.tagName === 'important'); + expect(trend).toBeDefined(); + expect(trend?.count).toBe(1); + }); + + it('devrait calculer les statistiques des tags', async () => { + const mockTasks = [ + { + id: '1', + title: 'Tâche 1', + status: 'todo' as TaskStatus, + priority: 'medium' as TaskPriority, + source: 'manual' as TaskSource, + sourceId: null, + ownerId: userId, + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-10'), + completedAt: null, + dueDate: null, + description: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + assignee: null, + taskTags: [ + { + tag: { + name: 'important', + color: '#ff0000', + }, + }, + ], + }, + { + id: '2', + title: 'Tâche 2', + status: 'todo' as TaskStatus, + priority: 'medium' as TaskPriority, + source: 'manual' as TaskSource, + sourceId: null, + ownerId: userId, + createdAt: new Date('2024-01-11'), + updatedAt: new Date('2024-01-11'), + completedAt: null, + dueDate: null, + description: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + assignee: null, + taskTags: [ + { + tag: { + name: 'important', + color: '#ff0000', + }, + }, + { + tag: { + name: 'urgent', + color: '#00ff00', + }, + }, + ], + }, + ]; + + const mockTags = [ + { name: 'important', color: '#ff0000' }, + { name: 'urgent', color: '#00ff00' }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + vi.mocked(prisma.tag.findMany).mockResolvedValue(mockTags as any); + + const result = + await TagAnalyticsService.getTagDistributionMetrics(userId); + + expect(result.tagStats.totalTags).toBe(2); + expect(result.tagStats.activeTags).toBe(2); + expect(result.tagStats.mostUsedTag).toBe('important'); + expect(result.tagStats.avgTasksPerTag).toBeGreaterThan(0); + }); + + it('devrait filtrer par sources si spécifié', async () => { + const mockTasks = [ + { + id: '1', + title: 'Tâche Jira', + status: 'todo' as TaskStatus, + priority: 'medium' as TaskPriority, + source: 'jira' as TaskSource, + sourceId: 'JIRA-1', + ownerId: userId, + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-10'), + completedAt: null, + dueDate: null, + description: null, + jiraProject: 'PROJ', + jiraKey: 'JIRA-1', + jiraType: 'Task', + assignee: null, + taskTags: [ + { + tag: { + name: 'important', + color: '#ff0000', + }, + }, + ], + }, + { + id: '2', + title: 'Tâche Manual', + status: 'todo' as TaskStatus, + priority: 'medium' as TaskPriority, + source: 'manual' as TaskSource, + sourceId: null, + ownerId: userId, + createdAt: new Date('2024-01-11'), + updatedAt: new Date('2024-01-11'), + completedAt: null, + dueDate: null, + description: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + assignee: null, + taskTags: [ + { + tag: { + name: 'urgent', + color: '#00ff00', + }, + }, + ], + }, + ]; + + const mockTags = [ + { name: 'important', color: '#ff0000' }, + { name: 'urgent', color: '#00ff00' }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + vi.mocked(prisma.tag.findMany).mockResolvedValue(mockTags as any); + + const result = await TagAnalyticsService.getTagDistributionMetrics( + userId, + undefined, + ['jira'] + ); + + const tagNames = result.tagDistribution.map((t) => t.tagName); + expect(tagNames).toContain('important'); + expect(tagNames).not.toContain('urgent'); + }); + + it('devrait utiliser une période personnalisée si fournie', async () => { + const startDate = new Date('2024-01-01'); + const endDate = new Date('2024-01-10'); + + vi.mocked(prisma.task.findMany).mockResolvedValue([]); + vi.mocked(prisma.tag.findMany).mockResolvedValue([]); + + await TagAnalyticsService.getTagDistributionMetrics(userId, { + start: startDate, + end: endDate, + }); + + expect(prisma.task.findMany).toHaveBeenCalled(); + }); + + it('devrait gérer les erreurs de base de données', async () => { + vi.mocked(prisma.task.findMany).mockRejectedValue( + new Error('Database error') + ); + vi.mocked(prisma.tag.findMany).mockResolvedValue([]); + + await expect( + TagAnalyticsService.getTagDistributionMetrics(userId) + ).rejects.toThrow( + 'Impossible de calculer les métriques de distribution par tags' + ); + }); + + it('devrait trier les tags par utilisation décroissante', async () => { + const mockTasks = [ + { + id: '1', + title: 'Tâche', + status: 'todo' as TaskStatus, + priority: 'medium' as TaskPriority, + source: 'manual' as TaskSource, + sourceId: null, + ownerId: userId, + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-10'), + completedAt: null, + dueDate: null, + description: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + assignee: null, + taskTags: [ + { + tag: { + name: 'frequent', + color: '#0000ff', + }, + }, + ], + }, + { + id: '2', + title: 'Tâche', + status: 'todo' as TaskStatus, + priority: 'medium' as TaskPriority, + source: 'manual' as TaskSource, + sourceId: null, + ownerId: userId, + createdAt: new Date('2024-01-11'), + updatedAt: new Date('2024-01-11'), + completedAt: null, + dueDate: null, + description: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + assignee: null, + taskTags: [ + { + tag: { + name: 'frequent', + color: '#0000ff', + }, + }, + { + tag: { + name: 'rare', + color: '#ffff00', + }, + }, + ], + }, + ]; + + const mockTags = [ + { name: 'frequent', color: '#0000ff' }, + { name: 'rare', color: '#ffff00' }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + vi.mocked(prisma.tag.findMany).mockResolvedValue(mockTags as any); + + const result = + await TagAnalyticsService.getTagDistributionMetrics(userId); + + expect(result.tagDistribution[0].tagName).toBe('frequent'); + expect(result.tagDistribution[0].count).toBeGreaterThan( + result.tagDistribution[1]?.count || 0 + ); + }); + }); +});