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