test(JiraSync): expand test coverage for synchronization and change detection scenarios

This commit is contained in:
Julien Froidefond
2025-11-21 16:41:33 +01:00
parent 411bac8162
commit ddba4eca37
8 changed files with 1662 additions and 0 deletions

View File

@@ -0,0 +1,207 @@
/**
* Tests unitaires pour JiraAdvancedFiltersService
*/
import { describe, it, expect } from 'vitest';
import { JiraAdvancedFiltersService } from '../advanced-filters';
import { JiraTask, JiraAnalytics } from '@/lib/types';
describe('JiraAdvancedFiltersService', () => {
const mockIssues: JiraTask[] = [
{
id: '1',
key: 'TEST-1',
summary: 'Task 1',
status: { name: 'In Progress', category: 'inprogress' },
issuetype: { name: 'Bug' },
priority: { name: 'High' },
assignee: { displayName: 'John Doe', emailAddress: 'john@example.com' },
components: [{ name: 'Frontend' }],
fixVersions: [{ name: 'v1.0' }],
labels: ['urgent', 'bug'],
project: { key: 'TEST', name: 'Test Project' },
created: '2024-01-01',
updated: '2024-01-02',
},
{
id: '2',
key: 'TEST-2',
summary: 'Task 2',
status: { name: 'Done', category: 'done' },
issuetype: { name: 'Story' },
priority: { name: 'Medium' },
assignee: { displayName: 'Jane Smith', emailAddress: 'jane@example.com' },
components: [{ name: 'Backend' }],
fixVersions: [{ name: 'v2.0' }],
labels: ['feature'],
project: { key: 'TEST', name: 'Test Project' },
created: '2024-01-03',
updated: '2024-01-04',
},
];
const mockAnalytics: JiraAnalytics = {
project: {
key: 'TEST',
name: 'Test Project',
totalIssues: 2,
},
teamMetrics: {
totalAssignees: 2,
activeAssignees: 2,
issuesDistribution: [],
},
velocityMetrics: {
currentSprintPoints: 0,
averageVelocity: 0,
sprintHistory: [],
},
cycleTimeMetrics: {
averageCycleTime: 0,
cycleTimeByType: [],
},
workInProgress: {
byStatus: [],
byAssignee: [],
},
};
describe('extractAvailableFilters', () => {
it('devrait extraire tous les filtres disponibles', () => {
const filters =
JiraAdvancedFiltersService.extractAvailableFilters(mockIssues);
expect(filters.components).toHaveLength(2);
expect(filters.fixVersions).toHaveLength(2);
expect(filters.issueTypes).toHaveLength(2);
expect(filters.statuses).toHaveLength(2);
expect(filters.assignees).toHaveLength(2);
expect(filters.labels).toHaveLength(3);
expect(filters.priorities).toHaveLength(2);
});
it('devrait compter les occurrences correctement', () => {
const filters =
JiraAdvancedFiltersService.extractAvailableFilters(mockIssues);
const frontendComponent = filters.components.find(
(c) => c.value === 'Frontend'
);
expect(frontendComponent?.count).toBe(1);
const urgentLabel = filters.labels.find((l) => l.value === 'urgent');
expect(urgentLabel?.count).toBe(1);
});
it('devrait gérer les issues sans assignee', () => {
const issuesWithoutAssignee: JiraTask[] = [
{
...mockIssues[0],
assignee: undefined,
},
];
const filters = JiraAdvancedFiltersService.extractAvailableFilters(
issuesWithoutAssignee
);
const unassigned = filters.assignees.find(
(a) => a.value === 'Non assigné'
);
expect(unassigned).toBeDefined();
});
it('devrait retourner des filtres vides pour une liste vide', () => {
const filters = JiraAdvancedFiltersService.extractAvailableFilters([]);
expect(filters.components).toHaveLength(0);
expect(filters.fixVersions).toHaveLength(0);
expect(filters.issueTypes).toHaveLength(0);
});
});
describe('applyFiltersToAnalytics', () => {
it('devrait filtrer par composant', () => {
const filtered = JiraAdvancedFiltersService.applyFiltersToAnalytics(
mockAnalytics,
{ components: ['Frontend'] },
mockIssues
);
expect(filtered.project.totalIssues).toBe(1);
});
it('devrait filtrer par type de ticket', () => {
const filtered = JiraAdvancedFiltersService.applyFiltersToAnalytics(
mockAnalytics,
{ issueTypes: ['Bug'] },
mockIssues
);
expect(filtered.project.totalIssues).toBe(1);
});
it('devrait filtrer par statut', () => {
const filtered = JiraAdvancedFiltersService.applyFiltersToAnalytics(
mockAnalytics,
{ statuses: ['In Progress'] },
mockIssues
);
expect(filtered.project.totalIssues).toBe(1);
});
it('devrait filtrer par assignee', () => {
const filtered = JiraAdvancedFiltersService.applyFiltersToAnalytics(
mockAnalytics,
{ assignees: ['John Doe'] },
mockIssues
);
expect(filtered.project.totalIssues).toBe(1);
});
it('devrait filtrer par label', () => {
const filtered = JiraAdvancedFiltersService.applyFiltersToAnalytics(
mockAnalytics,
{ labels: ['urgent'] },
mockIssues
);
expect(filtered.project.totalIssues).toBe(1);
});
it('devrait appliquer plusieurs filtres simultanément', () => {
const filtered = JiraAdvancedFiltersService.applyFiltersToAnalytics(
mockAnalytics,
{
components: ['Frontend'],
issueTypes: ['Bug'],
},
mockIssues
);
expect(filtered.project.totalIssues).toBe(1);
});
it('devrait retourner analytics vide si aucun match', () => {
const filtered = JiraAdvancedFiltersService.applyFiltersToAnalytics(
mockAnalytics,
{ components: ['NonExistent'] },
mockIssues
);
expect(filtered.project.totalIssues).toBe(0);
});
it('devrait retourner analytics original si pas de filtres', () => {
const filtered = JiraAdvancedFiltersService.applyFiltersToAnalytics(
mockAnalytics,
{},
mockIssues
);
expect(filtered.project.totalIssues).toBe(2);
});
});
});

