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.
- package/.dockerignore +15 -0
- package/.pushguardian-plugins.json +10 -0
- package/Dockerfile +41 -0
- package/Dockerfile.dev +20 -0
- package/README.md +386 -0
- package/TECHNO.md +139 -0
- package/babel.config.js +1 -0
- package/developper_utils.md +119 -0
- package/docker-compose.yml +41 -0
- package/docs/PLUGINS.md +223 -0
- package/docs/technical/architecture.md +298 -0
- package/docs/technical/performance-guide.md +390 -0
- package/docs/technical/plugin-persistence.md +169 -0
- package/docs/technical/plugins-guide.md +409 -0
- package/jest.config.js +22 -0
- package/package.json +53 -0
- package/plugins/example-plugin/index.js +55 -0
- package/plugins/example-plugin/plugin.json +8 -0
- package/scripts/coverage-report.js +75 -0
- package/src/cli/command/config.js +33 -0
- package/src/cli/command/install.js +137 -0
- package/src/cli/command/mirror.js +90 -0
- package/src/cli/command/performance.js +160 -0
- package/src/cli/command/plugin.js +171 -0
- package/src/cli/command/security.js +152 -0
- package/src/cli/command/shell.js +238 -0
- package/src/cli/command/validate.js +54 -0
- package/src/cli/index.js +23 -0
- package/src/cli/install/codeQualityTools.js +156 -0
- package/src/cli/install/hooks.js +89 -0
- package/src/cli/install/mirroring.js +299 -0
- package/src/core/codeQualityTools/configAnalyzer.js +216 -0
- package/src/core/codeQualityTools/configGenerator.js +381 -0
- package/src/core/codeQualityTools/configManager.js +65 -0
- package/src/core/codeQualityTools/fileDetector.js +62 -0
- package/src/core/codeQualityTools/languageTools.js +104 -0
- package/src/core/codeQualityTools/toolInstaller.js +53 -0
- package/src/core/configManager.js +43 -0
- package/src/core/errorCMD.js +9 -0
- package/src/core/interactiveMenu/interactiveMenu.js +73 -0
- package/src/core/mirroring/branchSynchronizer.js +59 -0
- package/src/core/mirroring/generate.js +114 -0
- package/src/core/mirroring/repoManager.js +112 -0
- package/src/core/mirroring/syncManager.js +176 -0
- package/src/core/module/env-loader.js +109 -0
- package/src/core/performance/metricsCollector.js +217 -0
- package/src/core/performance/performanceAnalyzer.js +182 -0
- package/src/core/plugins/basePlugin.js +89 -0
- package/src/core/plugins/pluginManager.js +123 -0
- package/src/core/plugins/pluginRegistry.js +215 -0
- package/src/core/validator.js +53 -0
- package/src/hooks/constrains/constrains.js +174 -0
- package/src/hooks/constrains/constraintEngine.js +140 -0
- package/src/utils/chalk-wrapper.js +26 -0
- package/src/utils/exec-wrapper.js +6 -0
- package/tests/fixtures/mock-eslint-config-array.js +8 -0
- package/tests/fixtures/mock-eslint-config-single.js +6 -0
- package/tests/fixtures/mockLoadedPlugin.js +11 -0
- package/tests/setup.js +28 -0
- package/tests/unit/basePlugin.test.js +355 -0
- package/tests/unit/branchSynchronizer.test.js +308 -0
- package/tests/unit/cli-commands.test.js +144 -0
- package/tests/unit/codeQualityConfigManager.test.js +233 -0
- package/tests/unit/codeQualityTools.test.js +36 -0
- package/tests/unit/command-install.test.js +247 -0
- package/tests/unit/command-mirror.test.js +179 -0
- package/tests/unit/command-performance.test.js +169 -0
- package/tests/unit/command-plugin.test.js +288 -0
- package/tests/unit/command-security.test.js +277 -0
- package/tests/unit/command-shell.test.js +325 -0
- package/tests/unit/configAnalyzer.test.js +593 -0
- package/tests/unit/configGenerator.test.js +808 -0
- package/tests/unit/configManager.test.js +195 -0
- package/tests/unit/constrains.test.js +463 -0
- package/tests/unit/constraint.test.js +554 -0
- package/tests/unit/env-loader.test.js +279 -0
- package/tests/unit/fileDetector.test.js +171 -0
- package/tests/unit/install-codeQualityTools.test.js +343 -0
- package/tests/unit/install-hooks.test.js +280 -0
- package/tests/unit/install-mirroring.test.js +731 -0
- package/tests/unit/install-modules.test.js +81 -0
- package/tests/unit/interactiveMenu.test.js +426 -0
- package/tests/unit/languageTools.test.js +244 -0
- package/tests/unit/metricsCollector.test.js +354 -0
- package/tests/unit/mirroring-generate.test.js +96 -0
- package/tests/unit/modules-exist.test.js +96 -0
- package/tests/unit/performanceAnalyzer.test.js +473 -0
- package/tests/unit/pluginManager.test.js +427 -0
- package/tests/unit/pluginRegistry.test.js +592 -0
- package/tests/unit/repoManager.test.js +469 -0
- package/tests/unit/reviewAppManager.test.js +5 -0
- package/tests/unit/security-command.test.js +43 -0
- package/tests/unit/syncManager.test.js +494 -0
- package/tests/unit/toolInstaller.test.js +240 -0
- package/tests/unit/utils.test.js +144 -0
- package/tests/unit/validator.test.js +215 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
jest.mock('fs');
|
|
5
|
+
|
|
6
|
+
const configManager = require('../../src/core/configManager');
|
|
7
|
+
|
|
8
|
+
describe('Core - configManager', () => {
|
|
9
|
+
const mockConfigPath = path.resolve(process.cwd(), 'push-guardian.config.json');
|
|
10
|
+
const originalConsoleError = console.error;
|
|
11
|
+
const originalProcessExit = process.exit;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
console.error = jest.fn();
|
|
16
|
+
console.log = jest.fn();
|
|
17
|
+
process.exit = jest.fn();
|
|
18
|
+
|
|
19
|
+
fs.existsSync.mockReturnValue(false);
|
|
20
|
+
fs.readFileSync.mockReturnValue('{}');
|
|
21
|
+
fs.writeFileSync.mockImplementation(() => {});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
console.error = originalConsoleError;
|
|
26
|
+
process.exit = originalProcessExit;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('loadConfig', () => {
|
|
30
|
+
test('doit créer fichier config si inexistant', () => {
|
|
31
|
+
fs.existsSync.mockReturnValue(false);
|
|
32
|
+
|
|
33
|
+
const config = configManager.loadConfig();
|
|
34
|
+
|
|
35
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
36
|
+
mockConfigPath,
|
|
37
|
+
JSON.stringify({}, null, 4)
|
|
38
|
+
);
|
|
39
|
+
expect(config).toEqual({});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('doit charger config existante', () => {
|
|
43
|
+
const mockConfig = { hooks: { 'commit-msg': {} } };
|
|
44
|
+
fs.existsSync.mockReturnValue(true);
|
|
45
|
+
fs.readFileSync.mockReturnValue(JSON.stringify(mockConfig));
|
|
46
|
+
|
|
47
|
+
const config = configManager.loadConfig();
|
|
48
|
+
|
|
49
|
+
expect(fs.readFileSync).toHaveBeenCalledWith(mockConfigPath, 'utf-8');
|
|
50
|
+
expect(config).toEqual(mockConfig);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('doit gérer erreur création fichier', () => {
|
|
54
|
+
fs.existsSync.mockReturnValue(false);
|
|
55
|
+
fs.writeFileSync.mockImplementation(() => {
|
|
56
|
+
throw new Error('Permission denied');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
configManager.loadConfig();
|
|
60
|
+
|
|
61
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
62
|
+
expect.stringContaining('Erreur lors de la création du fichier de configuration:'),
|
|
63
|
+
'Permission denied'
|
|
64
|
+
);
|
|
65
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('doit gérer erreur lecture fichier', () => {
|
|
69
|
+
fs.existsSync.mockReturnValue(true);
|
|
70
|
+
fs.readFileSync.mockImplementation(() => {
|
|
71
|
+
throw new Error('Cannot read file');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
configManager.loadConfig();
|
|
75
|
+
|
|
76
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
77
|
+
expect.stringContaining('Erreur lors du chargement de la configuration:'),
|
|
78
|
+
'Cannot read file'
|
|
79
|
+
);
|
|
80
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('doit gérer JSON invalide', () => {
|
|
84
|
+
fs.existsSync.mockReturnValue(true);
|
|
85
|
+
fs.readFileSync.mockReturnValue('{ invalid json }');
|
|
86
|
+
|
|
87
|
+
configManager.loadConfig();
|
|
88
|
+
|
|
89
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
90
|
+
expect.stringContaining('Erreur lors du chargement de la configuration:'),
|
|
91
|
+
expect.anything()
|
|
92
|
+
);
|
|
93
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('doit accepter chemin personnalisé', () => {
|
|
97
|
+
const customPath = '/custom/path/config.json';
|
|
98
|
+
fs.existsSync.mockReturnValue(true);
|
|
99
|
+
fs.readFileSync.mockReturnValue('{"custom": true}');
|
|
100
|
+
|
|
101
|
+
const config = configManager.loadConfig(customPath);
|
|
102
|
+
|
|
103
|
+
expect(fs.readFileSync).toHaveBeenCalledWith(customPath, 'utf-8');
|
|
104
|
+
expect(config).toEqual({ custom: true });
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('saveConfig', () => {
|
|
109
|
+
test('doit sauvegarder nouvelle config', () => {
|
|
110
|
+
const newConfig = { hooks: { 'pre-push': {} } };
|
|
111
|
+
|
|
112
|
+
configManager.saveConfig(newConfig);
|
|
113
|
+
|
|
114
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
115
|
+
mockConfigPath,
|
|
116
|
+
JSON.stringify(newConfig, null, 2)
|
|
117
|
+
);
|
|
118
|
+
expect(console.log).toHaveBeenCalledWith('✅ Configuration mise à jour avec succès.');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('doit gérer erreur sauvegarde', () => {
|
|
122
|
+
fs.writeFileSync.mockImplementation(() => {
|
|
123
|
+
throw new Error('Disk full');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
configManager.saveConfig({ test: true });
|
|
127
|
+
|
|
128
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
129
|
+
expect.stringContaining('Erreur lors de la sauvegarde de la configuration:'),
|
|
130
|
+
'Disk full'
|
|
131
|
+
);
|
|
132
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('doit formater JSON avec indentation', () => {
|
|
136
|
+
const complexConfig = {
|
|
137
|
+
hooks: {
|
|
138
|
+
'commit-msg': { type: ['ADD', 'FIX'] }
|
|
139
|
+
},
|
|
140
|
+
validate: {
|
|
141
|
+
directories: ['src/']
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
configManager.saveConfig(complexConfig);
|
|
146
|
+
|
|
147
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
148
|
+
mockConfigPath,
|
|
149
|
+
JSON.stringify(complexConfig, null, 2)
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('doit gérer config vide', () => {
|
|
154
|
+
configManager.saveConfig({});
|
|
155
|
+
|
|
156
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
157
|
+
mockConfigPath,
|
|
158
|
+
'{}'
|
|
159
|
+
);
|
|
160
|
+
expect(console.log).toHaveBeenCalledWith('✅ Configuration mise à jour avec succès.');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('bootstrap path guard', () => {
|
|
165
|
+
test('doit quitter si CONFIG_PATH n\'est pas dans le projet', () => {
|
|
166
|
+
jest.resetModules();
|
|
167
|
+
|
|
168
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
169
|
+
const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
170
|
+
const cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue('/home/project');
|
|
171
|
+
|
|
172
|
+
jest.doMock('path', () => {
|
|
173
|
+
const actual = jest.requireActual('path');
|
|
174
|
+
return {
|
|
175
|
+
...actual,
|
|
176
|
+
resolve: jest.fn(() => '/tmp/outside/push-guardian.config.json')
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
jest.isolateModules(() => {
|
|
181
|
+
require('../../src/core/configManager');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
185
|
+
'❌ Le fichier de configuration doit être situé à la racine du projet.'
|
|
186
|
+
);
|
|
187
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
188
|
+
|
|
189
|
+
cwdSpy.mockRestore();
|
|
190
|
+
consoleErrorSpy.mockRestore();
|
|
191
|
+
processExitSpy.mockRestore();
|
|
192
|
+
jest.dontMock('path');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
const { constrains, validateCommitMessage, validateBranchName, validatePrePush } = require('../../src/hooks/constrains/constrains');
|
|
2
|
+
const { loadConfig } = require('../../src/core/configManager');
|
|
3
|
+
const { constraintEngine } = require('../../src/hooks/constrains/constraintEngine');
|
|
4
|
+
const { execa } = require('execa');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
jest.mock('../../src/core/configManager');
|
|
8
|
+
jest.mock('execa');
|
|
9
|
+
jest.mock('fs');
|
|
10
|
+
|
|
11
|
+
describe('Hooks - constrains', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
15
|
+
jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
console.log.mockRestore();
|
|
20
|
+
process.exit.mockRestore();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('constrains', () => {
|
|
24
|
+
test('doit retourner success si aucune config de hooks', async () => {
|
|
25
|
+
loadConfig.mockReturnValue({ hooks: {} });
|
|
26
|
+
|
|
27
|
+
const result = await constrains('commit-msg', 'test message');
|
|
28
|
+
|
|
29
|
+
expect(result.success).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('doit valider un message de commit correct', async () => {
|
|
33
|
+
loadConfig.mockReturnValue({
|
|
34
|
+
hooks: {
|
|
35
|
+
'commit-msg': {
|
|
36
|
+
type: ['feat', 'fix'],
|
|
37
|
+
constraints: {}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const result = await constrains('commit-msg', '[feat]: add new feature');
|
|
43
|
+
|
|
44
|
+
expect(result.success).toBe(true);
|
|
45
|
+
expect(process.exit).not.toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('doit rejeter un message de commit incorrect', async () => {
|
|
49
|
+
loadConfig.mockReturnValue({
|
|
50
|
+
hooks: {
|
|
51
|
+
'commit-msg': {
|
|
52
|
+
type: ['feat', 'fix'],
|
|
53
|
+
constraints: {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await constrains('commit-msg', '[invalid]: bad message');
|
|
59
|
+
|
|
60
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('ne respecte pas les contraintes'));
|
|
61
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('doit valider un message avec contraintes', async () => {
|
|
65
|
+
loadConfig.mockReturnValue({
|
|
66
|
+
hooks: {
|
|
67
|
+
'commit-msg': {
|
|
68
|
+
type: ['feat'],
|
|
69
|
+
constraints: {
|
|
70
|
+
minLength: 10
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const result = await constrains('commit-msg', '[feat]: this is a long enough message');
|
|
77
|
+
|
|
78
|
+
expect(result.success).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('doit rejeter un message qui ne respecte pas les contraintes', async () => {
|
|
82
|
+
loadConfig.mockReturnValue({
|
|
83
|
+
hooks: {
|
|
84
|
+
'commit-msg': {
|
|
85
|
+
type: ['feat'],
|
|
86
|
+
constraints: {
|
|
87
|
+
minLength: 100
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await constrains('commit-msg', '[feat]: short');
|
|
94
|
+
|
|
95
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('doit ignorer une config hook nulle', async () => {
|
|
99
|
+
loadConfig.mockReturnValue({
|
|
100
|
+
hooks: {
|
|
101
|
+
'commit-msg': null
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const result = await constrains('commit-msg', '[feat]: ok');
|
|
106
|
+
|
|
107
|
+
expect(result.success).toBe(true);
|
|
108
|
+
expect(process.exit).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('validateCommitMessage', () => {
|
|
113
|
+
test('doit valider un message correctement formaté', async () => {
|
|
114
|
+
const validationInfo = {
|
|
115
|
+
msg: '[feat]: add new feature',
|
|
116
|
+
type: ['feat', 'fix'],
|
|
117
|
+
constraints: {}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const result = await validateCommitMessage(validationInfo);
|
|
121
|
+
|
|
122
|
+
expect(result.isValid).toBe(true);
|
|
123
|
+
expect(result.errors).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('doit extraire la description du message', async () => {
|
|
127
|
+
const validationInfo = {
|
|
128
|
+
msg: '[feat]: add new feature',
|
|
129
|
+
type: ['feat', 'fix'],
|
|
130
|
+
constraints: {}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const result = await validateCommitMessage(validationInfo);
|
|
134
|
+
|
|
135
|
+
expect(result.description).toBeTruthy();
|
|
136
|
+
expect(result.description.includes('add new feature')).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('doit rejeter un message mal formaté', async () => {
|
|
140
|
+
const validationInfo = {
|
|
141
|
+
msg: 'feat add new feature',
|
|
142
|
+
type: ['feat', 'fix'],
|
|
143
|
+
constraints: {}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const result = await validateCommitMessage(validationInfo);
|
|
147
|
+
|
|
148
|
+
expect(result.isValid).toBe(false);
|
|
149
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('doit rejeter un type non autorisé', async () => {
|
|
153
|
+
const validationInfo = {
|
|
154
|
+
msg: '[invalid]: add new feature',
|
|
155
|
+
type: ['feat', 'fix'],
|
|
156
|
+
constraints: {}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const result = await validateCommitMessage(validationInfo);
|
|
160
|
+
|
|
161
|
+
expect(result.isValid).toBe(false);
|
|
162
|
+
expect(result.errors).toContain('Le type "invalid" n\'est pas valide. Types autorisés: feat, fix');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('doit accepter tous les types si la liste est vide', async () => {
|
|
166
|
+
const validationInfo = {
|
|
167
|
+
msg: '[anytype]: add new feature',
|
|
168
|
+
type: [],
|
|
169
|
+
constraints: {}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const result = await validateCommitMessage(validationInfo);
|
|
173
|
+
|
|
174
|
+
expect(result.isValid).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('doit gérer les messages sans séparateur', async () => {
|
|
178
|
+
const validationInfo = {
|
|
179
|
+
msg: 'message sans format',
|
|
180
|
+
type: ['feat'],
|
|
181
|
+
constraints: {}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const result = await validateCommitMessage(validationInfo);
|
|
185
|
+
|
|
186
|
+
expect(result.isValid).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('doit gérer autoStartWith et corriger message', async () => {
|
|
190
|
+
const validationInfo = {
|
|
191
|
+
msg: '[feat]message sans séparateur',
|
|
192
|
+
type: ['feat'],
|
|
193
|
+
constraints: {
|
|
194
|
+
autoStartWith: ': ',
|
|
195
|
+
mustStartWith: ': '
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
execa.mockResolvedValue({ stdout: '.git' });
|
|
200
|
+
fs.existsSync.mockReturnValue(true);
|
|
201
|
+
fs.writeFileSync = jest.fn();
|
|
202
|
+
|
|
203
|
+
const result = await validateCommitMessage(validationInfo);
|
|
204
|
+
|
|
205
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
206
|
+
expect.any(String),
|
|
207
|
+
'[feat]: message sans séparateur'
|
|
208
|
+
);
|
|
209
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Message de commit corrigé'));
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('doit gérer erreur si COMMIT_EDITMSG absent avec autoStartWith', async () => {
|
|
213
|
+
const validationInfo = {
|
|
214
|
+
msg: '[feat]bad format',
|
|
215
|
+
type: ['feat'],
|
|
216
|
+
constraints: {
|
|
217
|
+
autoStartWith: ': ',
|
|
218
|
+
mustStartWith: ': '
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
execa.mockResolvedValue({ stdout: '.git' });
|
|
223
|
+
fs.existsSync.mockReturnValue(false);
|
|
224
|
+
|
|
225
|
+
await validateCommitMessage(validationInfo);
|
|
226
|
+
|
|
227
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('git commit --amend'));
|
|
228
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('doit gérer erreur git lors de correction autoStartWith', async () => {
|
|
232
|
+
const validationInfo = {
|
|
233
|
+
msg: '[feat]bad',
|
|
234
|
+
type: ['feat'],
|
|
235
|
+
constraints: {
|
|
236
|
+
autoStartWith: ': ',
|
|
237
|
+
mustStartWith: ': '
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
execa.mockRejectedValue(new Error('Git error'));
|
|
242
|
+
|
|
243
|
+
await validateCommitMessage(validationInfo);
|
|
244
|
+
|
|
245
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Erreur lors de la correction'));
|
|
246
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('doit retourner format invalide si autoStartWith actif sans fallback match', async () => {
|
|
250
|
+
const validationInfo = {
|
|
251
|
+
msg: 'message sans bracket',
|
|
252
|
+
type: ['feat'],
|
|
253
|
+
constraints: {
|
|
254
|
+
autoStartWith: ': ',
|
|
255
|
+
mustStartWith: ': '
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const result = await validateCommitMessage(validationInfo);
|
|
260
|
+
|
|
261
|
+
expect(result.isValid).toBe(false);
|
|
262
|
+
expect(result.errors[0]).toContain("n'est pas correctement formaté");
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('validateBranchName', () => {
|
|
267
|
+
test('doit valider nom de branche correct', async () => {
|
|
268
|
+
execa.mockResolvedValue({ stdout: 'feat/new-feature' });
|
|
269
|
+
|
|
270
|
+
const validationInfo = {
|
|
271
|
+
type: ['feat', 'fix']
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const result = await validateBranchName(validationInfo);
|
|
275
|
+
|
|
276
|
+
expect(result.isValid).toBe(true);
|
|
277
|
+
expect(result.branchDescription).toBe('new-feature');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('doit rejeter type de branche invalide', async () => {
|
|
281
|
+
execa.mockResolvedValue({ stdout: 'invalid/branch' });
|
|
282
|
+
|
|
283
|
+
const validationInfo = {
|
|
284
|
+
type: ['feat', 'fix']
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const result = await validateBranchName(validationInfo);
|
|
288
|
+
|
|
289
|
+
expect(result.isValid).toBe(false);
|
|
290
|
+
expect(result.errors[0]).toContain('type de branche "invalid" n\'est pas valide');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('doit gérer branche sans description', async () => {
|
|
294
|
+
execa.mockResolvedValue({ stdout: 'feat' });
|
|
295
|
+
|
|
296
|
+
const validationInfo = {
|
|
297
|
+
type: ['feat']
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const result = await validateBranchName(validationInfo);
|
|
301
|
+
|
|
302
|
+
expect(result.isValid).toBe(true);
|
|
303
|
+
expect(result.branchDescription).toBe('');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('doit gérer erreur git', async () => {
|
|
307
|
+
execa.mockRejectedValue(new Error('Git error'));
|
|
308
|
+
|
|
309
|
+
const validationInfo = {
|
|
310
|
+
type: ['feat']
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const result = await validateBranchName(validationInfo);
|
|
314
|
+
|
|
315
|
+
expect(console.log).toHaveBeenCalledWith('Erreur Git, validation ignorée:', 'Git error');
|
|
316
|
+
expect(result.isValid).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('doit ignorer un nom de branche non matché', async () => {
|
|
320
|
+
execa.mockResolvedValue({ stdout: '////' });
|
|
321
|
+
|
|
322
|
+
const result = await validateBranchName({ type: ['feat'] });
|
|
323
|
+
|
|
324
|
+
expect(result.isValid).toBe(true);
|
|
325
|
+
expect(result.errors).toEqual([]);
|
|
326
|
+
expect(result.branchDescription).toBe('');
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('validatePrePush', () => {
|
|
331
|
+
test('doit réussir pour nouvelle branche', async () => {
|
|
332
|
+
execa.mockImplementation((cmd, args) => {
|
|
333
|
+
if (args[0] === 'rev-parse') return Promise.resolve({ stdout: 'new-branch' });
|
|
334
|
+
if (args[0] === 'ls-remote') return Promise.resolve({ stdout: '' });
|
|
335
|
+
if (args[0] === 'validate') return Promise.resolve({ stdout: '' });
|
|
336
|
+
return Promise.resolve({ stdout: '' });
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const result = await validatePrePush();
|
|
340
|
+
|
|
341
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Nouvelle branche'));
|
|
342
|
+
expect(result.success).toBe(true);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('doit détecter branche distante en avance', async () => {
|
|
346
|
+
execa.mockImplementation((cmd, args) => {
|
|
347
|
+
if (args[0] === 'rev-parse') return Promise.resolve({ stdout: 'main' });
|
|
348
|
+
if (args[0] === 'ls-remote') return Promise.resolve({ stdout: 'refs/heads/main' });
|
|
349
|
+
if (args[0] === 'fetch') return Promise.resolve({ stdout: '' });
|
|
350
|
+
if (args[0] === 'rev-list') return Promise.resolve({ stdout: '5' });
|
|
351
|
+
if (args[0] === 'status') return Promise.resolve({ stdout: '' });
|
|
352
|
+
return Promise.resolve({ stdout: '' });
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
await validatePrePush();
|
|
356
|
+
|
|
357
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('branche distante "origin/main" est différente'));
|
|
358
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('git pull'));
|
|
359
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('doit détecter modifications locales non commit', async () => {
|
|
363
|
+
execa.mockImplementation((cmd, args) => {
|
|
364
|
+
if (args[0] === 'rev-parse') return Promise.resolve({ stdout: 'main' });
|
|
365
|
+
if (args[0] === 'ls-remote') return Promise.resolve({ stdout: 'refs/heads/main' });
|
|
366
|
+
if (args[0] === 'fetch') return Promise.resolve({ stdout: '' });
|
|
367
|
+
if (args[0] === 'rev-list') return Promise.resolve({ stdout: '3' });
|
|
368
|
+
if (args[0] === 'status') return Promise.resolve({ stdout: 'M file.js' });
|
|
369
|
+
return Promise.resolve({ stdout: '' });
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
await validatePrePush();
|
|
373
|
+
|
|
374
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('git stash && git pull && git stash pop'));
|
|
375
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test('doit gérer erreur fetch', async () => {
|
|
379
|
+
execa.mockImplementation((cmd, args) => {
|
|
380
|
+
if (args[0] === 'rev-parse') return Promise.resolve({ stdout: 'main' });
|
|
381
|
+
if (args[0] === 'ls-remote') return Promise.resolve({ stdout: 'refs/heads/main' });
|
|
382
|
+
if (args[0] === 'fetch') return Promise.reject(new Error('Fetch failed'));
|
|
383
|
+
if (cmd === 'npx') return Promise.resolve({ stdout: '' });
|
|
384
|
+
return Promise.resolve({ stdout: '' });
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
await validatePrePush();
|
|
388
|
+
|
|
389
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Impossible de vérifier la branche distante'));
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('doit appeler validation à la fin', async () => {
|
|
393
|
+
execa.mockImplementation((cmd, args) => {
|
|
394
|
+
if (args && args[0] === 'rev-parse') return Promise.resolve({ stdout: 'main' });
|
|
395
|
+
if (args && args[0] === 'ls-remote') return Promise.resolve({ stdout: '' });
|
|
396
|
+
if (cmd === 'npx') return Promise.resolve({ stdout: '' });
|
|
397
|
+
return Promise.resolve({ stdout: '' });
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
await validatePrePush();
|
|
401
|
+
|
|
402
|
+
expect(execa).toHaveBeenCalledWith('npx', ['push-guardian', 'validate', '-s']);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('ne doit pas quitter si la branche distante est synchronisée', async () => {
|
|
406
|
+
execa.mockImplementation((cmd, args) => {
|
|
407
|
+
if (args[0] === 'rev-parse') return Promise.resolve({ stdout: 'main' });
|
|
408
|
+
if (args[0] === 'ls-remote') return Promise.resolve({ stdout: 'refs/heads/main' });
|
|
409
|
+
if (args[0] === 'fetch') return Promise.resolve({ stdout: '' });
|
|
410
|
+
if (args[0] === 'rev-list') return Promise.resolve({ stdout: '0' });
|
|
411
|
+
if (cmd === 'npx') return Promise.resolve({ stdout: '' });
|
|
412
|
+
return Promise.resolve({ stdout: '' });
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const result = await validatePrePush();
|
|
416
|
+
|
|
417
|
+
expect(process.exit).not.toHaveBeenCalled();
|
|
418
|
+
expect(result.success).toBe(true);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe('constrains - post-checkout', () => {
|
|
423
|
+
test('doit valider branche en post-checkout', async () => {
|
|
424
|
+
loadConfig.mockReturnValue({
|
|
425
|
+
hooks: {
|
|
426
|
+
'post-checkout': {
|
|
427
|
+
type: ['feat', 'fix'],
|
|
428
|
+
constraints: {}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
execa.mockResolvedValue({ stdout: 'feat/new-branch' });
|
|
434
|
+
|
|
435
|
+
const result = await constrains('post-checkout', 'dummy');
|
|
436
|
+
|
|
437
|
+
expect(result.success).toBe(true);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
describe('constrains - pre-push', () => {
|
|
442
|
+
test('doit exécuter validatePrePush', async () => {
|
|
443
|
+
loadConfig.mockReturnValue({
|
|
444
|
+
hooks: {
|
|
445
|
+
'pre-push': {
|
|
446
|
+
constraints: {}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
execa.mockImplementation((cmd, args) => {
|
|
452
|
+
if (args && args[0] === 'rev-parse') return Promise.resolve({ stdout: 'main' });
|
|
453
|
+
if (args && args[0] === 'ls-remote') return Promise.resolve({ stdout: '' });
|
|
454
|
+
if (cmd === 'npx') return Promise.resolve({ stdout: '' });
|
|
455
|
+
return Promise.resolve({ stdout: '' });
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const result = await constrains('pre-push', 'dummy');
|
|
459
|
+
|
|
460
|
+
expect(result.success).toBe(true);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
});
|