push-guardian 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/.dockerignore +15 -0
  2. package/.pushguardian-plugins.json +10 -0
  3. package/Dockerfile +41 -0
  4. package/Dockerfile.dev +20 -0
  5. package/README.md +386 -0
  6. package/TECHNO.md +139 -0
  7. package/babel.config.js +1 -0
  8. package/developper_utils.md +119 -0
  9. package/docker-compose.yml +41 -0
  10. package/docs/PLUGINS.md +223 -0
  11. package/docs/technical/architecture.md +298 -0
  12. package/docs/technical/performance-guide.md +390 -0
  13. package/docs/technical/plugin-persistence.md +169 -0
  14. package/docs/technical/plugins-guide.md +409 -0
  15. package/jest.config.js +22 -0
  16. package/package.json +53 -0
  17. package/plugins/example-plugin/index.js +55 -0
  18. package/plugins/example-plugin/plugin.json +8 -0
  19. package/scripts/coverage-report.js +75 -0
  20. package/src/cli/command/config.js +33 -0
  21. package/src/cli/command/install.js +137 -0
  22. package/src/cli/command/mirror.js +90 -0
  23. package/src/cli/command/performance.js +160 -0
  24. package/src/cli/command/plugin.js +171 -0
  25. package/src/cli/command/security.js +152 -0
  26. package/src/cli/command/shell.js +238 -0
  27. package/src/cli/command/validate.js +54 -0
  28. package/src/cli/index.js +23 -0
  29. package/src/cli/install/codeQualityTools.js +156 -0
  30. package/src/cli/install/hooks.js +89 -0
  31. package/src/cli/install/mirroring.js +299 -0
  32. package/src/core/codeQualityTools/configAnalyzer.js +216 -0
  33. package/src/core/codeQualityTools/configGenerator.js +381 -0
  34. package/src/core/codeQualityTools/configManager.js +65 -0
  35. package/src/core/codeQualityTools/fileDetector.js +62 -0
  36. package/src/core/codeQualityTools/languageTools.js +104 -0
  37. package/src/core/codeQualityTools/toolInstaller.js +53 -0
  38. package/src/core/configManager.js +43 -0
  39. package/src/core/errorCMD.js +9 -0
  40. package/src/core/interactiveMenu/interactiveMenu.js +73 -0
  41. package/src/core/mirroring/branchSynchronizer.js +59 -0
  42. package/src/core/mirroring/generate.js +114 -0
  43. package/src/core/mirroring/repoManager.js +112 -0
  44. package/src/core/mirroring/syncManager.js +176 -0
  45. package/src/core/module/env-loader.js +109 -0
  46. package/src/core/performance/metricsCollector.js +217 -0
  47. package/src/core/performance/performanceAnalyzer.js +182 -0
  48. package/src/core/plugins/basePlugin.js +89 -0
  49. package/src/core/plugins/pluginManager.js +123 -0
  50. package/src/core/plugins/pluginRegistry.js +215 -0
  51. package/src/core/validator.js +53 -0
  52. package/src/hooks/constrains/constrains.js +174 -0
  53. package/src/hooks/constrains/constraintEngine.js +140 -0
  54. package/src/utils/chalk-wrapper.js +26 -0
  55. package/src/utils/exec-wrapper.js +6 -0
  56. package/tests/fixtures/mock-eslint-config-array.js +8 -0
  57. package/tests/fixtures/mock-eslint-config-single.js +6 -0
  58. package/tests/fixtures/mockLoadedPlugin.js +11 -0
  59. package/tests/setup.js +28 -0
  60. package/tests/unit/basePlugin.test.js +355 -0
  61. package/tests/unit/branchSynchronizer.test.js +308 -0
  62. package/tests/unit/cli-commands.test.js +144 -0
  63. package/tests/unit/codeQualityConfigManager.test.js +233 -0
  64. package/tests/unit/codeQualityTools.test.js +36 -0
  65. package/tests/unit/command-install.test.js +247 -0
  66. package/tests/unit/command-mirror.test.js +179 -0
  67. package/tests/unit/command-performance.test.js +169 -0
  68. package/tests/unit/command-plugin.test.js +288 -0
  69. package/tests/unit/command-security.test.js +277 -0
  70. package/tests/unit/command-shell.test.js +325 -0
  71. package/tests/unit/configAnalyzer.test.js +593 -0
  72. package/tests/unit/configGenerator.test.js +808 -0
  73. package/tests/unit/configManager.test.js +195 -0
  74. package/tests/unit/constrains.test.js +463 -0
  75. package/tests/unit/constraint.test.js +554 -0
  76. package/tests/unit/env-loader.test.js +279 -0
  77. package/tests/unit/fileDetector.test.js +171 -0
  78. package/tests/unit/install-codeQualityTools.test.js +343 -0
  79. package/tests/unit/install-hooks.test.js +280 -0
  80. package/tests/unit/install-mirroring.test.js +731 -0
  81. package/tests/unit/install-modules.test.js +81 -0
  82. package/tests/unit/interactiveMenu.test.js +426 -0
  83. package/tests/unit/languageTools.test.js +244 -0
  84. package/tests/unit/metricsCollector.test.js +354 -0
  85. package/tests/unit/mirroring-generate.test.js +96 -0
  86. package/tests/unit/modules-exist.test.js +96 -0
  87. package/tests/unit/performanceAnalyzer.test.js +473 -0
  88. package/tests/unit/pluginManager.test.js +427 -0
  89. package/tests/unit/pluginRegistry.test.js +592 -0
  90. package/tests/unit/repoManager.test.js +469 -0
  91. package/tests/unit/reviewAppManager.test.js +5 -0
  92. package/tests/unit/security-command.test.js +43 -0
  93. package/tests/unit/syncManager.test.js +494 -0
  94. package/tests/unit/toolInstaller.test.js +240 -0
  95. package/tests/unit/utils.test.js +144 -0
  96. package/tests/unit/validator.test.js +215 -0