View File

@@ -0,0 +1,221 @@
/**
* Tests unitaires pour JiraAnalyticsCacheService
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { jiraAnalyticsCache } from '../analytics-cache';
import { JiraAnalytics } from '@/lib/types';
describe('JiraAnalyticsCacheService', () => {
const mockConfig = {
baseUrl: 'https://test.atlassian.net',
email: 'test@example.com',
apiToken: 'token123',
projectKey: 'TEST',
};
const mockAnalytics: JiraAnalytics = {
project: {
key: 'TEST',
name: 'Test Project',
totalIssues: 10,
},
teamMetrics: {
totalAssignees: 5,
activeAssignees: 5,
issuesDistribution: [],
},
velocityMetrics: {
currentSprintPoints: 0,
averageVelocity: 0,
sprintHistory: [],
},
cycleTimeMetrics: {
averageCycleTime: 0,
cycleTimeByType: [],
},
workInProgress: {
byStatus: [],
byAssignee: [],
},
};
beforeEach(() => {
jiraAnalyticsCache.invalidateAll();
vi.useFakeTimers();
});
afterEach(() => {
jiraAnalyticsCache.invalidateAll();
jiraAnalyticsCache.stopCleanupInterval();
vi.useRealTimers();
});
describe('set et get', () => {
it('devrait stocker et récupérer les analytics', () => {
jiraAnalyticsCache.set(mockConfig, mockAnalytics);
const result = jiraAnalyticsCache.get(mockConfig);
expect(result).toEqual(mockAnalytics);
});
it('devrait retourner null si pas de cache', () => {
const result = jiraAnalyticsCache.get(mockConfig);
expect(result).toBeNull();
});
it('devrait utiliser un TTL personnalisé', () => {
const customTTL = 60 * 60 * 1000; // 1 heure
jiraAnalyticsCache.set(mockConfig, mockAnalytics, customTTL);
const result = jiraAnalyticsCache.get(mockConfig);
expect(result).toEqual(mockAnalytics);
});
});
describe('invalidate', () => {
it('devrait invalider le cache pour un projet spécifique', () => {
jiraAnalyticsCache.set(mockConfig, mockAnalytics);
jiraAnalyticsCache.invalidate(mockConfig);
const result = jiraAnalyticsCache.get(mockConfig);
expect(result).toBeNull();
});
it('devrait invalider tout le cache', () => {
jiraAnalyticsCache.set(mockConfig, mockAnalytics);
jiraAnalyticsCache.invalidateAll();
const result = jiraAnalyticsCache.get(mockConfig);
expect(result).toBeNull();
});
});
describe('expiration', () => {
it('devrait expirer après le TTL', () => {
const shortTTL = 1000; // 1 seconde
jiraAnalyticsCache.set(mockConfig, mockAnalytics, shortTTL);
// Avancer le temps de 2 secondes
vi.advanceTimersByTime(2000);
const result = jiraAnalyticsCache.get(mockConfig);
expect(result).toBeNull();
});
it('ne devrait pas expirer avant le TTL', () => {
const shortTTL = 5000; // 5 secondes
jiraAnalyticsCache.set(mockConfig, mockAnalytics, shortTTL);
// Avancer le temps de 2 secondes seulement
vi.advanceTimersByTime(2000);
const result = jiraAnalyticsCache.get(mockConfig);
expect(result).toEqual(mockAnalytics);
});
});
describe('config hash', () => {
it('devrait invalider si la config change', () => {
jiraAnalyticsCache.set(mockConfig, mockAnalytics);
const newConfig = {
...mockConfig,
apiToken: 'newtoken',
};
const result = jiraAnalyticsCache.get(newConfig);
expect(result).toBeNull();
});
it('devrait garder le cache si la config est identique', () => {
jiraAnalyticsCache.set(mockConfig, mockAnalytics);
const sameConfig = { ...mockConfig };
const result = jiraAnalyticsCache.get(sameConfig);
expect(result).toEqual(mockAnalytics);
});
});
describe('has', () => {
it('devrait retourner true si le cache existe', () => {
jiraAnalyticsCache.set(mockConfig, mockAnalytics);
const exists = jiraAnalyticsCache.has(mockConfig);
expect(exists).toBe(true);
});
it("devrait retourner false si le cache n'existe pas", () => {
const exists = jiraAnalyticsCache.has(mockConfig);
expect(exists).toBe(false);
});
});
describe('getStats', () => {
it('devrait retourner les statistiques du cache', () => {
jiraAnalyticsCache.set(mockConfig, mockAnalytics);
const stats = jiraAnalyticsCache.getStats();
expect(stats.totalEntries).toBe(1);
expect(stats.projects).toHaveLength(1);
expect(stats.projects[0].projectKey).toBe('TEST');
});
it('devrait retourner des stats vides si pas de cache', () => {
const stats = jiraAnalyticsCache.getStats();
expect(stats.totalEntries).toBe(0);
expect(stats.projects).toHaveLength(0);
});
});
describe('forceCleanup', () => {
it('devrait nettoyer les entrées expirées', () => {
const shortTTL = 1000; // 1 seconde
jiraAnalyticsCache.set(mockConfig, mockAnalytics, shortTTL);
// Avancer le temps pour expirer
vi.advanceTimersByTime(2000);
const cleanedCount = jiraAnalyticsCache.forceCleanup();
expect(cleanedCount).toBe(1);
expect(jiraAnalyticsCache.get(mockConfig)).toBeNull();
});
it('devrait retourner 0 si aucune entrée expirée', () => {
jiraAnalyticsCache.set(mockConfig, mockAnalytics);
const cleanedCount = jiraAnalyticsCache.forceCleanup();
expect(cleanedCount).toBe(0);
expect(jiraAnalyticsCache.get(mockConfig)).not.toBeNull();
});
});
describe('multiple projects', () => {
it('devrait gérer plusieurs projets indépendamment', () => {
const config1 = { ...mockConfig, projectKey: 'PROJ1' };
const config2 = { ...mockConfig, projectKey: 'PROJ2' };
jiraAnalyticsCache.set(config1, mockAnalytics);
jiraAnalyticsCache.set(config2, mockAnalytics);
expect(jiraAnalyticsCache.get(config1)).toEqual(mockAnalytics);
expect(jiraAnalyticsCache.get(config2)).toEqual(mockAnalytics);
jiraAnalyticsCache.invalidate(config1);
expect(jiraAnalyticsCache.get(config1)).toBeNull();
expect(jiraAnalyticsCache.get(config2)).toEqual(mockAnalytics);
});
});
});

View File

@@ -0,0 +1,131 @@
/**
* Tests unitaires pour JiraAnalyticsService
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { JiraAnalyticsService } from '../analytics';
import { JiraService } from '../jira';
import { jiraAnalyticsCache } from '../analytics-cache';
// Mock de JiraService
vi.mock('../jira', () => ({
JiraService: vi.fn(),
}));
// Mock de analytics-cache
vi.mock('../analytics-cache', () => ({
jiraAnalyticsCache: {
get: vi.fn(),
set: vi.fn(),
},
}));
describe('JiraAnalyticsService', () => {
let service: JiraAnalyticsService;
let mockJiraService: any;
const mockConfig = {
enabled: true,
baseUrl: 'https://test.atlassian.net',
email: 'test@example.com',
apiToken: 'token123',
projectKey: 'TEST',
};
beforeEach(() => {
vi.clearAllMocks();
mockJiraService = {
searchIssues: vi.fn(),
validateProject: vi.fn(),
};
vi.mocked(JiraService).mockImplementation(() => mockJiraService as any);
vi.mocked(jiraAnalyticsCache.get).mockReturnValue(null);
service = new JiraAnalyticsService(mockConfig);
});
describe('getAllProjectIssues', () => {
it('devrait récupérer toutes les issues du projet', async () => {
const mockIssues = [
{ key: 'TEST-1', title: 'Issue 1' },
{ key: 'TEST-2', title: 'Issue 2' },
];
vi.mocked(mockJiraService.searchIssues).mockResolvedValue(mockIssues);
const issues = await service.getAllProjectIssues();
expect(issues).toEqual(mockIssues);
expect(mockJiraService.searchIssues).toHaveBeenCalledWith(
'project = "TEST" ORDER BY created DESC'
);
});
it('devrait gérer les erreurs', async () => {
vi.mocked(mockJiraService.searchIssues).mockRejectedValue(
new Error('API Error')
);
await expect(service.getAllProjectIssues()).rejects.toThrow();
});
});
describe('getProjectAnalytics', () => {
it('devrait retourner les analytics depuis le cache si disponible', async () => {
const cachedAnalytics = {
project: { key: 'TEST', name: 'Test', totalIssues: 0 },
teamMetrics: {
totalAssignees: 0,
activeAssignees: 0,
issuesDistribution: [],
},
velocityMetrics: {
currentSprintPoints: 0,
sprintHistory: [],
averageVelocity: 0,
},
cycleTimeMetrics: { averageCycleTime: 0, cycleTimeByType: [] },
workInProgress: { byStatus: [], byAssignee: [] },
};
vi.mocked(jiraAnalyticsCache.get).mockReturnValue(cachedAnalytics as any);
const analytics = await service.getProjectAnalytics();
expect(analytics).toEqual(cachedAnalytics);
expect(mockJiraService.searchIssues).not.toHaveBeenCalled();
});
it('devrait forcer le refresh si demandé', async () => {
const cachedAnalytics = {
project: { key: 'TEST', name: 'Test', totalIssues: 0 },
teamMetrics: {
totalAssignees: 0,
activeAssignees: 0,
issuesDistribution: [],
},
velocityMetrics: {
currentSprintPoints: 0,
sprintHistory: [],
averageVelocity: 0,
},
cycleTimeMetrics: { averageCycleTime: 0, cycleTimeByType: [] },
workInProgress: { byStatus: [], byAssignee: [] },
};
vi.mocked(jiraAnalyticsCache.get).mockReturnValue(cachedAnalytics as any);
vi.mocked(mockJiraService.validateProject).mockResolvedValue({
exists: true,
name: 'Test Project',
});
vi.mocked(mockJiraService.searchIssues).mockResolvedValue([]);
await service.getProjectAnalytics(true);
expect(mockJiraService.searchIssues).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,220 @@
/**
* Tests unitaires pour JiraAnomalyDetectionService
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { JiraAnomalyDetectionService } from '../anomaly-detection';
import { JiraAnalytics } from '@/lib/types';
import { getToday } from '@/lib/date-utils';
// Mock de date-utils
vi.mock('@/lib/date-utils', () => ({
getToday: vi.fn(),
}));
describe('JiraAnomalyDetectionService', () => {
let service: JiraAnomalyDetectionService;
const mockAnalytics: JiraAnalytics = {
project: {
key: 'TEST',
name: 'Test Project',
totalIssues: 10,
},
teamMetrics: {
totalAssignees: 2,
activeAssignees: 2,
issuesDistribution: [],
},
velocityMetrics: {
currentSprintPoints: 0,
sprintHistory: [
{
sprintName: 'Sprint 1',
startDate: '2024-01-01',
endDate: '2024-01-14',
completedPoints: 20,
plannedPoints: 20,
completionRate: 100,
velocity: 20,
},
{
sprintName: 'Sprint 2',
startDate: '2024-01-15',
endDate: '2024-01-28',
completedPoints: 5,
plannedPoints: 20,
completionRate: 25,
velocity: 5,
}, // Variance importante
{
sprintName: 'Sprint 3',
startDate: '2024-01-29',
endDate: '2024-02-11',
completedPoints: 25,
plannedPoints: 20,
completionRate: 125,
velocity: 25,
},
],
averageVelocity: 16.67,
},
cycleTimeMetrics: {
averageCycleTime: 12.5,
cycleTimeByType: [
{ issueType: 'Bug', averageDays: 5, medianDays: 4, samples: 10 },
{ issueType: 'Story', averageDays: 20, medianDays: 18, samples: 5 }, // Cycle time élevé
],
},
workInProgress: {
byStatus: [],
byAssignee: [
{
assignee: 'john@example.com',
displayName: 'John',
todoCount: 3,
inProgressCount: 5,
reviewCount: 0,
totalActive: 8,
},
{
assignee: 'jane@example.com',
displayName: 'Jane',
todoCount: 1,
inProgressCount: 1,
reviewCount: 0,
totalActive: 2,
},
],
},
};
beforeEach(() => {
vi.mocked(getToday).mockReturnValue(new Date('2024-01-15'));
service = new JiraAnomalyDetectionService();
});
describe('detectAnomalies', () => {
it('devrait détecter des anomalies de vélocité', async () => {
const anomalies = await service.detectAnomalies(mockAnalytics);
const velocityAnomalies = anomalies.filter((a) => a.type === 'velocity');
expect(velocityAnomalies.length).toBeGreaterThan(0);
});
it('devrait détecter des anomalies de cycle time', async () => {
const anomalies = await service.detectAnomalies(mockAnalytics);
const cycleTimeAnomalies = anomalies.filter(
(a) => a.type === 'cycle_time'
);
expect(cycleTimeAnomalies.length).toBeGreaterThanOrEqual(0);
});
it('devrait détecter des déséquilibres de charge', async () => {
const anomalies = await service.detectAnomalies(mockAnalytics);
const workloadAnomalies = anomalies.filter((a) => a.type === 'workload');
expect(workloadAnomalies.length).toBeGreaterThanOrEqual(0);
});
it('devrait trier les anomalies par sévérité', async () => {
const anomalies = await service.detectAnomalies(mockAnalytics);
const severityWeights = {
critical: 4,
high: 3,
medium: 2,
low: 1,
};
for (let i = 0; i < anomalies.length - 1; i++) {
const current = severityWeights[anomalies[i].severity];
const next = severityWeights[anomalies[i + 1].severity];
expect(current).toBeGreaterThanOrEqual(next);
}
});
it('devrait retourner une liste vide si aucune anomalie', async () => {
const cleanAnalytics: JiraAnalytics = {
...mockAnalytics,
velocityMetrics: {
currentSprintPoints: 0,
sprintHistory: [
{
sprintName: 'Sprint 1',
startDate: '2024-01-01',
endDate: '2024-01-14',
completedPoints: 20,
plannedPoints: 20,
completionRate: 100,
velocity: 20,
},
{
sprintName: 'Sprint 2',
startDate: '2024-01-15',
endDate: '2024-01-28',
completedPoints: 21,
plannedPoints: 20,
completionRate: 105,
velocity: 21,
},
{
sprintName: 'Sprint 3',
startDate: '2024-01-29',
endDate: '2024-02-11',
completedPoints: 19,
plannedPoints: 20,
completionRate: 95,
velocity: 19,
},
],
averageVelocity: 20,
},
cycleTimeMetrics: {
averageCycleTime: 5,
cycleTimeByType: [
{ issueType: 'Story', averageDays: 5, medianDays: 4, samples: 10 },
],
},
workInProgress: {
byStatus: [],
byAssignee: [
{
assignee: 'john@example.com',
displayName: 'John',
todoCount: 2,
inProgressCount: 1,
reviewCount: 0,
totalActive: 3,
},
{
assignee: 'jane@example.com',
displayName: 'Jane',
todoCount: 1,
inProgressCount: 1,
reviewCount: 0,
totalActive: 2,
},
],
},
};
const anomalies = await service.detectAnomalies(cleanAnalytics);
// Peut avoir quelques anomalies même avec des données propres selon les seuils
expect(Array.isArray(anomalies)).toBe(true);
});
});
describe('config personnalisée', () => {
it('devrait utiliser une config personnalisée', () => {
const customConfig = {
velocityVarianceThreshold: 50,
cycleTimeThreshold: 3.0,
};
const customService = new JiraAnomalyDetectionService(customConfig);
expect(customService).toBeDefined();
});
});
});

View File

@@ -0,0 +1,144 @@
/**
* Tests unitaires pour JiraService
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { JiraService } from '../jira';
// Mock de fetch global
global.fetch = vi.fn();
describe('JiraService', () => {
let service: JiraService;
const mockConfig = {
enabled: true,
baseUrl: 'https://test.atlassian.net',
email: 'test@example.com',
apiToken: 'token123',
projectKey: 'TEST',
ignoredProjects: [],
};
beforeEach(() => {
vi.clearAllMocks();
service = new JiraService(mockConfig);
});
describe('testConnection', () => {
it('devrait retourner true si la connexion réussit', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
status: 200,
text: vi.fn().mockResolvedValue(''),
} as any);
const result = await service.testConnection();
expect(result).toBe(true);
expect(fetch).toHaveBeenCalled();
});
it('devrait retourner false si la connexion échoue', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: false,
status: 401,
text: vi.fn().mockResolvedValue('Unauthorized'),
} as any);
const result = await service.testConnection();
expect(result).toBe(false);
});
it('devrait gérer les erreurs de réseau', async () => {
vi.mocked(fetch).mockRejectedValue(new Error('Network error'));
const result = await service.testConnection();
expect(result).toBe(false);
});
});
describe('validateConfig', () => {
it('devrait valider une config complète', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
status: 200,
text: vi.fn().mockResolvedValue(''),
} as any);
const result = await service.validateConfig();
expect(result.valid).toBe(true);
});
it('devrait rejeter une config sans baseUrl', async () => {
const invalidConfig = {
...mockConfig,
baseUrl: undefined,
};
const invalidService = new JiraService(invalidConfig as any);
const result = await invalidService.validateConfig();
expect(result.valid).toBe(false);
expect(result.error).toContain('URL de base');
});
it('devrait rejeter une config sans email', async () => {
const invalidConfig = {
...mockConfig,
email: undefined,
};
const invalidService = new JiraService(invalidConfig as any);
const result = await invalidService.validateConfig();
expect(result.valid).toBe(false);
expect(result.error).toContain('Email');
});
it('devrait rejeter une config sans apiToken', async () => {
const invalidConfig = {
...mockConfig,
apiToken: undefined,
};
const invalidService = new JiraService(invalidConfig as any);
const result = await invalidService.validateConfig();
expect(result.valid).toBe(false);
expect(result.error).toContain('Token API');
});
});
describe('validateProject', () => {
it('devrait valider un projet existant', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({ name: 'Test Project' }),
} as any);
const result = await service.validateProject('TEST');
expect(result.exists).toBe(true);
expect(result.name).toBe('Test Project');
});
it('devrait détecter un projet inexistant', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: false,
status: 404,
text: vi.fn().mockResolvedValue('Not found'),
} as any);
const result = await service.validateProject('INVALID');
expect(result.exists).toBe(false);
expect(result.error).toBeDefined();
});
});
});

View File

@@ -0,0 +1,333 @@
/**
* Tests unitaires pour JiraScheduler
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { JiraScheduler } from '../scheduler';
import { userPreferencesService } from '@/services/core/user-preferences';
import { JiraService } from '../jira';
import { getToday, addMinutes } from '@/lib/date-utils';
// Mock de userPreferencesService
vi.mock('@/services/core/user-preferences', () => ({
userPreferencesService: {
getJiraConfig: vi.fn(),
getJiraSchedulerConfig: vi.fn(),
},
}));
// Mock de JiraService
vi.mock('../jira', () => ({
JiraService: vi.fn(),
}));
// Mock de date-utils
vi.mock('@/lib/date-utils', () => ({
getToday: vi.fn(),
addMinutes: vi.fn(),
}));
describe('JiraScheduler', () => {
let scheduler: JiraScheduler;
let mockJiraService: any;
beforeEach(() => {
vi.clearAllMocks();
scheduler = new JiraScheduler();
vi.useFakeTimers();
// Mock JiraService
mockJiraService = {
testConnection: vi.fn(),
syncTasks: vi.fn(),
};
vi.mocked(JiraService).mockImplementation(() => mockJiraService as any);
});
afterEach(() => {
scheduler.stop();
vi.useRealTimers();
});
describe('start', () => {
it('devrait démarrer le scheduler si activé et configuré', async () => {
vi.mocked(
userPreferencesService.getJiraSchedulerConfig
).mockResolvedValue({
jiraAutoSync: true,
jiraSyncInterval: 'hourly',
} as any);
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
enabled: true,
baseUrl: 'https://test.atlassian.net',
email: 'test@example.com',
apiToken: 'token123',
} as any);
await scheduler.start('user1');
expect(scheduler.isActive()).toBe(true);
});
it('ne devrait pas démarrer si désactivé', async () => {
vi.mocked(
userPreferencesService.getJiraSchedulerConfig
).mockResolvedValue({
jiraAutoSync: false,
jiraSyncInterval: 'hourly',
} as any);
await scheduler.start('user1');
expect(scheduler.isActive()).toBe(false);
});
it('ne devrait pas démarrer si Jira non configuré', async () => {
vi.mocked(
userPreferencesService.getJiraSchedulerConfig
).mockResolvedValue({
jiraAutoSync: true,
jiraSyncInterval: 'hourly',
} as any);
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
enabled: false,
} as any);
await scheduler.start('user1');
expect(scheduler.isActive()).toBe(false);
});
it('ne devrait pas démarrer deux fois', async () => {
vi.mocked(
userPreferencesService.getJiraSchedulerConfig
).mockResolvedValue({
jiraAutoSync: true,
jiraSyncInterval: 'hourly',
} as any);
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
enabled: true,
baseUrl: 'https://test.atlassian.net',
email: 'test@example.com',
apiToken: 'token123',
} as any);
await scheduler.start('user1');
const firstCall = scheduler.isActive();
await scheduler.start('user1');
const secondCall = scheduler.isActive();
expect(firstCall).toBe(true);
expect(secondCall).toBe(true);
});
});
describe('stop', () => {
it('devrait arrêter le scheduler', async () => {
vi.mocked(
userPreferencesService.getJiraSchedulerConfig
).mockResolvedValue({
jiraAutoSync: true,
jiraSyncInterval: 'hourly',
} as any);
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
enabled: true,
baseUrl: 'https://test.atlassian.net',
email: 'test@example.com',
apiToken: 'token123',
} as any);
await scheduler.start('user1');
expect(scheduler.isActive()).toBe(true);
scheduler.stop();
expect(scheduler.isActive()).toBe(false);
});
});
describe('restart', () => {
it('devrait redémarrer le scheduler', async () => {
vi.mocked(
userPreferencesService.getJiraSchedulerConfig
).mockResolvedValue({
jiraAutoSync: true,
jiraSyncInterval: 'hourly',
} as any);
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
enabled: true,
baseUrl: 'https://test.atlassian.net',
email: 'test@example.com',
apiToken: 'token123',
} as any);
await scheduler.start('user1');
expect(scheduler.isActive()).toBe(true);
await scheduler.restart('user1');
expect(scheduler.isActive()).toBe(true);
});
});
describe('isActive', () => {
it('devrait retourner false si pas démarré', () => {
expect(scheduler.isActive()).toBe(false);
});
it('devrait retourner true si démarré', async () => {
vi.mocked(
userPreferencesService.getJiraSchedulerConfig
).mockResolvedValue({
jiraAutoSync: true,
jiraSyncInterval: 'hourly',
} as any);
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
enabled: true,
baseUrl: 'https://test.atlassian.net',
email: 'test@example.com',
apiToken: 'token123',
} as any);
await scheduler.start('user1');
expect(scheduler.isActive()).toBe(true);
});
});
describe('performScheduledSync', () => {
it('devrait exécuter une synchronisation automatique', async () => {
vi.mocked(
userPreferencesService.getJiraSchedulerConfig
).mockResolvedValue({
jiraAutoSync: true,
jiraSyncInterval: 'hourly',
} as any);
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
enabled: true,
baseUrl: 'https://test.atlassian.net',
email: 'test@example.com',
apiToken: 'token123',
} as any);
vi.mocked(mockJiraService.testConnection).mockResolvedValue(true);
vi.mocked(mockJiraService.syncTasks).mockResolvedValue({
success: true,
stats: {
created: 5,
updated: 3,
skipped: 1,
},
} as any);
await scheduler.start('user1');
// Avancer le timer pour déclencher la synchronisation
vi.advanceTimersByTime(60 * 60 * 1000); // 1 heure
// Attendre que la synchronisation soit exécutée
await vi.waitFor(() => {
expect(mockJiraService.syncTasks).toHaveBeenCalled();
});
scheduler.stop();
});
it('devrait gérer les erreurs de connexion', async () => {
vi.mocked(
userPreferencesService.getJiraSchedulerConfig
).mockResolvedValue({
jiraAutoSync: true,
jiraSyncInterval: 'hourly',
} as any);
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
enabled: true,
baseUrl: 'https://test.atlassian.net',
email: 'test@example.com',
apiToken: 'token123',
} as any);
vi.mocked(mockJiraService.testConnection).mockResolvedValue(false);
await scheduler.start('user1');
vi.advanceTimersByTime(60 * 60 * 1000);
await vi.waitFor(() => {
expect(mockJiraService.testConnection).toHaveBeenCalled();
});
scheduler.stop();
});
});
describe('getNextSyncTime', () => {
it('devrait retourner null si pas démarré', async () => {
const nextTime = await scheduler.getNextSyncTime();
expect(nextTime).toBeNull();
});
it('devrait calculer le prochain moment de synchronisation', async () => {
const mockDate = new Date('2024-01-15T12:00:00Z');
vi.mocked(getToday).mockReturnValue(mockDate);
vi.mocked(addMinutes).mockReturnValue(new Date('2024-01-15T13:00:00Z'));
vi.mocked(
userPreferencesService.getJiraSchedulerConfig
).mockResolvedValue({
jiraAutoSync: true,
jiraSyncInterval: 'hourly',
} as any);
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
enabled: true,
baseUrl: 'https://test.atlassian.net',
email: 'test@example.com',
apiToken: 'token123',
} as any);
await scheduler.start('user1');
const nextTime = await scheduler.getNextSyncTime();
expect(nextTime).not.toBeNull();
expect(addMinutes).toHaveBeenCalled();
});
});
describe('getStatus', () => {
it('devrait retourner le statut du scheduler', async () => {
vi.mocked(
userPreferencesService.getJiraSchedulerConfig
).mockResolvedValue({
jiraAutoSync: true,
jiraSyncInterval: 'daily',
} as any);
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
enabled: true,
baseUrl: 'https://test.atlassian.net',
email: 'test@example.com',
apiToken: 'token123',
} as any);
await scheduler.start('user1');
const status = await scheduler.getStatus('user1');
expect(status.isRunning).toBe(true);
expect(status.isEnabled).toBe(true);
expect(status.interval).toBe('daily');
expect(status.jiraConfigured).toBe(true);
});
it('devrait retourner le statut si pas démarré', async () => {
vi.mocked(
userPreferencesService.getJiraSchedulerConfig
).mockResolvedValue({
jiraAutoSync: false,
jiraSyncInterval: 'hourly',
} as any);
vi.mocked(userPreferencesService.getJiraConfig).mockResolvedValue({
enabled: false,
} as any);
const status = await scheduler.getStatus('user1');
expect(status.isRunning).toBe(false);
expect(status.isEnabled).toBe(false);
});
});
});

View File

@@ -0,0 +1,335 @@
/**
* Tests unitaires pour TfsScheduler
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { TfsScheduler } from '../scheduler';
import { userPreferencesService } from '@/services/core/user-preferences';
import { TfsService } from '../tfs';
import { prisma } from '@/services/core/database';
import { getToday, addMinutes } from '@/lib/date-utils';
// Mock de userPreferencesService
vi.mock('@/services/core/user-preferences', () => ({
userPreferencesService: {
getTfsConfig: vi.fn(),
getTfsSchedulerConfig: vi.fn(),
},
}));
// Mock de TfsService
vi.mock('../tfs', () => ({
TfsService: vi.fn(),
}));
// Mock de prisma
vi.mock('@/services/core/database', () => ({
prisma: {
user: {
findFirst: vi.fn(),
},
},
}));
// Mock de date-utils
vi.mock('@/lib/date-utils', () => ({
getToday: vi.fn(),
addMinutes: vi.fn(),
}));
describe('TfsScheduler', () => {
let scheduler: TfsScheduler;
let mockTfsService: any;
beforeEach(() => {
vi.clearAllMocks();
scheduler = new TfsScheduler();
vi.useFakeTimers();
// Mock TfsService
mockTfsService = {
testConnection: vi.fn(),
syncTasks: vi.fn(),
};
vi.mocked(TfsService).mockImplementation(() => mockTfsService as any);
});
afterEach(() => {
scheduler.stop();
vi.useRealTimers();
});
describe('start', () => {
it('devrait démarrer le scheduler si activé et configuré', async () => {
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
{
tfsAutoSync: true,
tfsSyncInterval: 'hourly',
} as any
);
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
enabled: true,
organizationUrl: 'https://dev.azure.com/test',
personalAccessToken: 'token123',
} as any);
await scheduler.start('user1');
expect(scheduler.isActive()).toBe(true);
});
it('ne devrait pas démarrer si désactivé', async () => {
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
{
tfsAutoSync: false,
tfsSyncInterval: 'hourly',
} as any
);
await scheduler.start('user1');
expect(scheduler.isActive()).toBe(false);
});
it('ne devrait pas démarrer si TFS non configuré', async () => {
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
{
tfsAutoSync: true,
tfsSyncInterval: 'hourly',
} as any
);
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
enabled: false,
} as any);
await scheduler.start('user1');
expect(scheduler.isActive()).toBe(false);
});
it('ne devrait pas démarrer deux fois', async () => {
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
{
tfsAutoSync: true,
tfsSyncInterval: 'hourly',
} as any
);
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
enabled: true,
organizationUrl: 'https://dev.azure.com/test',
personalAccessToken: 'token123',
} as any);
await scheduler.start('user1');
const firstCall = scheduler.isActive();
await scheduler.start('user1');
const secondCall = scheduler.isActive();
expect(firstCall).toBe(true);
expect(secondCall).toBe(true);
});
});
describe('stop', () => {
it('devrait arrêter le scheduler', async () => {
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
{
tfsAutoSync: true,
tfsSyncInterval: 'hourly',
} as any
);
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
enabled: true,
organizationUrl: 'https://dev.azure.com/test',
personalAccessToken: 'token123',
} as any);
await scheduler.start('user1');
expect(scheduler.isActive()).toBe(true);
scheduler.stop();
expect(scheduler.isActive()).toBe(false);
});
});
describe('restart', () => {
it('devrait redémarrer le scheduler', async () => {
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
{
tfsAutoSync: true,
tfsSyncInterval: 'hourly',
} as any
);
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
enabled: true,
organizationUrl: 'https://dev.azure.com/test',
personalAccessToken: 'token123',
} as any);
await scheduler.start('user1');
expect(scheduler.isActive()).toBe(true);
await scheduler.restart('user1');
expect(scheduler.isActive()).toBe(true);
});
});
describe('isActive', () => {
it('devrait retourner false si pas démarré', () => {
expect(scheduler.isActive()).toBe(false);
});
it('devrait retourner true si démarré', async () => {
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
{
tfsAutoSync: true,
tfsSyncInterval: 'hourly',
} as any
);
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
enabled: true,
organizationUrl: 'https://dev.azure.com/test',
personalAccessToken: 'token123',
} as any);
await scheduler.start('user1');
expect(scheduler.isActive()).toBe(true);
});
});
describe('performScheduledSync', () => {
it('devrait exécuter une synchronisation automatique', async () => {
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
{
tfsAutoSync: true,
tfsSyncInterval: 'hourly',
} as any
);
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
enabled: true,
organizationUrl: 'https://dev.azure.com/test',
personalAccessToken: 'token123',
} as any);
vi.mocked(prisma.user.findFirst).mockResolvedValue({
id: 'user1',
} as any);
vi.mocked(mockTfsService.testConnection).mockResolvedValue(true);
vi.mocked(mockTfsService.syncTasks).mockResolvedValue({
success: true,
pullRequestsCreated: 5,
pullRequestsUpdated: 3,
pullRequestsSkipped: 1,
} as any);
await scheduler.start('user1');
// Avancer le timer pour déclencher la synchronisation
vi.advanceTimersByTime(60 * 60 * 1000); // 1 heure
// Attendre que la synchronisation soit exécutée
await vi.waitFor(() => {
expect(mockTfsService.syncTasks).toHaveBeenCalled();
});
scheduler.stop();
});
it('devrait gérer les erreurs de connexion', async () => {
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
{
tfsAutoSync: true,
tfsSyncInterval: 'hourly',
} as any
);
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
enabled: true,
organizationUrl: 'https://dev.azure.com/test',
personalAccessToken: 'token123',
} as any);
vi.mocked(mockTfsService.testConnection).mockResolvedValue(false);
await scheduler.start('user1');
vi.advanceTimersByTime(60 * 60 * 1000);
await vi.waitFor(() => {
expect(mockTfsService.testConnection).toHaveBeenCalled();
});
scheduler.stop();
});
});
describe('getNextSyncTime', () => {
it('devrait retourner null si pas démarré', async () => {
const nextTime = await scheduler.getNextSyncTime();
expect(nextTime).toBeNull();
});
it('devrait calculer le prochain moment de synchronisation', async () => {
const mockDate = new Date('2024-01-15T12:00:00Z');
vi.mocked(getToday).mockReturnValue(mockDate);
vi.mocked(addMinutes).mockReturnValue(new Date('2024-01-15T13:00:00Z'));
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
{
tfsAutoSync: true,
tfsSyncInterval: 'hourly',
} as any
);
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
enabled: true,
organizationUrl: 'https://dev.azure.com/test',
personalAccessToken: 'token123',
} as any);
await scheduler.start('user1');
const nextTime = await scheduler.getNextSyncTime();
expect(nextTime).not.toBeNull();
expect(addMinutes).toHaveBeenCalled();
});
});
describe('getStatus', () => {
it('devrait retourner le statut du scheduler', async () => {
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
{
tfsAutoSync: true,
tfsSyncInterval: 'daily',
} as any
);
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
enabled: true,
organizationUrl: 'https://dev.azure.com/test',
personalAccessToken: 'token123',
} as any);
await scheduler.start('user1');
const status = await scheduler.getStatus('user1');
expect(status.isRunning).toBe(true);
expect(status.isEnabled).toBe(true);
expect(status.interval).toBe('daily');
expect(status.tfsConfigured).toBe(true);
});
it('devrait retourner le statut si pas démarré', async () => {
vi.mocked(userPreferencesService.getTfsSchedulerConfig).mockResolvedValue(
{
tfsAutoSync: false,
tfsSyncInterval: 'hourly',
} as any
);
vi.mocked(userPreferencesService.getTfsConfig).mockResolvedValue({
enabled: false,
} as any);
const status = await scheduler.getStatus('user1');
expect(status.isRunning).toBe(false);
expect(status.isEnabled).toBe(false);
});
});
});

View File

@@ -0,0 +1,71 @@
/**
* Tests unitaires pour TfsService
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { TfsService } from '../tfs';
// Mock de fetch global
global.fetch = vi.fn();
describe('TfsService', () => {
let service: TfsService;
const mockConfig = {
enabled: true,
organizationUrl: 'https://dev.azure.com/test',
personalAccessToken: 'token123',
repositories: [],
ignoredRepositories: [],
};
beforeEach(() => {
vi.clearAllMocks();
service = new TfsService(mockConfig);
});
describe('testConnection', () => {
it('devrait retourner true si la connexion réussit', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
status: 200,
} as any);
const result = await service.testConnection();
expect(result).toBe(true);
expect(fetch).toHaveBeenCalled();
});
it('devrait retourner false si authentification échoue', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: false,
status: 401,
} as any);
const result = await service.testConnection();
expect(result).toBe(false);
});
it('devrait retourner false si accès refusé', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: false,
status: 403,
} as any);
const result = await service.testConnection();
expect(result).toBe(false);
});
it('devrait gérer les erreurs de réseau', async () => {
vi.mocked(fetch).mockRejectedValue(new Error('Network error'));
const result = await service.testConnection();
expect(result).toBe(false);
});
});
});