@friggframework/devtools 2.0.0--canary.545.c870571.0 → 2.0.0--canary.545.7b93757.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.
|
@@ -207,7 +207,7 @@ describe('CLI Command: build', () => {
|
|
|
207
207
|
await buildCommand({ stage: 'dev' });
|
|
208
208
|
|
|
209
209
|
expect(consoleLogSpy).toHaveBeenCalledWith('Building the serverless application...');
|
|
210
|
-
expect(consoleLogSpy).toHaveBeenCalledWith('
|
|
210
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('Packaging serverless application...');
|
|
211
211
|
});
|
|
212
212
|
|
|
213
213
|
it('should construct complete valid serverless command', async () => {
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Dispatch Tests
|
|
3
|
+
*
|
|
4
|
+
* Verifies that CLI commands correctly route to provider plugins
|
|
5
|
+
* based on appDefinition.provider. These tests ensure:
|
|
6
|
+
*
|
|
7
|
+
* 1. AWS (default) falls through to existing serverless behavior
|
|
8
|
+
* 2. Non-AWS providers delegate to the provider plugin
|
|
9
|
+
* 3. AWS-only commands reject non-AWS providers
|
|
10
|
+
*
|
|
11
|
+
* When adding a new provider, these tests should pass without changes
|
|
12
|
+
* as long as the provider implements the plugin interface.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
// Jest hoists jest.mock() calls — variable names prefixed with `mock` are allowed
|
|
18
|
+
function mockCreateProvider(overrides = {}) {
|
|
19
|
+
return {
|
|
20
|
+
name: 'test-provider',
|
|
21
|
+
deploy: jest.fn().mockResolvedValue({ url: 'https://test.example.com' }),
|
|
22
|
+
validate: jest.fn().mockReturnValue({ valid: true, errors: [], warnings: [] }),
|
|
23
|
+
preflightCheck: jest.fn().mockResolvedValue({ ready: true, missing: [] }),
|
|
24
|
+
generateConfig: jest.fn().mockReturnValue('# generated config'),
|
|
25
|
+
generateEnvTemplate: jest.fn().mockReturnValue({}),
|
|
26
|
+
getFunctionEntryPoints: jest.fn().mockReturnValue({
|
|
27
|
+
'api.js': '// api handler',
|
|
28
|
+
'worker-background.js': '// worker handler',
|
|
29
|
+
}),
|
|
30
|
+
detect: jest.fn().mockReturnValue(false),
|
|
31
|
+
createHandler: jest.fn(),
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a mock child process that auto-fires the 'close' event with exit 0.
|
|
38
|
+
* This prevents deploy command from hanging on the `await` for the close event.
|
|
39
|
+
*/
|
|
40
|
+
function mockCreateChildProcess(exitCode = 0) {
|
|
41
|
+
return {
|
|
42
|
+
on: jest.fn((event, callback) => {
|
|
43
|
+
if (event === 'close') {
|
|
44
|
+
// Fire asynchronously to simulate real behavior
|
|
45
|
+
setImmediate(() => callback(exitCode));
|
|
46
|
+
}
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Deploy Command ────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
describe('deploy command: provider dispatch', () => {
|
|
54
|
+
let deployCommand;
|
|
55
|
+
let spawn;
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
jest.resetModules();
|
|
59
|
+
jest.clearAllMocks();
|
|
60
|
+
|
|
61
|
+
jest.mock('child_process', () => ({
|
|
62
|
+
spawn: jest.fn(),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
jest.mock('fs', () => ({
|
|
66
|
+
existsSync: jest.fn().mockReturnValue(false),
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
// Mock doctor-command (deploy imports it; it has heavy AWS deps)
|
|
70
|
+
jest.mock('../../../doctor-command', () => ({
|
|
71
|
+
doctorCommand: jest.fn(),
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
spawn = require('child_process').spawn;
|
|
75
|
+
spawn.mockReturnValue(mockCreateChildProcess(0));
|
|
76
|
+
|
|
77
|
+
jest.spyOn(console, 'log').mockImplementation();
|
|
78
|
+
jest.spyOn(console, 'warn').mockImplementation();
|
|
79
|
+
jest.spyOn(console, 'error').mockImplementation();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
jest.restoreAllMocks();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('falls through to osls for AWS (no provider in appDefinition)', async () => {
|
|
87
|
+
({ deployCommand } = require('../../../deploy-command'));
|
|
88
|
+
|
|
89
|
+
await deployCommand({ stage: 'dev', skipDoctor: true });
|
|
90
|
+
|
|
91
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
92
|
+
'osls',
|
|
93
|
+
expect.arrayContaining(['deploy']),
|
|
94
|
+
expect.any(Object)
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('falls through to osls when provider is explicitly "aws"', async () => {
|
|
99
|
+
const fs = require('fs');
|
|
100
|
+
fs.existsSync.mockReturnValue(true);
|
|
101
|
+
|
|
102
|
+
jest.mock(
|
|
103
|
+
path.join(process.cwd(), 'index.js'),
|
|
104
|
+
() => ({ Definition: { provider: 'aws', environment: {} } }),
|
|
105
|
+
{ virtual: true }
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
({ deployCommand } = require('../../../deploy-command'));
|
|
109
|
+
await deployCommand({ stage: 'dev', skipDoctor: true });
|
|
110
|
+
|
|
111
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
112
|
+
'osls',
|
|
113
|
+
expect.arrayContaining(['deploy']),
|
|
114
|
+
expect.any(Object)
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('delegates to provider.deploy() for non-AWS provider', async () => {
|
|
119
|
+
const mockProvider = mockCreateProvider({ name: 'netlify' });
|
|
120
|
+
const fs = require('fs');
|
|
121
|
+
fs.existsSync.mockReturnValue(true);
|
|
122
|
+
|
|
123
|
+
jest.mock(
|
|
124
|
+
path.join(process.cwd(), 'index.js'),
|
|
125
|
+
() => ({ Definition: { provider: 'netlify', environment: {} } }),
|
|
126
|
+
{ virtual: true }
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
jest.mock('@friggframework/core/providers/resolve-provider', () => ({
|
|
130
|
+
resolveProvider: () => mockProvider,
|
|
131
|
+
}), { virtual: true });
|
|
132
|
+
|
|
133
|
+
({ deployCommand } = require('../../../deploy-command'));
|
|
134
|
+
await deployCommand({ stage: 'production' });
|
|
135
|
+
|
|
136
|
+
// Should NOT spawn osls
|
|
137
|
+
expect(spawn).not.toHaveBeenCalled();
|
|
138
|
+
|
|
139
|
+
// Should call provider.validate then provider.deploy
|
|
140
|
+
expect(mockProvider.validate).toHaveBeenCalled();
|
|
141
|
+
expect(mockProvider.deploy).toHaveBeenCalledWith(
|
|
142
|
+
expect.objectContaining({ provider: 'netlify' }),
|
|
143
|
+
expect.objectContaining({ stage: 'production', prod: true })
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('exits when provider.validate() reports errors', async () => {
|
|
148
|
+
const mockProvider = mockCreateProvider({
|
|
149
|
+
name: 'netlify',
|
|
150
|
+
validate: jest.fn().mockReturnValue({
|
|
151
|
+
valid: false,
|
|
152
|
+
errors: ['KMS encryption not supported on Netlify'],
|
|
153
|
+
warnings: [],
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const fs = require('fs');
|
|
158
|
+
fs.existsSync.mockReturnValue(true);
|
|
159
|
+
|
|
160
|
+
jest.mock(
|
|
161
|
+
path.join(process.cwd(), 'index.js'),
|
|
162
|
+
() => ({ Definition: { provider: 'netlify' } }),
|
|
163
|
+
{ virtual: true }
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
jest.mock('@friggframework/core/providers/resolve-provider', () => ({
|
|
167
|
+
resolveProvider: () => mockProvider,
|
|
168
|
+
}), { virtual: true });
|
|
169
|
+
|
|
170
|
+
jest.spyOn(process, 'exit').mockImplementation();
|
|
171
|
+
|
|
172
|
+
({ deployCommand } = require('../../../deploy-command'));
|
|
173
|
+
await deployCommand({ stage: 'dev' });
|
|
174
|
+
|
|
175
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
176
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
177
|
+
expect.stringContaining('KMS encryption not supported')
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('exits when provider.deploy() throws', async () => {
|
|
182
|
+
const mockDeployError = new Error('Preflight check failed');
|
|
183
|
+
mockDeployError.missing = ['NETLIFY_AUTH_TOKEN'];
|
|
184
|
+
|
|
185
|
+
const mockProvider = mockCreateProvider({
|
|
186
|
+
name: 'netlify',
|
|
187
|
+
deploy: jest.fn().mockRejectedValue(mockDeployError),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const fs = require('fs');
|
|
191
|
+
fs.existsSync.mockReturnValue(true);
|
|
192
|
+
|
|
193
|
+
jest.mock(
|
|
194
|
+
path.join(process.cwd(), 'index.js'),
|
|
195
|
+
() => ({ Definition: { provider: 'netlify' } }),
|
|
196
|
+
{ virtual: true }
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
jest.mock('@friggframework/core/providers/resolve-provider', () => ({
|
|
200
|
+
resolveProvider: () => mockProvider,
|
|
201
|
+
}), { virtual: true });
|
|
202
|
+
|
|
203
|
+
jest.spyOn(process, 'exit').mockImplementation();
|
|
204
|
+
|
|
205
|
+
({ deployCommand } = require('../../../deploy-command'));
|
|
206
|
+
await deployCommand({ stage: 'dev' });
|
|
207
|
+
|
|
208
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
209
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
210
|
+
expect.stringContaining('Preflight check failed')
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ─── Build Command ─────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
describe('build command: provider dispatch', () => {
|
|
218
|
+
let buildCommand;
|
|
219
|
+
let spawnSync;
|
|
220
|
+
|
|
221
|
+
beforeEach(() => {
|
|
222
|
+
jest.resetModules();
|
|
223
|
+
jest.clearAllMocks();
|
|
224
|
+
|
|
225
|
+
jest.mock('child_process', () => ({
|
|
226
|
+
spawnSync: jest.fn().mockReturnValue({ status: 0 }),
|
|
227
|
+
}));
|
|
228
|
+
|
|
229
|
+
spawnSync = require('child_process').spawnSync;
|
|
230
|
+
|
|
231
|
+
jest.spyOn(console, 'log').mockImplementation();
|
|
232
|
+
jest.spyOn(console, 'warn').mockImplementation();
|
|
233
|
+
jest.spyOn(console, 'error').mockImplementation();
|
|
234
|
+
jest.spyOn(process, 'exit').mockImplementation();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
afterEach(() => {
|
|
238
|
+
jest.restoreAllMocks();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('falls through to osls for AWS (default)', async () => {
|
|
242
|
+
({ buildCommand } = require('../../../build-command'));
|
|
243
|
+
|
|
244
|
+
await buildCommand({ stage: 'dev' });
|
|
245
|
+
|
|
246
|
+
expect(spawnSync).toHaveBeenCalledWith(
|
|
247
|
+
'osls',
|
|
248
|
+
expect.arrayContaining(['package']),
|
|
249
|
+
expect.any(Object)
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('delegates to provider build for non-AWS', async () => {
|
|
254
|
+
const mockProvider = mockCreateProvider({ name: 'netlify' });
|
|
255
|
+
|
|
256
|
+
jest.mock('../../../utils/provider-helper', () => ({
|
|
257
|
+
loadProviderForCli: () => ({
|
|
258
|
+
appDefinition: { provider: 'netlify' },
|
|
259
|
+
provider: mockProvider,
|
|
260
|
+
providerName: 'netlify',
|
|
261
|
+
}),
|
|
262
|
+
}));
|
|
263
|
+
|
|
264
|
+
jest.mock('fs', () => ({
|
|
265
|
+
writeFileSync: jest.fn(),
|
|
266
|
+
mkdirSync: jest.fn(),
|
|
267
|
+
}));
|
|
268
|
+
|
|
269
|
+
({ buildCommand } = require('../../../build-command'));
|
|
270
|
+
await buildCommand({ stage: 'dev' });
|
|
271
|
+
|
|
272
|
+
// Should NOT call osls
|
|
273
|
+
expect(spawnSync).not.toHaveBeenCalled();
|
|
274
|
+
|
|
275
|
+
// Should call provider's build pipeline
|
|
276
|
+
expect(mockProvider.validate).toHaveBeenCalled();
|
|
277
|
+
expect(mockProvider.generateConfig).toHaveBeenCalled();
|
|
278
|
+
expect(mockProvider.getFunctionEntryPoints).toHaveBeenCalled();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// ─── AWS-Only Command Guards ───────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
describe('AWS-only command guards', () => {
|
|
285
|
+
let processExitSpy;
|
|
286
|
+
|
|
287
|
+
beforeEach(() => {
|
|
288
|
+
jest.resetModules();
|
|
289
|
+
jest.clearAllMocks();
|
|
290
|
+
|
|
291
|
+
jest.spyOn(console, 'log').mockImplementation();
|
|
292
|
+
jest.spyOn(console, 'warn').mockImplementation();
|
|
293
|
+
jest.spyOn(console, 'error').mockImplementation();
|
|
294
|
+
processExitSpy = jest.spyOn(process, 'exit').mockImplementation();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
afterEach(() => {
|
|
298
|
+
jest.restoreAllMocks();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('repair command rejects non-AWS provider', async () => {
|
|
302
|
+
jest.mock('../../../utils/provider-helper', () => ({
|
|
303
|
+
loadProviderForCli: () => ({
|
|
304
|
+
appDefinition: { provider: 'netlify' },
|
|
305
|
+
provider: mockCreateProvider({ name: 'netlify' }),
|
|
306
|
+
providerName: 'netlify',
|
|
307
|
+
}),
|
|
308
|
+
}));
|
|
309
|
+
|
|
310
|
+
jest.mock('../../../utils/output', () => ({
|
|
311
|
+
info: jest.fn(),
|
|
312
|
+
error: jest.fn(),
|
|
313
|
+
log: jest.fn(),
|
|
314
|
+
success: jest.fn(),
|
|
315
|
+
warn: jest.fn(),
|
|
316
|
+
}));
|
|
317
|
+
|
|
318
|
+
jest.mock('../../../../infrastructure/domains/health/domain/value-objects/stack-identifier', () => jest.fn());
|
|
319
|
+
jest.mock('../../../../infrastructure/domains/health/application/use-cases/run-health-check-use-case', () => jest.fn());
|
|
320
|
+
jest.mock('../../../../infrastructure/domains/health/application/use-cases/repair-via-import-use-case', () => jest.fn());
|
|
321
|
+
jest.mock('../../../../infrastructure/domains/health/application/use-cases/reconcile-properties-use-case', () => jest.fn());
|
|
322
|
+
jest.mock('../../../../infrastructure/domains/health/application/use-cases/execute-resource-import-use-case', () => jest.fn());
|
|
323
|
+
jest.mock('../../../../infrastructure/domains/health/infrastructure/adapters/aws-stack-repository', () => jest.fn());
|
|
324
|
+
jest.mock('../../../../infrastructure/domains/health/infrastructure/adapters/aws-resource-detector', () => jest.fn());
|
|
325
|
+
jest.mock('../../../../infrastructure/domains/health/infrastructure/adapters/aws-resource-importer', () => jest.fn());
|
|
326
|
+
jest.mock('../../../../infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler', () => jest.fn());
|
|
327
|
+
jest.mock('../../../../infrastructure/domains/health/domain/services/mismatch-analyzer', () => jest.fn());
|
|
328
|
+
jest.mock('../../../../infrastructure/domains/health/domain/services/health-score-calculator', () => jest.fn());
|
|
329
|
+
jest.mock('../../../../infrastructure/domains/health/domain/services/template-parser', () => ({
|
|
330
|
+
TemplateParser: jest.fn(),
|
|
331
|
+
}));
|
|
332
|
+
jest.mock('../../../../infrastructure/domains/health/domain/services/import-template-generator', () => ({
|
|
333
|
+
ImportTemplateGenerator: jest.fn(),
|
|
334
|
+
}));
|
|
335
|
+
jest.mock('../../../../infrastructure/domains/health/domain/services/import-progress-monitor', () => ({
|
|
336
|
+
ImportProgressMonitor: jest.fn(),
|
|
337
|
+
}));
|
|
338
|
+
|
|
339
|
+
const { repairCommand } = require('../../../repair-command');
|
|
340
|
+
const output = require('../../../utils/output');
|
|
341
|
+
|
|
342
|
+
await repairCommand('my-stack', { import: true });
|
|
343
|
+
|
|
344
|
+
expect(output.error).toHaveBeenCalledWith(
|
|
345
|
+
expect.stringContaining('only available for AWS')
|
|
346
|
+
);
|
|
347
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('generate-iam command rejects non-AWS provider', async () => {
|
|
351
|
+
jest.mock('../../../utils/provider-helper', () => ({
|
|
352
|
+
loadProviderForCli: () => ({
|
|
353
|
+
appDefinition: { provider: 'netlify' },
|
|
354
|
+
provider: mockCreateProvider({ name: 'netlify' }),
|
|
355
|
+
providerName: 'netlify',
|
|
356
|
+
}),
|
|
357
|
+
}));
|
|
358
|
+
|
|
359
|
+
jest.mock('fs-extra', () => ({
|
|
360
|
+
existsSync: jest.fn(),
|
|
361
|
+
writeFileSync: jest.fn(),
|
|
362
|
+
ensureDirSync: jest.fn(),
|
|
363
|
+
}), { virtual: true });
|
|
364
|
+
|
|
365
|
+
jest.mock('@friggframework/core', () => ({
|
|
366
|
+
findNearestBackendPackageJson: jest.fn(),
|
|
367
|
+
}), { virtual: true });
|
|
368
|
+
|
|
369
|
+
jest.mock('../../../../infrastructure/domains/security/iam-generator', () => ({
|
|
370
|
+
generateIAMCloudFormation: jest.fn(),
|
|
371
|
+
getFeatureSummary: jest.fn(),
|
|
372
|
+
}));
|
|
373
|
+
|
|
374
|
+
const { generateIamCommand } = require('../../../generate-iam-command');
|
|
375
|
+
|
|
376
|
+
await generateIamCommand({});
|
|
377
|
+
|
|
378
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
379
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
380
|
+
expect.stringContaining('only available for AWS')
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/devtools",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.545.
|
|
4
|
+
"version": "2.0.0--canary.545.7b93757.0",
|
|
5
5
|
"bin": {
|
|
6
6
|
"frigg": "./frigg-cli/index.js"
|
|
7
7
|
},
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"@babel/eslint-parser": "^7.18.9",
|
|
26
26
|
"@babel/parser": "^7.25.3",
|
|
27
27
|
"@babel/traverse": "^7.25.3",
|
|
28
|
-
"@friggframework/core": "2.0.0--canary.545.
|
|
29
|
-
"@friggframework/schemas": "2.0.0--canary.545.
|
|
30
|
-
"@friggframework/test": "2.0.0--canary.545.
|
|
28
|
+
"@friggframework/core": "2.0.0--canary.545.7b93757.0",
|
|
29
|
+
"@friggframework/schemas": "2.0.0--canary.545.7b93757.0",
|
|
30
|
+
"@friggframework/test": "2.0.0--canary.545.7b93757.0",
|
|
31
31
|
"@hapi/boom": "^10.0.1",
|
|
32
32
|
"@inquirer/prompts": "^5.3.8",
|
|
33
33
|
"axios": "^1.7.2",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"validate-npm-package-name": "^5.0.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@friggframework/eslint-config": "2.0.0--canary.545.
|
|
59
|
-
"@friggframework/prettier-config": "2.0.0--canary.545.
|
|
58
|
+
"@friggframework/eslint-config": "2.0.0--canary.545.7b93757.0",
|
|
59
|
+
"@friggframework/prettier-config": "2.0.0--canary.545.7b93757.0",
|
|
60
60
|
"aws-sdk-client-mock": "^4.1.0",
|
|
61
61
|
"aws-sdk-client-mock-jest": "^4.1.0",
|
|
62
62
|
"exit-x": "^0.2.2",
|
|
@@ -89,5 +89,5 @@
|
|
|
89
89
|
"publishConfig": {
|
|
90
90
|
"access": "public"
|
|
91
91
|
},
|
|
92
|
-
"gitHead": "
|
|
92
|
+
"gitHead": "7b93757c03681d277e1abd49aa475bd09f7f2e6a"
|
|
93
93
|
}
|