@@ -0,0 +1,355 @@
1
+ const BasePlugin = require('../../src/core/plugins/basePlugin');
2
+
3
+ describe('Core Plugins - basePlugin', () => {
4
+ let plugin;
5
+ let consoleLogSpy;
6
+
7
+ const mockManifest = {
8
+ name: 'test-plugin',
9
+ version: '1.0.0',
10
+ description: 'A test plugin',
11
+ author: 'Test Author',
12
+ main: 'index.js'
13
+ };
14
+
15
+ beforeEach(() => {
16
+ consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
17
+ plugin = new BasePlugin(mockManifest);
18
+ });
19
+
20
+ afterEach(() => {
21
+ consoleLogSpy.mockRestore();
22
+ });
23
+
24
+ describe('construction', () => {
25
+ test('doit créer instance avec manifest', () => {
26
+ expect(plugin.name).toBe('test-plugin');
27
+ expect(plugin.version).toBe('1.0.0');
28
+ expect(plugin.description).toBe('A test plugin');
29
+ expect(plugin.author).toBe('Test Author');
30
+ });
31
+
32
+ test('doit initialiser enabled à true', () => {
33
+ expect(plugin.enabled).toBe(true);
34
+ });
35
+
36
+ test('doit initialiser Map vide pour commandes', () => {
37
+ expect(plugin.commands).toBeInstanceOf(Map);
38
+ expect(plugin.commands.size).toBe(0);
39
+ });
40
+
41
+ test('doit stocker manifest', () => {
42
+ expect(plugin.manifest).toEqual(mockManifest);
43
+ });
44
+
45
+ test('doit gérer manifest sans description', () => {
46
+ const minimalManifest = {
47
+ name: 'minimal-plugin',
48
+ version: '1.0.0'
49
+ };
50
+ const minimalPlugin = new BasePlugin(minimalManifest);
51
+
52
+ expect(minimalPlugin.description).toBe('');
53
+ expect(minimalPlugin.author).toBe('');
54
+ });
55
+
56
+ test('doit gérer manifest sans author', () => {
57
+ const noAuthorManifest = {
58
+ name: 'no-author-plugin',
59
+ version: '1.0.0',
60
+ description: 'Plugin without author'
61
+ };
62
+ const noAuthorPlugin = new BasePlugin(noAuthorManifest);
63
+
64
+ expect(noAuthorPlugin.author).toBe('');
65
+ expect(noAuthorPlugin.description).toBe('Plugin without author');
66
+ });
67
+ });
68
+
69
+ describe('registerCommand', () => {
70
+ test('doit enregistrer une commande simple', () => {
71
+ const handler = jest.fn();
72
+ plugin.registerCommand('test-cmd', handler);
73
+
74
+ expect(plugin.commands.has('test-cmd')).toBe(true);
75
+ expect(plugin.commands.get('test-cmd').handler).toBe(handler);
76
+ });
77
+
78
+ test('doit enregistrer commande avec description', () => {
79
+ const handler = jest.fn();
80
+ const options = { description: 'Test command' };
81
+
82
+ plugin.registerCommand('test-cmd', handler, options);
83
+
84
+ const command = plugin.commands.get('test-cmd');
85
+ expect(command.description).toBe('Test command');
86
+ });
87
+
88
+ test('doit enregistrer commande avec args', () => {
89
+ const handler = jest.fn();
90
+ const options = { args: ['arg1', 'arg2'] };
91
+
92
+ plugin.registerCommand('test-cmd', handler, options);
93
+
94
+ const command = plugin.commands.get('test-cmd');
95
+ expect(command.args).toEqual(['arg1', 'arg2']);
96
+ });
97
+
98
+ test('doit utiliser valeurs par défaut si options vide', () => {
99
+ const handler = jest.fn();
100
+ plugin.registerCommand('test-cmd', handler);
101
+
102
+ const command = plugin.commands.get('test-cmd');
103
+ expect(command.description).toBe('');
104
+ expect(command.args).toEqual([]);
105
+ });
106
+
107
+ test('doit afficher message de confirmation', () => {
108
+ const handler = jest.fn();
109
+ plugin.registerCommand('test-cmd', handler);
110
+
111
+ expect(consoleLogSpy).toHaveBeenCalledWith(
112
+ expect.stringContaining('Commande test-cmd enregistree')
113
+ );
114
+ expect(consoleLogSpy).toHaveBeenCalledWith(
115
+ expect.stringContaining('test-plugin')
116
+ );
117
+ });
118
+
119
+ test('doit stocker nom de commande', () => {
120
+ const handler = jest.fn();
121
+ plugin.registerCommand('my-command', handler);
122
+
123
+ const command = plugin.commands.get('my-command');
124
+ expect(command.name).toBe('my-command');
125
+ });
126
+
127
+ test('doit pouvoir enregistrer plusieurs commandes', () => {
128
+ plugin.registerCommand('cmd1', jest.fn());
129
+ plugin.registerCommand('cmd2', jest.fn());
130
+ plugin.registerCommand('cmd3', jest.fn());
131
+
132
+ expect(plugin.commands.size).toBe(3);
133
+ expect(plugin.commands.has('cmd1')).toBe(true);
134
+ expect(plugin.commands.has('cmd2')).toBe(true);
135
+ expect(plugin.commands.has('cmd3')).toBe(true);
136
+ });
137
+
138
+ test('doit écraser commande existante', () => {
139
+ const handler1 = jest.fn();
140
+ const handler2 = jest.fn();
141
+
142
+ plugin.registerCommand('test-cmd', handler1);
143
+ plugin.registerCommand('test-cmd', handler2);
144
+
145
+ expect(plugin.commands.size).toBe(1);
146
+ expect(plugin.commands.get('test-cmd').handler).toBe(handler2);
147
+ });
148
+ });
149
+
150
+ describe('executeCommand', () => {
151
+ test('doit exécuter commande enregistrée', async () => {
152
+ const handler = jest.fn().mockResolvedValue('result');
153
+ plugin.registerCommand('test-cmd', handler);
154
+
155
+ const result = await plugin.executeCommand('test-cmd');
156
+
157
+ expect(handler).toHaveBeenCalled();
158
+ expect(result).toBe('result');
159
+ });
160
+
161
+ test('doit passer args à handler', async () => {
162
+ const handler = jest.fn().mockResolvedValue(undefined);
163
+ plugin.registerCommand('test-cmd', handler);
164
+
165
+ await plugin.executeCommand('test-cmd', ['arg1', 'arg2']);
166
+
167
+ expect(handler).toHaveBeenCalledWith(['arg1', 'arg2'], {});
168
+ });
169
+
170
+ test('doit passer options à handler', async () => {
171
+ const handler = jest.fn().mockResolvedValue(undefined);
172
+ plugin.registerCommand('test-cmd', handler);
173
+
174
+ await plugin.executeCommand('test-cmd', [], { opt: 'value' });
175
+
176
+ expect(handler).toHaveBeenCalledWith([], { opt: 'value' });
177
+ });
178
+
179
+ test('doit utiliser valeurs par défaut pour args et options', async () => {
180
+ const handler = jest.fn().mockResolvedValue(undefined);
181
+ plugin.registerCommand('test-cmd', handler);
182
+
183
+ await plugin.executeCommand('test-cmd');
184
+
185
+ expect(handler).toHaveBeenCalledWith([], {});
186
+ });
187
+
188
+ test('doit throw si commande non trouvée', async () => {
189
+ await expect(
190
+ plugin.executeCommand('non-existent')
191
+ ).rejects.toThrow('Commande non-existent non trouvee dans test-plugin');
192
+ });
193
+
194
+ test('doit throw si plugin désactivé', async () => {
195
+ const handler = jest.fn();
196
+ plugin.registerCommand('test-cmd', handler);
197
+ plugin.enabled = false;
198
+
199
+ await expect(
200
+ plugin.executeCommand('test-cmd')
201
+ ).rejects.toThrow('Plugin test-plugin est desactive');
202
+
203
+ expect(handler).not.toHaveBeenCalled();
204
+ });
205
+
206
+ test('doit gérer erreur dans handler', async () => {
207
+ const handler = jest.fn().mockRejectedValue(new Error('Handler error'));
208
+ plugin.registerCommand('test-cmd', handler);
209
+
210
+ await expect(
211
+ plugin.executeCommand('test-cmd')
212
+ ).rejects.toThrow('Handler error');
213
+
214
+ expect(consoleLogSpy).toHaveBeenCalledWith(
215
+ expect.stringContaining('Erreur dans test-plugin:test-cmd'),
216
+ expect.stringContaining('Handler error')
217
+ );
218
+ });
219
+
220
+ test('doit propager erreur après log', async () => {
221
+ const error = new Error('Test error');
222
+ const handler = jest.fn().mockRejectedValue(error);
223
+ plugin.registerCommand('test-cmd', handler);
224
+
225
+ await expect(
226
+ plugin.executeCommand('test-cmd')
227
+ ).rejects.toThrow('Test error');
228
+ });
229
+
230
+ test('doit retourner résultat du handler', async () => {
231
+ const handler = jest.fn().mockResolvedValue({ data: 'test' });
232
+ plugin.registerCommand('test-cmd', handler);
233
+
234
+ const result = await plugin.executeCommand('test-cmd');
235
+
236
+ expect(result).toEqual({ data: 'test' });
237
+ });
238
+
239
+ test('doit gérer handler synchrone', async () => {
240
+ const handler = jest.fn().mockReturnValue('sync result');
241
+ plugin.registerCommand('test-cmd', handler);
242
+
243
+ const result = await plugin.executeCommand('test-cmd');
244
+
245
+ expect(result).toBe('sync result');
246
+ });
247
+ });
248
+
249
+ describe('getManifest', () => {
250
+ test('doit retourner manifest', () => {
251
+ const manifest = plugin.getManifest();
252
+
253
+ expect(manifest).toEqual(mockManifest);
254
+ });
255
+
256
+ test('doit retourner même référence que manifest stocké', () => {
257
+ const manifest = plugin.getManifest();
258
+
259
+ expect(manifest).toBe(plugin.manifest);
260
+ });
261
+
262
+ test('doit inclure toutes les propriétés du manifest', () => {
263
+ const manifest = plugin.getManifest();
264
+
265
+ expect(manifest.name).toBe('test-plugin');
266
+ expect(manifest.version).toBe('1.0.0');
267
+ expect(manifest.description).toBe('A test plugin');
268
+ expect(manifest.author).toBe('Test Author');
269
+ expect(manifest.main).toBe('index.js');
270
+ });
271
+ });
272
+
273
+ describe('cleanup', () => {
274
+ test('doit nettoyer les commandes', async () => {
275
+ plugin.registerCommand('cmd1', jest.fn());
276
+ plugin.registerCommand('cmd2', jest.fn());
277
+
278
+ await plugin.cleanup();
279
+
280
+ expect(plugin.commands.size).toBe(0);
281
+ });
282
+
283
+ test('doit afficher message de nettoyage', async () => {
284
+ await plugin.cleanup();
285
+
286
+ expect(consoleLogSpy).toHaveBeenCalledWith(
287
+ expect.stringContaining('Nettoyage du plugin: test-plugin')
288
+ );
289
+ });
290
+
291
+ test('doit vider Map des commandes', async () => {
292
+ plugin.registerCommand('test', jest.fn());
293
+ expect(plugin.commands.size).toBe(1);
294
+
295
+ await plugin.cleanup();
296
+
297
+ expect(plugin.commands.size).toBe(0);
298
+ expect(plugin.commands.has('test')).toBe(false);
299
+ });
300
+
301
+ test('doit être async', async () => {
302
+ const result = plugin.cleanup();
303
+ expect(result).toBeInstanceOf(Promise);
304
+ await result;
305
+ });
306
+ });
307
+
308
+ describe('propriétés', () => {
309
+ test('enabled doit être modifiable', () => {
310
+ expect(plugin.enabled).toBe(true);
311
+ plugin.enabled = false;
312
+ expect(plugin.enabled).toBe(false);
313
+ });
314
+
315
+ test('commands doit être accessible', () => {
316
+ expect(plugin.commands).toBeInstanceOf(Map);
317
+ plugin.registerCommand('test', jest.fn());
318
+ expect(plugin.commands.size).toBe(1);
319
+ });
320
+
321
+ test('toutes les propriétés doivent être accessibles', () => {
322
+ expect(plugin.name).toBeDefined();
323
+ expect(plugin.version).toBeDefined();
324
+ expect(plugin.description).toBeDefined();
325
+ expect(plugin.author).toBeDefined();
326
+ expect(plugin.enabled).toBeDefined();
327
+ expect(plugin.commands).toBeDefined();
328
+ expect(plugin.manifest).toBeDefined();
329
+ });
330
+ });
331
+
332
+ describe('cas d\'utilisation complexes', () => {
333
+ test('doit gérer workflow complet', async () => {
334
+ // Enregistrer commandes
335
+ const handler1 = jest.fn().mockResolvedValue('result1');
336
+ const handler2 = jest.fn().mockResolvedValue('result2');
337
+
338
+ plugin.registerCommand('cmd1', handler1, { description: 'Command 1' });
339
+ plugin.registerCommand('cmd2', handler2, { description: 'Command 2' });
340
+
341
+ // Exécuter commandes
342
+ const result1 = await plugin.executeCommand('cmd1', ['arg'], { opt: true });
343
+ const result2 = await plugin.executeCommand('cmd2');
344
+
345
+ expect(result1).toBe('result1');
346
+ expect(result2).toBe('result2');
347
+ expect(handler1).toHaveBeenCalledWith(['arg'], { opt: true });
348
+ expect(handler2).toHaveBeenCalledWith([], {});
349
+
350
+ // Cleanup
351
+ await plugin.cleanup();
352
+ expect(plugin.commands.size).toBe(0);
353
+ });
354
+ });
355
+ });
@@ -0,0 +1,308 @@
1
+ const { BranchSynchronizer } = require('../../src/core/mirroring/branchSynchronizer');
2
+
3
+ describe('Core Mirroring - BranchSynchronizer', () => {
4
+ let mockClients;
5
+ let synchronizer;
6
+
7
+ beforeEach(() => {
8
+ jest.spyOn(console, 'warn').mockImplementation(() => {});
9
+
10
+ mockClients = {
11
+ github: {
12
+ repos: {
13
+ listBranches: jest.fn()
14
+ },
15
+ git: {
16
+ createRef: jest.fn()
17
+ }
18
+ },
19
+ gitlab: {
20
+ Branches: {
21
+ all: jest.fn(),
22
+ create: jest.fn()
23
+ }
24
+ },
25
+ bitbucket: {
26
+ repositories: {
27
+ listBranches: jest.fn(),
28
+ createBranch: jest.fn()
29
+ }
30
+ }
31
+ };
32
+ });
33
+
34
+ afterEach(() => {
35
+ console.warn.mockRestore();
36
+ });
37
+
38
+ describe('construction', () => {
39
+ test('le module se charge sans erreur', () => {
40
+ expect(BranchSynchronizer).toBeDefined();
41
+ });
42
+
43
+ test('la classe BranchSynchronizer existe', () => {
44
+ expect(typeof BranchSynchronizer).toBe('function');
45
+ });
46
+
47
+ test('peut créer une instance avec des clients', () => {
48
+ const clients = { github: {}, gitlab: {} };
49
+ const synchronizer = new BranchSynchronizer(clients);
50
+ expect(synchronizer).toBeDefined();
51
+ expect(synchronizer.clients).toEqual(clients);
52
+ });
53
+
54
+ test('stocke les clients correctement', () => {
55
+ synchronizer = new BranchSynchronizer(mockClients);
56
+ expect(synchronizer.clients).toBe(mockClients);
57
+ });
58
+ });
59
+
60
+ describe('méthodes', () => {
61
+ test('a une méthode syncBranches', () => {
62
+ const synchronizer = new BranchSynchronizer({});
63
+ expect(typeof synchronizer.syncBranches).toBe('function');
64
+ });
65
+
66
+ test('a une méthode getBranches', () => {
67
+ const synchronizer = new BranchSynchronizer({});
68
+ expect(typeof synchronizer.getBranches).toBe('function');
69
+ });
70
+
71
+ test('a une méthode createBranch', () => {
72
+ const synchronizer = new BranchSynchronizer({});
73
+ expect(typeof synchronizer.createBranch).toBe('function');
74
+ });
75
+ });
76
+
77
+ describe('getBranches', () => {
78
+ beforeEach(() => {
79
+ synchronizer = new BranchSynchronizer(mockClients);
80
+ });
81
+
82
+ test('retourne une erreur si plateforme non supportée', async () => {
83
+ await expect(synchronizer.getBranches('unsupported', 'repo', 'owner')).rejects.toThrow(
84
+ 'Plateforme non prise en charge'
85
+ );
86
+ });
87
+
88
+ test('retourne erreur non implémentée si client existe mais plateforme inconnue', async () => {
89
+ const sync = new BranchSynchronizer({ custom: {} });
90
+
91
+ await expect(sync.getBranches('custom', 'repo', 'owner')).rejects.toThrow(
92
+ 'Liste des branches non implémentée pour custom'
93
+ );
94
+ });
95
+
96
+ test('retourne erreur si client non défini', async () => {
97
+ const sync = new BranchSynchronizer({});
98
+ await expect(sync.getBranches('github', 'repo', 'owner')).rejects.toThrow(
99
+ 'Plateforme non prise en charge'
100
+ );
101
+ });
102
+
103
+ test('doit récupérer branches GitHub', async () => {
104
+ const mockBranches = [
105
+ { name: 'main', commit: { sha: 'abc123' } },
106
+ { name: 'dev', commit: { sha: 'def456' } }
107
+ ];
108
+ mockClients.github.repos.listBranches.mockResolvedValue({ data: mockBranches });
109
+
110
+ const result = await synchronizer.getBranches('github', 'test-repo', 'test-owner');
111
+
112
+ expect(mockClients.github.repos.listBranches).toHaveBeenCalledWith({
113
+ owner: 'test-owner',
114
+ repo: 'test-repo'
115
+ });
116
+ expect(result).toEqual(mockBranches);
117
+ });
118
+
119
+ test('doit récupérer branches GitLab', async () => {
120
+ const mockBranches = [{ name: 'main' }, { name: 'dev' }];
121
+ mockClients.gitlab.Branches.all.mockResolvedValue(mockBranches);
122
+
123
+ const result = await synchronizer.getBranches('gitlab', 'test-repo', 'test-owner');
124
+
125
+ expect(mockClients.gitlab.Branches.all).toHaveBeenCalledWith('test-repo');
126
+ expect(result).toEqual(mockBranches);
127
+ });
128
+
129
+ test('doit récupérer branches Bitbucket', async () => {
130
+ const mockBranches = [{ name: 'main' }, { name: 'dev' }];
131
+ mockClients.bitbucket.repositories.listBranches.mockResolvedValue(mockBranches);
132
+
133
+ const result = await synchronizer.getBranches('bitbucket', 'test-repo', 'test-owner');
134
+
135
+ expect(mockClients.bitbucket.repositories.listBranches).toHaveBeenCalledWith({
136
+ workspace: 'test-owner',
137
+ repo_slug: 'test-repo'
138
+ });
139
+ expect(result).toEqual(mockBranches);
140
+ });
141
+
142
+ test('doit utiliser workspace par défaut pour Bitbucket', async () => {
143
+ mockClients.bitbucket.repositories.listBranches.mockResolvedValue([]);
144
+
145
+ await synchronizer.getBranches('bitbucket', 'test-repo', null);
146
+
147
+ expect(mockClients.bitbucket.repositories.listBranches).toHaveBeenCalledWith({
148
+ workspace: 'workspace',
149
+ repo_slug: 'test-repo'
150
+ });
151
+ });
152
+
153
+ test('doit propager erreur API GitHub', async () => {
154
+ mockClients.github.repos.listBranches.mockRejectedValue(new Error('API Error'));
155
+
156
+ await expect(synchronizer.getBranches('github', 'repo', 'owner')).rejects.toThrow('API Error');
157
+ });
158
+ });
159
+
160
+ describe('createBranch', () => {
161
+ beforeEach(() => {
162
+ synchronizer = new BranchSynchronizer(mockClients);
163
+ });
164
+
165
+ test('retourne une erreur si plateforme non supportée', async () => {
166
+ const branchData = { name: 'test', commit: { sha: 'abc123' } };
167
+ await expect(synchronizer.createBranch('unsupported', 'repo', branchData, 'owner')).rejects.toThrow(
168
+ 'Plateforme non prise en charge'
169
+ );
170
+ });
171
+
172
+ test('retourne erreur non implémentée si client existe mais plateforme inconnue', async () => {
173
+ const sync = new BranchSynchronizer({ custom: {} });
174
+ const branchData = { name: 'test', commit: { sha: 'abc123' } };
175
+
176
+ await expect(sync.createBranch('custom', 'repo', branchData, 'owner')).rejects.toThrow(
177
+ "La création de branche n'est pas implémentée pour custom"
178
+ );
179
+ });
180
+
181
+ test('retourne erreur si client non défini', async () => {
182
+ const sync = new BranchSynchronizer({});
183
+ const branchData = { name: 'test', commit: { sha: 'abc123' } };
184
+ await expect(sync.createBranch('github', 'repo', branchData, 'owner')).rejects.toThrow(
185
+ 'Plateforme non prise en charge'
186
+ );
187
+ });
188
+
189
+ test('doit créer branche GitHub', async () => {
190
+ const branchData = { name: 'feature-test', commit: { sha: 'abc123' } };
191
+ mockClients.github.git.createRef.mockResolvedValue({ data: { ref: 'refs/heads/feature-test' } });
192
+
193
+ await synchronizer.createBranch('github', 'test-repo', branchData, 'test-owner');
194
+
195
+ expect(mockClients.github.git.createRef).toHaveBeenCalledWith({
196
+ owner: 'test-owner',
197
+ repo: 'test-repo',
198
+ ref: 'refs/heads/feature-test',
199
+ sha: 'abc123'
200
+ });
201
+ });
202
+
203
+ test('doit créer branche GitLab', async () => {
204
+ const branchData = { name: 'feature-test', commit: { sha: 'abc123' } };
205
+ mockClients.gitlab.Branches.create.mockResolvedValue({ name: 'feature-test' });
206
+
207
+ await synchronizer.createBranch('gitlab', 'test-repo', branchData, 'test-owner');
208
+
209
+ expect(mockClients.gitlab.Branches.create).toHaveBeenCalledWith('test-repo', 'feature-test', {
210
+ ref: 'abc123'
211
+ });
212
+ });
213
+
214
+ test('doit créer branche Bitbucket', async () => {
215
+ const branchData = { name: 'feature-test', commit: { sha: 'abc123' } };
216
+ mockClients.bitbucket.repositories.createBranch.mockResolvedValue({ name: 'feature-test' });
217
+
218
+ await synchronizer.createBranch('bitbucket', 'test-repo', branchData, 'test-owner');
219
+
220
+ expect(mockClients.bitbucket.repositories.createBranch).toHaveBeenCalledWith({
221
+ workspace: 'test-owner',
222
+ repo_slug: 'test-repo',
223
+ name: 'feature-test',
224
+ target: { hash: 'abc123' }
225
+ });
226
+ });
227
+
228
+ test('doit utiliser workspace par défaut pour Bitbucket', async () => {
229
+ const branchData = { name: 'test', commit: { sha: 'abc123' } };
230
+ mockClients.bitbucket.repositories.createBranch.mockResolvedValue({});
231
+
232
+ await synchronizer.createBranch('bitbucket', 'repo', branchData, null);
233
+
234
+ expect(mockClients.bitbucket.repositories.createBranch).toHaveBeenCalledWith({
235
+ workspace: 'workspace',
236
+ repo_slug: 'repo',
237
+ name: 'test',
238
+ target: { hash: 'abc123' }
239
+ });
240
+ });
241
+ });
242
+
243
+ describe('syncBranches', () => {
244
+ beforeEach(() => {
245
+ synchronizer = new BranchSynchronizer(mockClients);
246
+ });
247
+
248
+ test('doit synchroniser branches entre plateformes', async () => {
249
+ const mockBranches = [
250
+ { name: 'main', commit: { sha: 'abc123' } },
251
+ { name: 'dev', commit: { sha: 'def456' } }
252
+ ];
253
+ mockClients.github.repos.listBranches.mockResolvedValue({ data: mockBranches });
254
+ mockClients.gitlab.Branches.create.mockResolvedValue({});
255
+
256
+ await synchronizer.syncBranches('github', 'gitlab', 'source-repo', 'target-repo', 'owner1', 'owner2');
257
+
258
+ expect(mockClients.github.repos.listBranches).toHaveBeenCalled();
259
+ expect(mockClients.gitlab.Branches.create).toHaveBeenCalledTimes(2);
260
+ });
261
+
262
+ test('doit continuer si création branche échoue', async () => {
263
+ const mockBranches = [
264
+ { name: 'main', commit: { sha: 'abc123' } },
265
+ { name: 'dev', commit: { sha: 'def456' } }
266
+ ];
267
+ mockClients.github.repos.listBranches.mockResolvedValue({ data: mockBranches });
268
+ mockClients.gitlab.Branches.create
269
+ .mockRejectedValueOnce(new Error('Branch exists'))
270
+ .mockResolvedValueOnce({});
271
+
272
+ await synchronizer.syncBranches('github', 'gitlab', 'source-repo', 'target-repo', 'owner1', 'owner2');
273
+
274
+ expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Impossible de créer la branche'));
275
+ expect(mockClients.gitlab.Branches.create).toHaveBeenCalledTimes(2);
276
+ });
277
+
278
+ test('doit lever erreur si getBranches échoue', async () => {
279
+ mockClients.github.repos.listBranches.mockRejectedValue(new Error('API Error'));
280
+
281
+ await expect(
282
+ synchronizer.syncBranches('github', 'gitlab', 'source-repo', 'target-repo', 'owner1', 'owner2')
283
+ ).rejects.toThrow('La synchronisation des branches a échoué');
284
+ });
285
+
286
+ test('doit traiter liste vide de branches', async () => {
287
+ mockClients.github.repos.listBranches.mockResolvedValue({ data: [] });
288
+
289
+ await synchronizer.syncBranches('github', 'gitlab', 'source-repo', 'target-repo', 'owner1', 'owner2');
290
+
291
+ expect(mockClients.gitlab.Branches.create).not.toHaveBeenCalled();
292
+ });
293
+
294
+ test('doit afficher warning pour chaque échec', async () => {
295
+ const mockBranches = [
296
+ { name: 'main', commit: { sha: 'abc123' } },
297
+ { name: 'dev', commit: { sha: 'def456' } },
298
+ { name: 'test', commit: { sha: 'ghi789' } }
299
+ ];
300
+ mockClients.github.repos.listBranches.mockResolvedValue({ data: mockBranches });
301
+ mockClients.gitlab.Branches.create.mockRejectedValue(new Error('Failed'));
302
+
303
+ await synchronizer.syncBranches('github', 'gitlab', 'source-repo', 'target-repo', 'owner1', 'owner2');
304
+
305
+ expect(console.warn).toHaveBeenCalledTimes(3);
306
+ });
307
+ });
308
+ });