@friggframework/devtools 2.0.0-next.41 → 2.0.0-next.42
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/frigg-cli/__tests__/unit/commands/build.test.js +173 -405
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +548 -0
- package/frigg-cli/__tests__/unit/commands/install.test.js +359 -377
- package/frigg-cli/__tests__/unit/commands/ui.test.js +266 -512
- package/frigg-cli/__tests__/unit/utils/database-validator.test.js +366 -0
- package/frigg-cli/__tests__/unit/utils/error-messages.test.js +304 -0
- package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +486 -0
- package/frigg-cli/__tests__/utils/prisma-mock.js +194 -0
- package/frigg-cli/__tests__/utils/test-setup.js +22 -21
- package/frigg-cli/db-setup-command/index.js +186 -0
- package/frigg-cli/generate-command/__tests__/generate-command.test.js +151 -162
- package/frigg-cli/generate-iam-command.js +7 -4
- package/frigg-cli/index.js +9 -1
- package/frigg-cli/install-command/index.js +1 -1
- package/frigg-cli/jest.config.js +124 -0
- package/frigg-cli/package.json +4 -1
- package/frigg-cli/start-command/index.js +95 -2
- package/frigg-cli/start-command/start-command.test.js +161 -19
- package/frigg-cli/utils/database-validator.js +158 -0
- package/frigg-cli/utils/error-messages.js +257 -0
- package/frigg-cli/utils/prisma-runner.js +280 -0
- package/infrastructure/CLAUDE.md +481 -0
- package/infrastructure/IAM-POLICY-TEMPLATES.md +30 -12
- package/infrastructure/create-frigg-infrastructure.js +0 -2
- package/infrastructure/iam-generator.js +18 -38
- package/infrastructure/iam-generator.test.js +40 -8
- package/package.json +6 -6
- package/test/index.js +2 -4
- package/test/mock-integration.js +4 -14
- package/frigg-cli/__tests__/jest.config.js +0 -102
- package/frigg-cli/__tests__/utils/command-tester.js +0 -170
- package/test/auther-definition-tester.js +0 -125
|
@@ -1,418 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test suite for install command
|
|
3
|
+
*
|
|
4
|
+
* Tests the ACTUAL Frigg implementation including:
|
|
5
|
+
* - Package search and selection (mocked - external npm)
|
|
6
|
+
* - Package installation via npm (mocked - external)
|
|
7
|
+
* - Integration file creation (REAL - tests actual file generation)
|
|
8
|
+
* - Backend.js updates (REAL - tests actual file parsing/updating)
|
|
9
|
+
* - Git commits (mocked - external)
|
|
10
|
+
* - Environment variable handling (mocked - interactive)
|
|
11
|
+
* - Label sanitization (REAL - tests actual regex logic)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Mock ONLY external boundaries - let Frigg logic run!
|
|
15
|
+
jest.mock('fs-extra'); // Mock at I/O level
|
|
16
|
+
jest.mock('../../../install-command/install-package', () => ({
|
|
17
|
+
installPackage: jest.fn() // External: npm install
|
|
18
|
+
}));
|
|
19
|
+
jest.mock('../../../install-command/commit-changes', () => ({
|
|
20
|
+
commitChanges: jest.fn() // External: git commands
|
|
21
|
+
}));
|
|
22
|
+
jest.mock('../../../install-command/environment-variables', () => ({
|
|
23
|
+
handleEnvVariables: jest.fn() // External: interactive prompts
|
|
24
|
+
}));
|
|
25
|
+
jest.mock('../../../install-command/validate-package', () => ({
|
|
26
|
+
validatePackageExists: jest.fn(), // External: npm registry
|
|
27
|
+
searchAndSelectPackage: jest.fn() // External: interactive selection
|
|
28
|
+
}));
|
|
29
|
+
jest.mock('@friggframework/core', () => ({
|
|
30
|
+
findNearestBackendPackageJson: jest.fn(),
|
|
31
|
+
validateBackendPath: jest.fn()
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
// DON'T mock these - let them run to test actual Frigg logic:
|
|
35
|
+
// - createIntegrationFile (tests file generation)
|
|
36
|
+
// - updateBackendJsFile (tests file parsing)
|
|
37
|
+
// - logger (just console.log, we'll spy on console)
|
|
38
|
+
// - getIntegrationTemplate (tests template generation)
|
|
39
|
+
|
|
40
|
+
// Require after mocks
|
|
41
|
+
const fs = require('fs-extra');
|
|
42
|
+
const { installPackage } = require('../../../install-command/install-package');
|
|
43
|
+
const { commitChanges } = require('../../../install-command/commit-changes');
|
|
44
|
+
const { handleEnvVariables } = require('../../../install-command/environment-variables');
|
|
45
|
+
const { validatePackageExists, searchAndSelectPackage } = require('../../../install-command/validate-package');
|
|
46
|
+
const { findNearestBackendPackageJson, validateBackendPath } = require('@friggframework/core');
|
|
1
47
|
const { installCommand } = require('../../../install-command');
|
|
2
|
-
const { CommandTester } = require('../../utils/command-tester');
|
|
3
|
-
const { MockFactory } = require('../../utils/mock-factory');
|
|
4
|
-
const { TestFixtures } = require('../../utils/test-fixtures');
|
|
5
48
|
|
|
6
49
|
describe('CLI Command: install', () => {
|
|
7
|
-
let
|
|
8
|
-
let
|
|
9
|
-
|
|
50
|
+
let processExitSpy;
|
|
51
|
+
let consoleLogSpy;
|
|
52
|
+
let consoleErrorSpy;
|
|
53
|
+
const mockBackendPath = '/mock/backend/package.json';
|
|
54
|
+
const mockBackendDir = '/mock/backend';
|
|
55
|
+
|
|
10
56
|
beforeEach(() => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
57
|
+
jest.clearAllMocks();
|
|
58
|
+
|
|
59
|
+
// Mock process.exit to prevent actual exit
|
|
60
|
+
processExitSpy = jest.spyOn(process, 'exit').mockImplementation();
|
|
61
|
+
|
|
62
|
+
// Spy on console for logger (don't mock logger - test it!)
|
|
63
|
+
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
64
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
65
|
+
|
|
66
|
+
// Setup fs-extra mocks - Let Frigg code run, just mock I/O
|
|
67
|
+
fs.ensureDirSync = jest.fn();
|
|
68
|
+
fs.writeFileSync = jest.fn();
|
|
69
|
+
fs.readFileSync = jest.fn().mockReturnValue(`
|
|
70
|
+
// Sample backend.js file
|
|
71
|
+
const integrations = [
|
|
72
|
+
// Existing integrations
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
integrations: []
|
|
77
|
+
};
|
|
78
|
+
`);
|
|
79
|
+
fs.existsSync = jest.fn().mockReturnValue(true);
|
|
80
|
+
|
|
81
|
+
// Setup default successful mocks for external boundaries
|
|
82
|
+
searchAndSelectPackage.mockResolvedValue(['@friggframework/api-module-slack']);
|
|
83
|
+
findNearestBackendPackageJson.mockReturnValue(mockBackendPath);
|
|
84
|
+
validateBackendPath.mockReturnValue(true);
|
|
85
|
+
validatePackageExists.mockResolvedValue(true);
|
|
86
|
+
installPackage.mockReturnValue(undefined);
|
|
87
|
+
handleEnvVariables.mockResolvedValue(undefined);
|
|
88
|
+
|
|
89
|
+
// Mock the dynamic require() of installed package using jest.doMock
|
|
90
|
+
const path = require('path');
|
|
91
|
+
const slackModulePath = path.resolve(mockBackendPath, '../../node_modules/@friggframework/api-module-slack');
|
|
92
|
+
|
|
93
|
+
jest.doMock(slackModulePath, () => ({
|
|
94
|
+
Config: { label: 'Slack' },
|
|
95
|
+
Api: class SlackApi {}
|
|
96
|
+
}), { virtual: true });
|
|
22
97
|
});
|
|
23
98
|
|
|
24
99
|
afterEach(() => {
|
|
25
|
-
|
|
26
|
-
|
|
100
|
+
processExitSpy.mockRestore();
|
|
101
|
+
consoleLogSpy.mockRestore();
|
|
102
|
+
consoleErrorSpy.mockRestore();
|
|
103
|
+
jest.resetModules(); // Clear module cache after each test
|
|
27
104
|
});
|
|
28
105
|
|
|
29
106
|
describe('Success Cases', () => {
|
|
30
|
-
it('should
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.mock('@friggframework/core', {
|
|
40
|
-
findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
|
|
41
|
-
validateBackendPath: jest.fn().mockReturnValue(true)
|
|
42
|
-
})
|
|
43
|
-
.mock('./install-command/install-package', {
|
|
44
|
-
installPackage: jest.fn().mockResolvedValue(true)
|
|
45
|
-
})
|
|
46
|
-
.mock('./install-command/integration-file', {
|
|
47
|
-
createIntegrationFile: jest.fn().mockResolvedValue(true)
|
|
48
|
-
})
|
|
49
|
-
.mock('./install-command/backend-js', {
|
|
50
|
-
updateBackendJsFile: jest.fn().mockResolvedValue(true)
|
|
51
|
-
})
|
|
52
|
-
.mock('./install-command/commit-changes', {
|
|
53
|
-
commitChanges: jest.fn().mockResolvedValue(true)
|
|
54
|
-
})
|
|
55
|
-
.mock('./install-command/logger', mocks.logger);
|
|
56
|
-
|
|
57
|
-
// Act
|
|
58
|
-
const result = await commandTester.execute([moduleName]);
|
|
59
|
-
|
|
60
|
-
// Assert
|
|
61
|
-
expect(result.success).toBe(true);
|
|
62
|
-
expect(result.exitCode).toBe(0);
|
|
107
|
+
it('should orchestrate complete installation workflow', async () => {
|
|
108
|
+
await installCommand('slack');
|
|
109
|
+
|
|
110
|
+
// Verify external boundaries called
|
|
111
|
+
expect(searchAndSelectPackage).toHaveBeenCalledWith('slack');
|
|
112
|
+
expect(findNearestBackendPackageJson).toHaveBeenCalled();
|
|
113
|
+
expect(validateBackendPath).toHaveBeenCalledWith(mockBackendPath);
|
|
114
|
+
expect(validatePackageExists).toHaveBeenCalledWith('@friggframework/api-module-slack');
|
|
115
|
+
expect(installPackage).toHaveBeenCalledWith(mockBackendPath, '@friggframework/api-module-slack');
|
|
63
116
|
});
|
|
64
117
|
|
|
65
|
-
it('should
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// Act
|
|
93
|
-
const result = await commandTester.execute([moduleName, '--app-path', customPath]);
|
|
94
|
-
|
|
95
|
-
// Assert
|
|
96
|
-
expect(result.success).toBe(true);
|
|
97
|
-
expect(result.exitCode).toBe(0);
|
|
118
|
+
it('should create integration file with correct path and content', async () => {
|
|
119
|
+
await installCommand('slack');
|
|
120
|
+
|
|
121
|
+
// Verify directory creation
|
|
122
|
+
expect(fs.ensureDirSync).toHaveBeenCalledWith(
|
|
123
|
+
expect.stringMatching(/src\/integrations$/)
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Verify integration file written with correct path
|
|
127
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
128
|
+
expect.stringMatching(/SlackIntegration\.js$/),
|
|
129
|
+
expect.any(String)
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Get the actual content that was written
|
|
133
|
+
const writeCall = fs.writeFileSync.mock.calls.find(call =>
|
|
134
|
+
call[0].includes('SlackIntegration.js')
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(writeCall).toBeDefined();
|
|
138
|
+
const [filePath, content] = writeCall;
|
|
139
|
+
|
|
140
|
+
// Verify file content contains valid integration class
|
|
141
|
+
expect(content).toContain('class SlackIntegration extends IntegrationBase');
|
|
142
|
+
expect(content).toContain('@friggframework/core');
|
|
143
|
+
expect(content).toContain('@friggframework/api-module-slack');
|
|
98
144
|
});
|
|
99
145
|
|
|
100
|
-
it('should
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
createIntegrationFile: jest.fn().mockResolvedValue(true)
|
|
118
|
-
})
|
|
119
|
-
.mock('./install-command/backend-js', {
|
|
120
|
-
updateBackendJsFile: jest.fn().mockResolvedValue(true)
|
|
121
|
-
})
|
|
122
|
-
.mock('./install-command/commit-changes', {
|
|
123
|
-
commitChanges: jest.fn().mockResolvedValue(true)
|
|
124
|
-
})
|
|
125
|
-
.mock('./install-command/logger', mocks.logger);
|
|
126
|
-
|
|
127
|
-
// Act
|
|
128
|
-
const result = await commandTester.execute([moduleName]);
|
|
129
|
-
|
|
130
|
-
// Assert
|
|
131
|
-
expect(result.success).toBe(true);
|
|
132
|
-
expect(result.exitCode).toBe(0);
|
|
146
|
+
it('should generate valid JavaScript template', async () => {
|
|
147
|
+
await installCommand('slack');
|
|
148
|
+
|
|
149
|
+
const writeCall = fs.writeFileSync.mock.calls.find(call =>
|
|
150
|
+
call[0].includes('SlackIntegration.js')
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const [, content] = writeCall;
|
|
154
|
+
|
|
155
|
+
// Verify template has required structure
|
|
156
|
+
expect(content).toMatch(/class \w+Integration extends IntegrationBase/);
|
|
157
|
+
expect(content).toContain('static Config =');
|
|
158
|
+
expect(content).toContain('static Options =');
|
|
159
|
+
expect(content).toContain('static modules =');
|
|
160
|
+
|
|
161
|
+
// Verify template is syntactically valid (no unclosed braces, etc)
|
|
162
|
+
expect(content.split('{').length).toBe(content.split('}').length);
|
|
133
163
|
});
|
|
134
|
-
});
|
|
135
164
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
165
|
+
it('should update backend.js with integration import', async () => {
|
|
166
|
+
await installCommand('slack');
|
|
167
|
+
|
|
168
|
+
// Verify backend.js was read
|
|
169
|
+
expect(fs.readFileSync).toHaveBeenCalledWith(
|
|
170
|
+
expect.stringMatching(/backend\.js$/),
|
|
171
|
+
'utf-8'
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Verify backend.js was written back with import
|
|
175
|
+
const backendWriteCall = fs.writeFileSync.mock.calls.find(call =>
|
|
176
|
+
call[0].includes('backend.js')
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(backendWriteCall).toBeDefined();
|
|
180
|
+
const [, updatedBackend] = backendWriteCall;
|
|
181
|
+
|
|
182
|
+
// Verify import statement added
|
|
183
|
+
expect(updatedBackend).toContain('const SlackIntegration = require');
|
|
184
|
+
expect(updatedBackend).toContain('./src/integrations/SlackIntegration');
|
|
185
|
+
|
|
186
|
+
// Verify integration added to array
|
|
187
|
+
expect(updatedBackend).toContain('SlackIntegration,');
|
|
153
188
|
});
|
|
154
189
|
|
|
155
|
-
it('should
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
commandTester
|
|
160
|
-
.mock('./install-command/validate-package', {
|
|
161
|
-
validatePackageExists: jest.fn().mockResolvedValue(true)
|
|
162
|
-
})
|
|
163
|
-
.mock('@friggframework/core', {
|
|
164
|
-
findNearestBackendPackageJson: jest.fn().mockReturnValue('/invalid/path'),
|
|
165
|
-
validateBackendPath: jest.fn().mockImplementation(() => {
|
|
166
|
-
throw new Error('Invalid backend path');
|
|
167
|
-
})
|
|
168
|
-
})
|
|
169
|
-
.mock('./install-command/logger', mocks.logger);
|
|
170
|
-
|
|
171
|
-
// Act
|
|
172
|
-
const result = await commandTester.execute([moduleName]);
|
|
173
|
-
|
|
174
|
-
// Assert
|
|
175
|
-
expect(result.success).toBe(false);
|
|
176
|
-
expect(result.exitCode).toBe(1);
|
|
190
|
+
it('should commit changes after file operations', async () => {
|
|
191
|
+
await installCommand('slack');
|
|
192
|
+
|
|
193
|
+
expect(commitChanges).toHaveBeenCalledWith(mockBackendPath, 'Slack');
|
|
177
194
|
});
|
|
178
195
|
|
|
179
|
-
it('should handle installation
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
.
|
|
185
|
-
|
|
186
|
-
})
|
|
187
|
-
.mock('@friggframework/core', {
|
|
188
|
-
findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
|
|
189
|
-
validateBackendPath: jest.fn().mockReturnValue(true)
|
|
190
|
-
})
|
|
191
|
-
.mock('./install-command/install-package', {
|
|
192
|
-
installPackage: jest.fn().mockRejectedValue(new Error('Installation failed'))
|
|
193
|
-
})
|
|
194
|
-
.mock('./install-command/logger', mocks.logger);
|
|
195
|
-
|
|
196
|
-
// Act
|
|
197
|
-
const result = await commandTester.execute([moduleName]);
|
|
198
|
-
|
|
199
|
-
// Assert
|
|
200
|
-
expect(result.success).toBe(false);
|
|
201
|
-
expect(result.exitCode).toBe(1);
|
|
196
|
+
it('should handle environment variables after installation', async () => {
|
|
197
|
+
await installCommand('slack');
|
|
198
|
+
|
|
199
|
+
expect(handleEnvVariables).toHaveBeenCalledWith(
|
|
200
|
+
mockBackendPath,
|
|
201
|
+
expect.stringContaining('@friggframework/api-module-slack')
|
|
202
|
+
);
|
|
202
203
|
});
|
|
203
204
|
|
|
204
|
-
it('should
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
.
|
|
210
|
-
|
|
211
|
-
})
|
|
212
|
-
.mock('@friggframework/core', {
|
|
213
|
-
findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
|
|
214
|
-
validateBackendPath: jest.fn().mockReturnValue(true)
|
|
215
|
-
})
|
|
216
|
-
.mock('./install-command/install-package', {
|
|
217
|
-
installPackage: jest.fn().mockResolvedValue(true)
|
|
218
|
-
})
|
|
219
|
-
.mock('./install-command/integration-file', {
|
|
220
|
-
createIntegrationFile: jest.fn().mockRejectedValue(new Error('File creation failed'))
|
|
221
|
-
})
|
|
222
|
-
.mock('./install-command/logger', mocks.logger);
|
|
223
|
-
|
|
224
|
-
// Act
|
|
225
|
-
const result = await commandTester.execute([moduleName]);
|
|
226
|
-
|
|
227
|
-
// Assert
|
|
228
|
-
expect(result.success).toBe(false);
|
|
229
|
-
expect(result.exitCode).toBe(1);
|
|
205
|
+
it('should log info messages during installation', async () => {
|
|
206
|
+
await installCommand('slack');
|
|
207
|
+
|
|
208
|
+
// Verify logger actually logged (we spy on console)
|
|
209
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
210
|
+
expect.stringContaining('Successfully installed @friggframework/api-module-slack')
|
|
211
|
+
);
|
|
230
212
|
});
|
|
231
|
-
});
|
|
232
213
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
214
|
+
it('should install multiple packages sequentially', async () => {
|
|
215
|
+
searchAndSelectPackage.mockResolvedValue([
|
|
216
|
+
'@friggframework/api-module-slack',
|
|
217
|
+
'@friggframework/api-module-hubspot'
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
// Mock HubSpot module (Slack already mocked in beforeEach)
|
|
221
|
+
const path = require('path');
|
|
222
|
+
const hubspotPath = path.resolve(mockBackendPath, '../../node_modules/@friggframework/api-module-hubspot');
|
|
223
|
+
|
|
224
|
+
jest.doMock(hubspotPath, () => ({
|
|
225
|
+
Config: { label: 'HubSpot' },
|
|
226
|
+
Api: class HubSpotApi {}
|
|
227
|
+
}), { virtual: true });
|
|
228
|
+
|
|
229
|
+
await installCommand('crm');
|
|
238
230
|
|
|
239
|
-
|
|
240
|
-
|
|
231
|
+
expect(validatePackageExists).toHaveBeenCalledTimes(2);
|
|
232
|
+
expect(installPackage).toHaveBeenCalledTimes(2);
|
|
241
233
|
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
234
|
+
// Verify TWO integration files created
|
|
235
|
+
const integrationFiles = fs.writeFileSync.mock.calls.filter(call =>
|
|
236
|
+
call[0].includes('Integration.js') && !call[0].includes('backend.js')
|
|
237
|
+
);
|
|
238
|
+
expect(integrationFiles.length).toBe(2);
|
|
239
|
+
|
|
240
|
+
// Verify both files have correct names
|
|
241
|
+
expect(integrationFiles[0][0]).toContain('SlackIntegration.js');
|
|
242
|
+
expect(integrationFiles[1][0]).toContain('HubSpotIntegration.js');
|
|
245
243
|
});
|
|
246
244
|
|
|
247
|
-
it('should
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
245
|
+
it('should sanitize label by removing invalid characters', async () => {
|
|
246
|
+
// Mock different package with special characters in label
|
|
247
|
+
searchAndSelectPackage.mockResolvedValue(['@friggframework/api-module-google-drive']);
|
|
248
|
+
|
|
249
|
+
const path = require('path');
|
|
250
|
+
const googleDrivePath = path.resolve(mockBackendPath, '../../node_modules/@friggframework/api-module-google-drive');
|
|
251
|
+
|
|
252
|
+
jest.doMock(googleDrivePath, () => ({
|
|
253
|
+
Config: { label: 'Google<Drive>' }, // Has invalid characters
|
|
254
|
+
Api: class GoogleDriveApi {}
|
|
255
|
+
}), { virtual: true });
|
|
256
|
+
|
|
257
|
+
await installCommand('google-drive');
|
|
258
|
+
|
|
259
|
+
// Verify sanitized label used in file name
|
|
260
|
+
const writeCall = fs.writeFileSync.mock.calls.find(call =>
|
|
261
|
+
call[0].includes('Integration.js')
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Should be GoogleDrive, not Google<Drive>
|
|
265
|
+
expect(writeCall[0]).toContain('GoogleDriveIntegration.js');
|
|
266
|
+
expect(writeCall[0]).not.toContain('<');
|
|
267
|
+
expect(writeCall[0]).not.toContain('>');
|
|
268
|
+
|
|
269
|
+
// Verify content uses sanitized name
|
|
270
|
+
expect(writeCall[1]).toContain('class GoogleDriveIntegration');
|
|
271
|
+
expect(writeCall[1]).not.toContain('Google<Drive>');
|
|
263
272
|
});
|
|
264
273
|
|
|
265
|
-
it('should
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
274
|
+
it('should sanitize label by removing spaces', async () => {
|
|
275
|
+
// Mock different package with spaces in label
|
|
276
|
+
searchAndSelectPackage.mockResolvedValue(['@friggframework/api-module-google-calendar']);
|
|
277
|
+
|
|
278
|
+
const path = require('path');
|
|
279
|
+
const googleCalendarPath = path.resolve(mockBackendPath, '../../node_modules/@friggframework/api-module-google-calendar');
|
|
280
|
+
|
|
281
|
+
jest.doMock(googleCalendarPath, () => ({
|
|
282
|
+
Config: { label: 'Google Calendar' }, // Has spaces
|
|
283
|
+
Api: class GoogleCalendarApi {}
|
|
284
|
+
}), { virtual: true });
|
|
285
|
+
|
|
286
|
+
await installCommand('google-calendar');
|
|
287
|
+
|
|
288
|
+
// Verify sanitized label used in file name (no spaces)
|
|
289
|
+
const writeCall = fs.writeFileSync.mock.calls.find(call =>
|
|
290
|
+
call[0].includes('Integration.js')
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
expect(writeCall[0]).toContain('GoogleCalendarIntegration.js');
|
|
294
|
+
expect(writeCall[0]).not.toContain(' ');
|
|
295
|
+
|
|
296
|
+
// Verify content uses sanitized name
|
|
297
|
+
expect(writeCall[1]).toContain('class GoogleCalendarIntegration');
|
|
298
|
+
expect(writeCall[1]).not.toMatch(/class Google Calendar/);
|
|
281
299
|
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('Early Exit Cases', () => {
|
|
303
|
+
it('should return early when no packages selected', async () => {
|
|
304
|
+
searchAndSelectPackage.mockResolvedValue([]);
|
|
282
305
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const result = await commandTester.execute([moduleName]);
|
|
305
|
-
|
|
306
|
-
// Assert
|
|
307
|
-
expect(result.success).toBe(false);
|
|
308
|
-
expect(result.exitCode).toBe(1);
|
|
306
|
+
await installCommand('slack');
|
|
307
|
+
|
|
308
|
+
expect(findNearestBackendPackageJson).not.toHaveBeenCalled();
|
|
309
|
+
expect(validatePackageExists).not.toHaveBeenCalled();
|
|
310
|
+
expect(installPackage).not.toHaveBeenCalled();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should return early when packages is null', async () => {
|
|
314
|
+
searchAndSelectPackage.mockResolvedValue(null);
|
|
315
|
+
|
|
316
|
+
await installCommand('slack');
|
|
317
|
+
|
|
318
|
+
expect(findNearestBackendPackageJson).not.toHaveBeenCalled();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should return early when packages is undefined', async () => {
|
|
322
|
+
searchAndSelectPackage.mockResolvedValue(undefined);
|
|
323
|
+
|
|
324
|
+
await installCommand('slack');
|
|
325
|
+
|
|
326
|
+
expect(findNearestBackendPackageJson).not.toHaveBeenCalled();
|
|
309
327
|
});
|
|
310
328
|
});
|
|
311
329
|
|
|
312
|
-
describe('
|
|
313
|
-
it('should
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
.mock('@friggframework/core', {
|
|
323
|
-
findNearestBackendPackageJson: jest.fn().mockReturnValue('/custom/backend/package.json'),
|
|
324
|
-
validateBackendPath: jest.fn().mockReturnValue(true)
|
|
325
|
-
})
|
|
326
|
-
.mock('./install-command/install-package', {
|
|
327
|
-
installPackage: jest.fn().mockResolvedValue(true)
|
|
328
|
-
})
|
|
329
|
-
.mock('./install-command/integration-file', {
|
|
330
|
-
createIntegrationFile: jest.fn().mockResolvedValue(true)
|
|
331
|
-
})
|
|
332
|
-
.mock('./install-command/backend-js', {
|
|
333
|
-
updateBackendJsFile: jest.fn().mockResolvedValue(true)
|
|
334
|
-
})
|
|
335
|
-
.mock('./install-command/commit-changes', {
|
|
336
|
-
commitChanges: jest.fn().mockResolvedValue(true)
|
|
337
|
-
})
|
|
338
|
-
.mock('./install-command/logger', mocks.logger);
|
|
339
|
-
|
|
340
|
-
// Act
|
|
341
|
-
const result = await commandTester.execute([moduleName, '--app-path', customPath]);
|
|
342
|
-
|
|
343
|
-
// Assert
|
|
344
|
-
expect(result.success).toBe(true);
|
|
345
|
-
expect(result.args).toEqual([moduleName, '--app-path', customPath]);
|
|
330
|
+
describe('Error Handling', () => {
|
|
331
|
+
it('should log error and exit on searchAndSelectPackage failure', async () => {
|
|
332
|
+
const error = new Error('Search failed');
|
|
333
|
+
searchAndSelectPackage.mockRejectedValue(error);
|
|
334
|
+
|
|
335
|
+
await installCommand('slack');
|
|
336
|
+
|
|
337
|
+
// Verify error logged via console.error (we spy on it)
|
|
338
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', error);
|
|
339
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
346
340
|
});
|
|
347
341
|
|
|
348
|
-
it('should
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
})
|
|
357
|
-
.mock('@friggframework/core', {
|
|
358
|
-
findNearestBackendPackageJson: jest.fn().mockReturnValue('/mock/backend/package.json'),
|
|
359
|
-
validateBackendPath: jest.fn().mockReturnValue(true)
|
|
360
|
-
})
|
|
361
|
-
.mock('./install-command/install-package', {
|
|
362
|
-
installPackage: jest.fn().mockResolvedValue(true)
|
|
363
|
-
})
|
|
364
|
-
.mock('./install-command/integration-file', {
|
|
365
|
-
createIntegrationFile: jest.fn().mockResolvedValue(true)
|
|
366
|
-
})
|
|
367
|
-
.mock('./install-command/backend-js', {
|
|
368
|
-
updateBackendJsFile: jest.fn().mockResolvedValue(true)
|
|
369
|
-
})
|
|
370
|
-
.mock('./install-command/commit-changes', {
|
|
371
|
-
commitChanges: jest.fn().mockResolvedValue(true)
|
|
372
|
-
})
|
|
373
|
-
.mock('./install-command/logger', mocks.logger);
|
|
374
|
-
|
|
375
|
-
// Act
|
|
376
|
-
const result = await commandTester.execute([moduleName, '--config', configPath]);
|
|
377
|
-
|
|
378
|
-
// Assert
|
|
379
|
-
expect(result.success).toBe(true);
|
|
380
|
-
expect(result.args).toEqual([moduleName, '--config', configPath]);
|
|
342
|
+
it('should log error and exit on validatePackageExists failure', async () => {
|
|
343
|
+
const error = new Error('Package not found');
|
|
344
|
+
validatePackageExists.mockRejectedValue(error);
|
|
345
|
+
|
|
346
|
+
await installCommand('slack');
|
|
347
|
+
|
|
348
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', error);
|
|
349
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
381
350
|
});
|
|
382
351
|
|
|
383
|
-
it('should
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
352
|
+
it('should log error and exit on installPackage failure', async () => {
|
|
353
|
+
const error = new Error('Installation failed');
|
|
354
|
+
installPackage.mockImplementation(() => {
|
|
355
|
+
throw error;
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
await installCommand('slack');
|
|
359
|
+
|
|
360
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', error);
|
|
361
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should log error and exit on file write failure (createIntegrationFile)', async () => {
|
|
365
|
+
// Make fs.writeFileSync throw - tests REAL error path
|
|
366
|
+
const error = new Error('EACCES: permission denied');
|
|
367
|
+
fs.writeFileSync.mockImplementation(() => {
|
|
368
|
+
throw error;
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
await installCommand('slack');
|
|
372
|
+
|
|
373
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', expect.any(Error));
|
|
374
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('should log error and exit on backend.js read failure', async () => {
|
|
378
|
+
// Make fs.readFileSync throw - tests REAL error path
|
|
379
|
+
const error = new Error('ENOENT: file not found');
|
|
380
|
+
fs.readFileSync.mockImplementation(() => {
|
|
381
|
+
throw error;
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
await installCommand('slack');
|
|
385
|
+
|
|
386
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', expect.any(Error));
|
|
387
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('should log error and exit on handleEnvVariables failure', async () => {
|
|
391
|
+
const error = new Error('Env variables failed');
|
|
392
|
+
handleEnvVariables.mockRejectedValue(error);
|
|
393
|
+
|
|
394
|
+
await installCommand('slack');
|
|
395
|
+
|
|
396
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', error);
|
|
397
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
416
398
|
});
|
|
417
399
|
});
|
|
418
|
-
});
|
|
400
|
+
});
|