test(JiraSync): enhance test coverage for change detection logic and preserved fields

This commit is contained in:
Julien Froidefond
2025-11-21 14:56:16 +01:00
parent a1d631037e
commit 5c9b2b9d8f
5 changed files with 1708 additions and 0 deletions

View File

@@ -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é');
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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
);
});
});
});