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,731 @@
1
+ const fs = require('fs');
2
+
3
+ jest.mock('fs');
4
+ jest.mock('readline', () => ({
5
+ createInterface: jest.fn(() => ({
6
+ question: jest.fn(),
7
+ close: jest.fn()
8
+ }))
9
+ }));
10
+ jest.mock('../../src/core/module/env-loader');
11
+ jest.mock('../../src/utils/chalk-wrapper', () => ({
12
+ getChalk: () => ({
13
+ red: (msg) => msg,
14
+ green: (msg) => msg,
15
+ blue: (msg) => msg,
16
+ yellow: (msg) => msg,
17
+ gray: (msg) => msg,
18
+ cyan: (msg) => msg
19
+ })
20
+ }));
21
+ jest.mock('../../src/core/interactiveMenu/interactiveMenu');
22
+
23
+ const { getEnv, saveEnv } = require('../../src/core/module/env-loader');
24
+ const mirroring = require('../../src/cli/install/mirroring');
25
+ const interactiveMenu = require('../../src/core/interactiveMenu/interactiveMenu');
26
+
27
+ describe('CLI Install - mirroring', () => {
28
+ beforeEach(() => {
29
+ jest.clearAllMocks();
30
+ console.log = jest.fn();
31
+ console.error = jest.fn();
32
+ jest.spyOn(process, 'exit').mockImplementation(() => {});
33
+
34
+ // Mock env-loader
35
+ getEnv.mockReturnValue('');
36
+ saveEnv.mockImplementation(() => {});
37
+
38
+ fs.existsSync.mockReturnValue(false);
39
+ fs.readFileSync.mockReturnValue('{}');
40
+ fs.writeFileSync.mockImplementation(() => {});
41
+
42
+ // Mock interactiveMenu
43
+ interactiveMenu.mockResolvedValue([]);
44
+ });
45
+
46
+ afterEach(() => {
47
+ // Nettoyer les timers et promesses pendantes
48
+ jest.clearAllTimers();
49
+ jest.restoreAllMocks();
50
+ });
51
+
52
+ describe('getCredentialsFromEnv', () => {
53
+ test('doit charger token GitHub depuis env', () => {
54
+ getEnv.mockImplementation((key) => {
55
+ if (key === 'GITHUB_TOKEN') return 'env_github_token';
56
+ return '';
57
+ });
58
+
59
+ const result = mirroring.getCredentialsFromEnv('github');
60
+
61
+ expect(getEnv).toHaveBeenCalledWith('GITHUB_TOKEN', null, true);
62
+ expect(result).toEqual({ token: 'env_github_token' });
63
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Token GitHub chargé'));
64
+ });
65
+
66
+ test('doit charger token GitLab depuis env', () => {
67
+ getEnv.mockImplementation((key) => {
68
+ if (key === 'GITLAB_TOKEN') return 'env_gitlab_token';
69
+ return '';
70
+ });
71
+
72
+ const result = mirroring.getCredentialsFromEnv('gitlab');
73
+
74
+ expect(getEnv).toHaveBeenCalledWith('GITLAB_TOKEN', null, true);
75
+ expect(result).toEqual({ token: 'env_gitlab_token' });
76
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Token GitLab chargé'));
77
+ });
78
+
79
+ test('doit charger credentials BitBucket depuis env', () => {
80
+ getEnv.mockImplementation((key) => {
81
+ if (key === 'BITBUCKET_USERNAME') return 'env_user';
82
+ if (key === 'BITBUCKET_PASSWORD') return 'env_pass';
83
+ return '';
84
+ });
85
+
86
+ const result = mirroring.getCredentialsFromEnv('bitbucket');
87
+
88
+ expect(getEnv).toHaveBeenCalledWith('BITBUCKET_USERNAME', null, true);
89
+ expect(getEnv).toHaveBeenCalledWith('BITBUCKET_PASSWORD', null, true);
90
+ expect(result).toEqual({
91
+ username: 'env_user',
92
+ password: 'env_pass'
93
+ });
94
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Credentials BitBucket chargés'));
95
+ });
96
+
97
+ test('doit charger credentials Azure depuis env', () => {
98
+ getEnv.mockImplementation((key) => {
99
+ if (key === 'AZURE_DEVOPS_URL') return 'https://dev.azure.com/org';
100
+ if (key === 'AZURE_DEVOPS_TOKEN') return 'azure_token';
101
+ return '';
102
+ });
103
+
104
+ const result = mirroring.getCredentialsFromEnv('azure');
105
+
106
+ expect(result).toEqual({
107
+ url: 'https://dev.azure.com/org',
108
+ token: 'azure_token'
109
+ });
110
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Credentials Azure DevOps chargés'));
111
+ });
112
+
113
+ test('doit afficher message aide si variable env manquante', () => {
114
+ getEnv.mockImplementation(() => {
115
+ throw new Error('Variable not found');
116
+ });
117
+
118
+ // Capturer la Promise retournée sans l'attendre
119
+ try {
120
+ mirroring.getCredentialsFromEnv('github');
121
+ } catch (e) {
122
+ // Ignorer l'erreur
123
+ }
124
+
125
+ expect(console.log).toHaveBeenCalledWith(
126
+ expect.stringContaining('Variable d\'environnement manquante pour github')
127
+ );
128
+ expect(console.log).toHaveBeenCalledWith(
129
+ expect.stringContaining('Vous pouvez créer un fichier .env')
130
+ );
131
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Exemple de fichier .env:'));
132
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('GITHUB_TOKEN=votre_token_ici'));
133
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('GITLAB_TOKEN=votre_token_ici'));
134
+ });
135
+
136
+ test('doit retourner des credentials vides pour plateforme inconnue', () => {
137
+ const result = mirroring.getCredentialsFromEnv('unknown');
138
+ expect(result).toEqual({});
139
+ });
140
+ });
141
+
142
+ describe('saveCredentialsToEnv', () => {
143
+ test('doit sauvegarder token GitHub dans .env', () => {
144
+ getEnv.mockReturnValue(''); // Pas de token existant
145
+
146
+ mirroring.saveCredentialsToEnv({
147
+ github: { token: 'new_github_token' }
148
+ });
149
+
150
+ expect(saveEnv).toHaveBeenCalledWith('GITHUB_TOKEN', 'new_github_token', '.env');
151
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Credentials sauvegardés'));
152
+ });
153
+
154
+ test('ne doit pas écraser token GitHub existant', () => {
155
+ getEnv.mockImplementation((key) => {
156
+ if (key === 'GITHUB_TOKEN') return 'existing_token';
157
+ return '';
158
+ });
159
+
160
+ mirroring.saveCredentialsToEnv({
161
+ github: { token: 'new_token' }
162
+ });
163
+
164
+ expect(saveEnv).not.toHaveBeenCalledWith('GITHUB_TOKEN', expect.anything(), expect.anything());
165
+ });
166
+
167
+ test('doit sauvegarder credentials BitBucket', () => {
168
+ getEnv.mockReturnValue('');
169
+
170
+ mirroring.saveCredentialsToEnv({
171
+ bitbucket: {
172
+ username: 'bitbucket_user',
173
+ password: 'bitbucket_pass'
174
+ }
175
+ });
176
+
177
+ expect(saveEnv).toHaveBeenCalledWith('BITBUCKET_USERNAME', 'bitbucket_user', '.env');
178
+ expect(saveEnv).toHaveBeenCalledWith('BITBUCKET_PASSWORD', 'bitbucket_pass', '.env');
179
+ });
180
+
181
+ test('doit sauvegarder credentials Azure', () => {
182
+ getEnv.mockReturnValue('');
183
+
184
+ mirroring.saveCredentialsToEnv({
185
+ azure: {
186
+ url: 'https://dev.azure.com/org',
187
+ token: 'azure_token'
188
+ }
189
+ });
190
+
191
+ expect(saveEnv).toHaveBeenCalledWith('AZURE_DEVOPS_URL', 'https://dev.azure.com/org', '.env');
192
+ expect(saveEnv).toHaveBeenCalledWith('AZURE_DEVOPS_TOKEN', 'azure_token', '.env');
193
+ });
194
+
195
+ test('doit gérer plusieurs plateformes', () => {
196
+ getEnv.mockReturnValue('');
197
+
198
+ mirroring.saveCredentialsToEnv({
199
+ github: { token: 'gh_token' },
200
+ gitlab: { token: 'gl_token' }
201
+ });
202
+
203
+ expect(saveEnv).toHaveBeenCalledWith('GITHUB_TOKEN', 'gh_token', '.env');
204
+ expect(saveEnv).toHaveBeenCalledWith('GITLAB_TOKEN', 'gl_token', '.env');
205
+ });
206
+ });
207
+
208
+ describe('createMirroringConfig', () => {
209
+ test('doit créer config avec plateformes sélectionnées', () => {
210
+ mirroring.createMirroringConfig(
211
+ ['GitHub', 'GitLab'],
212
+ {},
213
+ { autoSync: true, syncInterval: 12 }
214
+ );
215
+
216
+ const configWrites = fs.writeFileSync.mock.calls.filter(
217
+ call => call[0] === 'push-guardian.config.json'
218
+ );
219
+ const savedConfig = JSON.parse(configWrites[0][1]);
220
+
221
+ expect(savedConfig.mirroring.enabled).toBe(true);
222
+ expect(savedConfig.mirroring.platforms.github.enabled).toBe(true);
223
+ expect(savedConfig.mirroring.platforms.gitlab.enabled).toBe(true);
224
+ expect(savedConfig.mirroring.platforms.bitbucket.enabled).toBe(false);
225
+ expect(savedConfig.mirroring.defaultSettings.autoSync).toBe(true);
226
+ expect(savedConfig.mirroring.defaultSettings.syncInterval).toBe(12);
227
+ });
228
+
229
+ test('doit utiliser paramètres par défaut', () => {
230
+ mirroring.createMirroringConfig(['GitHub'], {}, {});
231
+
232
+ const configWrites = fs.writeFileSync.mock.calls.filter(
233
+ call => call[0] === 'push-guardian.config.json'
234
+ );
235
+ const savedConfig = JSON.parse(configWrites[0][1]);
236
+
237
+ expect(savedConfig.mirroring.defaultSettings.autoSync).toBe(false);
238
+ expect(savedConfig.mirroring.defaultSettings.syncInterval).toBe(24);
239
+ expect(savedConfig.mirroring.defaultSettings.includeBranches).toBe(true);
240
+ expect(savedConfig.mirroring.defaultSettings.includeTags).toBe(true);
241
+ });
242
+
243
+ test('doit fusionner avec config existante', () => {
244
+ const existingConfig = {
245
+ hooks: { 'commit-msg': {} },
246
+ install: { hooks: true }
247
+ };
248
+ fs.existsSync.mockReturnValue(true);
249
+ fs.readFileSync.mockReturnValue(JSON.stringify(existingConfig));
250
+
251
+ mirroring.createMirroringConfig(['GitHub'], {}, {});
252
+
253
+ const configWrites = fs.writeFileSync.mock.calls.filter(
254
+ call => call[0] === 'push-guardian.config.json'
255
+ );
256
+ const savedConfig = JSON.parse(configWrites[0][1]);
257
+
258
+ expect(savedConfig.hooks).toEqual({ 'commit-msg': {} });
259
+ expect(savedConfig.install.hooks).toBe(true);
260
+ expect(savedConfig.install.mirroring).toBe(true);
261
+ });
262
+
263
+ test('doit gérer erreur écriture fichier', () => {
264
+ fs.writeFileSync.mockImplementation(() => {
265
+ throw new Error('Permission denied');
266
+ });
267
+
268
+ mirroring.createMirroringConfig(['GitHub'], {}, {});
269
+
270
+ expect(console.log).toHaveBeenCalledWith(
271
+ expect.stringContaining('Erreur lors de la création de la configuration du mirroring:'),
272
+ 'Permission denied'
273
+ );
274
+ });
275
+
276
+ test('doit afficher message succès', () => {
277
+ mirroring.createMirroringConfig(['GitHub'], {}, {});
278
+
279
+ expect(console.log).toHaveBeenCalledWith(
280
+ expect.stringContaining('Configuration du système de mirroring mise à jour')
281
+ );
282
+ });
283
+
284
+ test('doit gérer config JSON invalide', () => {
285
+ fs.existsSync.mockReturnValue(true);
286
+ fs.readFileSync.mockReturnValue('invalid json {');
287
+
288
+ mirroring.createMirroringConfig(['GitHub'], {}, {});
289
+
290
+ const configWrites = fs.writeFileSync.mock.calls.filter(
291
+ call => call[0] === 'push-guardian.config.json'
292
+ );
293
+ expect(configWrites.length).toBeGreaterThan(0);
294
+ });
295
+
296
+ test('doit inclure toutes les plateformes dans config', () => {
297
+ mirroring.createMirroringConfig(['GitHub'], {}, {});
298
+
299
+ const configWrites = fs.writeFileSync.mock.calls.filter(
300
+ call => call[0] === 'push-guardian.config.json'
301
+ );
302
+ const savedConfig = JSON.parse(configWrites[0][1]);
303
+
304
+ expect(savedConfig.mirroring.platforms.github).toBeDefined();
305
+ expect(savedConfig.mirroring.platforms.gitlab).toBeDefined();
306
+ expect(savedConfig.mirroring.platforms.bitbucket).toBeDefined();
307
+ expect(savedConfig.mirroring.platforms.azure).toBeDefined();
308
+ });
309
+
310
+ test('doit activer uniquement plateformes sélectionnées', () => {
311
+ mirroring.createMirroringConfig(['GitLab'], {}, {});
312
+
313
+ const configWrites = fs.writeFileSync.mock.calls.filter(
314
+ call => call[0] === 'push-guardian.config.json'
315
+ );
316
+ const savedConfig = JSON.parse(configWrites[0][1]);
317
+
318
+ expect(savedConfig.mirroring.platforms.gitlab.enabled).toBe(true);
319
+ expect(savedConfig.mirroring.platforms.github.enabled).toBe(false);
320
+ expect(savedConfig.mirroring.platforms.bitbucket.enabled).toBe(false);
321
+ });
322
+
323
+ test('doit ignorer une plateforme inconnue', () => {
324
+ mirroring.createMirroringConfig(['Unknown Platform'], {}, {});
325
+
326
+ const configWrites = fs.writeFileSync.mock.calls.filter(
327
+ call => call[0] === 'push-guardian.config.json'
328
+ );
329
+ const savedConfig = JSON.parse(configWrites[0][1]);
330
+
331
+ expect(savedConfig.mirroring.platforms.github.enabled).toBe(false);
332
+ expect(savedConfig.mirroring.platforms.gitlab.enabled).toBe(false);
333
+ expect(savedConfig.mirroring.platforms.bitbucket.enabled).toBe(false);
334
+ expect(savedConfig.mirroring.platforms.azure.enabled).toBe(false);
335
+ });
336
+ });
337
+
338
+ describe('askCredentials', () => {
339
+ let mockRl;
340
+
341
+ beforeEach(() => {
342
+ const readline = require('readline');
343
+ mockRl = {
344
+ question: jest.fn(),
345
+ close: jest.fn()
346
+ };
347
+ readline.createInterface.mockReturnValue(mockRl);
348
+ });
349
+
350
+ test('doit demander token pour GitHub', async () => {
351
+ mockRl.question.mockImplementation((question, callback) => {
352
+ callback('test_github_token');
353
+ });
354
+
355
+ const promise = mirroring.askCredentials('github');
356
+ const result = await promise;
357
+
358
+ expect(result).toEqual({ token: 'test_github_token' });
359
+ expect(mockRl.question).toHaveBeenCalledWith(
360
+ expect.stringContaining('token GitHub'),
361
+ expect.any(Function)
362
+ );
363
+ expect(mockRl.close).toHaveBeenCalled();
364
+ });
365
+
366
+ test('doit demander token pour GitLab', async () => {
367
+ mockRl.question.mockImplementation((question, callback) => {
368
+ callback('test_gitlab_token');
369
+ });
370
+
371
+ const result = await mirroring.askCredentials('gitlab');
372
+
373
+ expect(result).toEqual({ token: 'test_gitlab_token' });
374
+ expect(mockRl.question).toHaveBeenCalledWith(
375
+ expect.stringContaining('token GitLab'),
376
+ expect.any(Function)
377
+ );
378
+ });
379
+
380
+ test('doit demander username et password pour BitBucket', async () => {
381
+ let questionCount = 0;
382
+ mockRl.question.mockImplementation((question, callback) => {
383
+ questionCount++;
384
+ if (questionCount === 1) {
385
+ callback('bitbucket_user');
386
+ } else {
387
+ callback('bitbucket_pass');
388
+ }
389
+ });
390
+
391
+ const result = await mirroring.askCredentials('bitbucket');
392
+
393
+ expect(result).toEqual({
394
+ username: 'bitbucket_user',
395
+ password: 'bitbucket_pass'
396
+ });
397
+ expect(mockRl.question).toHaveBeenCalledTimes(2);
398
+ });
399
+
400
+ test('doit demander URL et token pour Azure DevOps', async () => {
401
+ let questionCount = 0;
402
+ mockRl.question.mockImplementation((question, callback) => {
403
+ questionCount++;
404
+ if (questionCount === 1) {
405
+ callback('https://dev.azure.com/org');
406
+ } else {
407
+ callback('azure_token');
408
+ }
409
+ });
410
+
411
+ const result = await mirroring.askCredentials('azure');
412
+
413
+ expect(result).toEqual({
414
+ url: 'https://dev.azure.com/org',
415
+ token: 'azure_token'
416
+ });
417
+ expect(mockRl.question).toHaveBeenCalledTimes(2);
418
+ });
419
+
420
+ test('doit retourner objet vide pour plateforme inconnue', async () => {
421
+ const result = await mirroring.askCredentials('unknown');
422
+
423
+ expect(result).toEqual({});
424
+ expect(mockRl.close).toHaveBeenCalled();
425
+ expect(mockRl.question).not.toHaveBeenCalled();
426
+ });
427
+
428
+ test('doit gérer plateforme avec espaces', async () => {
429
+ let questionCount = 0;
430
+ mockRl.question.mockImplementation((question, callback) => {
431
+ questionCount++;
432
+ if (questionCount === 1) {
433
+ callback('https://dev.azure.com/test');
434
+ } else {
435
+ callback('test_token');
436
+ }
437
+ });
438
+
439
+ const result = await mirroring.askCredentials('azure');
440
+
441
+ expect(result).toEqual({
442
+ url: 'https://dev.azure.com/test',
443
+ token: 'test_token'
444
+ });
445
+ });
446
+ });
447
+
448
+ describe('saveCredentialsToEnv - edge cases', () => {
449
+ test('doit gérer credentials GitLab', () => {
450
+ getEnv.mockReturnValue('');
451
+
452
+ mirroring.saveCredentialsToEnv({
453
+ gitlab: { token: 'gitlab_token_123' }
454
+ });
455
+
456
+ expect(saveEnv).toHaveBeenCalledWith('GITLAB_TOKEN', 'gitlab_token_123', '.env');
457
+ });
458
+
459
+ test('ne doit pas sauvegarder si pas de token', () => {
460
+ getEnv.mockReturnValue('');
461
+
462
+ mirroring.saveCredentialsToEnv({
463
+ github: {}
464
+ });
465
+
466
+ expect(saveEnv).not.toHaveBeenCalledWith('GITHUB_TOKEN', expect.anything(), expect.anything());
467
+ });
468
+
469
+ test('ne doit pas écraser credentials BitBucket existants', () => {
470
+ getEnv.mockImplementation((key) => {
471
+ if (key === 'BITBUCKET_USERNAME') return 'existing_user';
472
+ return '';
473
+ });
474
+
475
+ mirroring.saveCredentialsToEnv({
476
+ bitbucket: { username: 'new_user', password: 'new_pass' }
477
+ });
478
+
479
+ expect(saveEnv).not.toHaveBeenCalledWith('BITBUCKET_USERNAME', expect.anything(), expect.anything());
480
+ });
481
+
482
+ test('ne doit pas écraser credentials Azure existants', () => {
483
+ getEnv.mockImplementation((key) => {
484
+ if (key === 'AZURE_DEVOPS_URL') return 'https://existing.com';
485
+ return '';
486
+ });
487
+
488
+ mirroring.saveCredentialsToEnv({
489
+ azure: { url: 'https://new.com', token: 'new_token' }
490
+ });
491
+
492
+ expect(saveEnv).not.toHaveBeenCalledWith('AZURE_DEVOPS_URL', expect.anything(), expect.anything());
493
+ });
494
+
495
+ test('doit afficher message de log pour chaque plateforme', () => {
496
+ getEnv.mockReturnValue('');
497
+
498
+ mirroring.saveCredentialsToEnv({
499
+ github: { token: 'gh_token' },
500
+ gitlab: { token: 'gl_token' }
501
+ });
502
+
503
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Sauvegarde des credentials pour github'));
504
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Sauvegarde des credentials pour gitlab'));
505
+ });
506
+
507
+ test('doit gérer bitbucket partiel (username sans password)', () => {
508
+ getEnv.mockReturnValue('');
509
+
510
+ mirroring.saveCredentialsToEnv({
511
+ bitbucket: { username: 'partial_user' }
512
+ });
513
+
514
+ expect(saveEnv).toHaveBeenCalledWith('BITBUCKET_USERNAME', 'partial_user', '.env');
515
+ expect(saveEnv).not.toHaveBeenCalledWith('BITBUCKET_PASSWORD', expect.anything(), '.env');
516
+ });
517
+
518
+ test('doit gérer azure partiel (url sans token)', () => {
519
+ getEnv.mockReturnValue('');
520
+
521
+ mirroring.saveCredentialsToEnv({
522
+ azure: { url: 'https://dev.azure.com/partial' }
523
+ });
524
+
525
+ expect(saveEnv).toHaveBeenCalledWith('AZURE_DEVOPS_URL', 'https://dev.azure.com/partial', '.env');
526
+ expect(saveEnv).not.toHaveBeenCalledWith('AZURE_DEVOPS_TOKEN', expect.anything(), '.env');
527
+ });
528
+
529
+ test('ne doit rien sauvegarder si bitbucket est vide', () => {
530
+ getEnv.mockReturnValue('');
531
+
532
+ mirroring.saveCredentialsToEnv({
533
+ bitbucket: {}
534
+ });
535
+
536
+ expect(saveEnv).not.toHaveBeenCalledWith('BITBUCKET_USERNAME', expect.anything(), '.env');
537
+ expect(saveEnv).not.toHaveBeenCalledWith('BITBUCKET_PASSWORD', expect.anything(), '.env');
538
+ });
539
+
540
+ test('ne doit rien sauvegarder si azure est vide', () => {
541
+ getEnv.mockReturnValue('');
542
+
543
+ mirroring.saveCredentialsToEnv({
544
+ azure: {}
545
+ });
546
+
547
+ expect(saveEnv).not.toHaveBeenCalledWith('AZURE_DEVOPS_URL', expect.anything(), '.env');
548
+ expect(saveEnv).not.toHaveBeenCalledWith('AZURE_DEVOPS_TOKEN', expect.anything(), '.env');
549
+ });
550
+ });
551
+
552
+ describe('installMirroringTools', () => {
553
+ let mockRl;
554
+
555
+ beforeEach(() => {
556
+ const readline = require('readline');
557
+ mockRl = {
558
+ question: jest.fn(),
559
+ close: jest.fn()
560
+ };
561
+ readline.createInterface.mockReturnValue(mockRl);
562
+ });
563
+
564
+ test('doit afficher message si déjà installé', async () => {
565
+ fs.existsSync.mockReturnValue(true);
566
+ fs.readFileSync.mockReturnValue(JSON.stringify({
567
+ install: { mirroring: true }
568
+ }));
569
+
570
+ await mirroring.installMirroringTools();
571
+
572
+ expect(console.log).toHaveBeenCalledWith(
573
+ expect.stringContaining('Le système de mirroring est déjà installé')
574
+ );
575
+ });
576
+
577
+ test('doit afficher message si aucune plateforme sélectionnée', async () => {
578
+ interactiveMenu.mockResolvedValue([]);
579
+
580
+ await mirroring.installMirroringTools();
581
+
582
+ expect(console.log).toHaveBeenCalledWith(
583
+ expect.stringContaining('Aucune plateforme sélectionnée')
584
+ );
585
+ });
586
+
587
+ test('doit configurer une seule plateforme', async () => {
588
+ interactiveMenu.mockResolvedValue(['GitHub']);
589
+ getEnv.mockReturnValue('test_token');
590
+
591
+ // Mock askForDefaults pour répondre avec plateformes valides puis réponses vides
592
+ let questionCount = 0;
593
+ mockRl.question.mockImplementation((question, callback) => {
594
+ questionCount++;
595
+ if (questionCount === 1) callback('github'); // source platform
596
+ else if (questionCount === 2) callback('gitlab'); // target platform
597
+ else callback(''); // réponses vides pour les questions optionnelles
598
+ });
599
+
600
+ await mirroring.installMirroringTools();
601
+
602
+ expect(interactiveMenu).toHaveBeenCalled();
603
+ const configWrites = fs.writeFileSync.mock.calls.filter(
604
+ call => call[0] === 'push-guardian.config.json'
605
+ );
606
+ expect(configWrites.length).toBeGreaterThan(0);
607
+ });
608
+
609
+ test('doit configurer plusieurs plateformes', async () => {
610
+ interactiveMenu.mockResolvedValue(['GitHub', 'GitLab']);
611
+ getEnv.mockImplementation((key) => {
612
+ if (key === 'GITHUB_TOKEN') return 'github_token';
613
+ if (key === 'GITLAB_TOKEN') return 'gitlab_token';
614
+ return '';
615
+ });
616
+
617
+ // Mock askForDefaults pour répondre avec plateformes valides puis réponses vides
618
+ let questionCount = 0;
619
+ mockRl.question.mockImplementation((question, callback) => {
620
+ questionCount++;
621
+ if (questionCount === 1) callback('github');
622
+ else if (questionCount === 2) callback('gitlab');
623
+ else callback('');
624
+ });
625
+
626
+ await mirroring.installMirroringTools();
627
+
628
+ const configWrites = fs.writeFileSync.mock.calls.filter(
629
+ call => call[0] === 'push-guardian.config.json'
630
+ );
631
+ expect(configWrites.length).toBeGreaterThan(0);
632
+ });
633
+
634
+ test('doit appeler interactiveMenu avec bonnes options', async () => {
635
+ // Mock askForDefaults
636
+ let questionCount = 0;
637
+ mockRl.question.mockImplementation((question, callback) => {
638
+ questionCount++;
639
+ if (questionCount === 1) callback('github');
640
+ else if (questionCount === 2) callback('gitlab');
641
+ else callback('');
642
+ });
643
+
644
+ await mirroring.installMirroringTools();
645
+
646
+ expect(interactiveMenu).toHaveBeenCalledWith(
647
+ 'Choisissez les plateformes à activer pour le mirroring:',
648
+ ['GitHub', 'GitLab', 'BitBucket', 'Azure DevOps']
649
+ );
650
+ });
651
+
652
+ test('doit redemander une plateforme par défaut invalide', async () => {
653
+ interactiveMenu.mockResolvedValue(['GitHub']);
654
+ getEnv.mockReturnValue('test_token');
655
+
656
+ const answers = ['invalid-platform', 'github', 'gitlab', '', '', '', ''];
657
+ mockRl.question.mockImplementation((question, callback) => {
658
+ callback(answers.shift() || '');
659
+ });
660
+
661
+ await mirroring.installMirroringTools();
662
+
663
+ expect(console.log).toHaveBeenCalledWith(
664
+ expect.stringContaining('Valeur invalide. Plateformes supportées')
665
+ );
666
+ });
667
+
668
+ test('doit sauvegarder toutes les valeurs par défaut non vides dans .env', async () => {
669
+ interactiveMenu.mockResolvedValue(['GitHub']);
670
+ getEnv.mockReturnValue('test_token');
671
+
672
+ const answers = ['github', 'gitlab', 'repo-src', 'owner-src', 'repo-dst', 'owner-dst'];
673
+ mockRl.question.mockImplementation((question, callback) => {
674
+ callback(answers.shift() || '');
675
+ });
676
+
677
+ await mirroring.installMirroringTools();
678
+
679
+ expect(saveEnv).toHaveBeenCalledWith('SOURCE_PLATFORM', 'github');
680
+ expect(saveEnv).toHaveBeenCalledWith('TARGET_PLATFORM', 'gitlab');
681
+ expect(saveEnv).toHaveBeenCalledWith('SOURCE_REPO', 'repo-src');
682
+ expect(saveEnv).toHaveBeenCalledWith('SOURCE_OWNER', 'owner-src');
683
+ expect(saveEnv).toHaveBeenCalledWith('TARGET_REPO', 'repo-dst');
684
+ expect(saveEnv).toHaveBeenCalledWith('TARGET_OWNER', 'owner-dst');
685
+ });
686
+
687
+ test('ne doit pas sauvegarder de valeur par défaut vide', async () => {
688
+ interactiveMenu.mockResolvedValue(['GitHub']);
689
+ getEnv.mockReturnValue('test_token');
690
+
691
+ const answers = ['github', 'gitlab', '', '', '', ''];
692
+ mockRl.question.mockImplementation((question, callback) => {
693
+ callback(answers.shift() || '');
694
+ });
695
+
696
+ await mirroring.installMirroringTools();
697
+
698
+ expect(saveEnv).toHaveBeenCalledWith('SOURCE_PLATFORM', 'github');
699
+ expect(saveEnv).toHaveBeenCalledWith('TARGET_PLATFORM', 'gitlab');
700
+ expect(saveEnv).not.toHaveBeenCalledWith('SOURCE_REPO', expect.anything());
701
+ expect(saveEnv).not.toHaveBeenCalledWith('TARGET_OWNER', expect.anything());
702
+ });
703
+
704
+ test('doit continuer si déjà installé mais install.mirroring=false', async () => {
705
+ fs.existsSync.mockReturnValue(true);
706
+ fs.readFileSync.mockReturnValue(JSON.stringify({ install: { mirroring: false } }));
707
+ interactiveMenu.mockResolvedValue([]);
708
+
709
+ await mirroring.installMirroringTools();
710
+
711
+ expect(console.log).toHaveBeenCalledWith(
712
+ expect.stringContaining('Aucune plateforme sélectionnée')
713
+ );
714
+ });
715
+
716
+ test('doit gérer des réponses vides pour toutes les valeurs par défaut', async () => {
717
+ interactiveMenu.mockResolvedValue(['GitHub']);
718
+ getEnv.mockReturnValue('test_token');
719
+
720
+ const answers = ['', '', '', '', '', ''];
721
+ mockRl.question.mockImplementation((question, callback) => {
722
+ callback(answers.shift() || '');
723
+ });
724
+
725
+ await mirroring.installMirroringTools();
726
+
727
+ expect(saveEnv).not.toHaveBeenCalledWith('SOURCE_PLATFORM', expect.anything());
728
+ expect(saveEnv).not.toHaveBeenCalledWith('TARGET_PLATFORM', expect.anything());
729
+ });
730
+ });
731
+ });