test(JiraSync): expand test coverage for synchronization and change detection scenarios
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
221
src/services/integrations/jira/__tests__/analytics-cache.test.ts
Normal file
221
src/services/integrations/jira/__tests__/analytics-cache.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
131
src/services/integrations/jira/__tests__/analytics.test.ts
Normal file
131
src/services/integrations/jira/__tests__/analytics.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
144
src/services/integrations/jira/__tests__/jira.test.ts
Normal file
144
src/services/integrations/jira/__tests__/jira.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
333
src/services/integrations/jira/__tests__/scheduler.test.ts
Normal file
333
src/services/integrations/jira/__tests__/scheduler.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
335
src/services/integrations/tfs/__tests__/scheduler.test.ts
Normal file
335
src/services/integrations/tfs/__tests__/scheduler.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
71
src/services/integrations/tfs/__tests__/tfs.test.ts
Normal file
71
src/services/integrations/tfs/__tests__/tfs.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user