@friggframework/devtools 2.0.0-next.47 → 2.0.0-next.48
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/README.md +1290 -0
- package/frigg-cli/__tests__/unit/commands/build.test.js +279 -0
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +548 -0
- package/frigg-cli/__tests__/unit/commands/deploy.test.js +320 -0
- package/frigg-cli/__tests__/unit/commands/doctor.test.js +309 -0
- package/frigg-cli/__tests__/unit/commands/install.test.js +400 -0
- package/frigg-cli/__tests__/unit/commands/ui.test.js +346 -0
- package/frigg-cli/__tests__/unit/dependencies.test.js +74 -0
- 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/version-detection.test.js +171 -0
- package/frigg-cli/__tests__/utils/mock-factory.js +270 -0
- package/frigg-cli/__tests__/utils/prisma-mock.js +194 -0
- package/frigg-cli/__tests__/utils/test-fixtures.js +463 -0
- package/frigg-cli/__tests__/utils/test-setup.js +287 -0
- package/frigg-cli/build-command/index.js +66 -0
- package/frigg-cli/db-setup-command/index.js +193 -0
- package/frigg-cli/deploy-command/SPEC-DEPLOY-DRY-RUN.md +981 -0
- package/frigg-cli/deploy-command/index.js +302 -0
- package/frigg-cli/doctor-command/index.js +335 -0
- package/frigg-cli/generate-command/__tests__/generate-command.test.js +301 -0
- package/frigg-cli/generate-command/azure-generator.js +43 -0
- package/frigg-cli/generate-command/gcp-generator.js +47 -0
- package/frigg-cli/generate-command/index.js +332 -0
- package/frigg-cli/generate-command/terraform-generator.js +555 -0
- package/frigg-cli/generate-iam-command.js +118 -0
- package/frigg-cli/index.js +173 -0
- package/frigg-cli/index.test.js +158 -0
- package/frigg-cli/init-command/backend-first-handler.js +756 -0
- package/frigg-cli/init-command/index.js +93 -0
- package/frigg-cli/init-command/template-handler.js +143 -0
- package/frigg-cli/install-command/backend-js.js +33 -0
- package/frigg-cli/install-command/commit-changes.js +16 -0
- package/frigg-cli/install-command/environment-variables.js +127 -0
- package/frigg-cli/install-command/environment-variables.test.js +136 -0
- package/frigg-cli/install-command/index.js +54 -0
- package/frigg-cli/install-command/install-package.js +13 -0
- package/frigg-cli/install-command/integration-file.js +30 -0
- package/frigg-cli/install-command/logger.js +12 -0
- package/frigg-cli/install-command/template.js +90 -0
- package/frigg-cli/install-command/validate-package.js +75 -0
- package/frigg-cli/jest.config.js +124 -0
- package/frigg-cli/package.json +63 -0
- package/frigg-cli/repair-command/index.js +564 -0
- package/frigg-cli/start-command/index.js +149 -0
- package/frigg-cli/start-command/start-command.test.js +297 -0
- package/frigg-cli/test/init-command.test.js +180 -0
- package/frigg-cli/test/npm-registry.test.js +319 -0
- package/frigg-cli/ui-command/index.js +154 -0
- package/frigg-cli/utils/app-resolver.js +319 -0
- package/frigg-cli/utils/backend-path.js +25 -0
- package/frigg-cli/utils/database-validator.js +154 -0
- package/frigg-cli/utils/error-messages.js +257 -0
- package/frigg-cli/utils/npm-registry.js +167 -0
- package/frigg-cli/utils/process-manager.js +199 -0
- package/frigg-cli/utils/repo-detection.js +405 -0
- package/infrastructure/create-frigg-infrastructure.js +125 -12
- package/infrastructure/docs/PRE-DEPLOYMENT-HEALTH-CHECK-SPEC.md +1317 -0
- package/infrastructure/domains/shared/resource-discovery.enhanced.test.js +306 -0
- package/infrastructure/domains/shared/resource-discovery.js +31 -2
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +1 -1
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +109 -5
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +310 -4
- package/infrastructure/domains/shared/validation/plugin-validator.js +187 -0
- package/infrastructure/domains/shared/validation/plugin-validator.test.js +323 -0
- package/infrastructure/infrastructure-composer.js +22 -0
- package/layers/prisma/.build-complete +3 -0
- package/package.json +18 -7
- package/management-ui/package-lock.json +0 -16517
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test suite for deploy command
|
|
3
|
+
*
|
|
4
|
+
* Tests the serverless deployment functionality including:
|
|
5
|
+
* - Command execution with spawn
|
|
6
|
+
* - Stage option handling
|
|
7
|
+
* - Environment variable filtering and propagation
|
|
8
|
+
* - SLS_STAGE propagation for resource discovery
|
|
9
|
+
* - Error handling
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Mock dependencies BEFORE requiring modules
|
|
13
|
+
jest.mock('child_process', () => ({
|
|
14
|
+
spawn: jest.fn()
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
jest.mock('fs', () => ({
|
|
18
|
+
existsSync: jest.fn()
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Require after mocks
|
|
22
|
+
const { spawn } = require('child_process');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const { deployCommand } = require('../../../deploy-command');
|
|
25
|
+
|
|
26
|
+
describe('CLI Command: deploy', () => {
|
|
27
|
+
let consoleLogSpy;
|
|
28
|
+
let consoleWarnSpy;
|
|
29
|
+
let consoleErrorSpy;
|
|
30
|
+
let mockChildProcess;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
jest.clearAllMocks();
|
|
34
|
+
|
|
35
|
+
// Mock console methods
|
|
36
|
+
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
37
|
+
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
38
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
39
|
+
|
|
40
|
+
// Mock child process
|
|
41
|
+
mockChildProcess = {
|
|
42
|
+
on: jest.fn()
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Mock successful spawn by default
|
|
46
|
+
spawn.mockReturnValue(mockChildProcess);
|
|
47
|
+
|
|
48
|
+
// Mock fs.existsSync to return false (no app definition)
|
|
49
|
+
fs.existsSync.mockReturnValue(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
consoleLogSpy.mockRestore();
|
|
54
|
+
consoleWarnSpy.mockRestore();
|
|
55
|
+
consoleErrorSpy.mockRestore();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('Success Cases', () => {
|
|
59
|
+
it('should spawn serverless with default stage', async () => {
|
|
60
|
+
await deployCommand({ stage: 'dev' });
|
|
61
|
+
|
|
62
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
63
|
+
'osls',
|
|
64
|
+
['deploy', '--config', 'infrastructure.js', '--stage', 'dev'],
|
|
65
|
+
expect.objectContaining({
|
|
66
|
+
cwd: expect.any(String),
|
|
67
|
+
stdio: 'inherit'
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should spawn serverless with production stage', async () => {
|
|
73
|
+
await deployCommand({ stage: 'production' });
|
|
74
|
+
|
|
75
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
76
|
+
'osls',
|
|
77
|
+
expect.arrayContaining(['--stage', 'production']),
|
|
78
|
+
expect.any(Object)
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should spawn serverless with qa stage', async () => {
|
|
83
|
+
await deployCommand({ stage: 'qa' });
|
|
84
|
+
|
|
85
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
86
|
+
'osls',
|
|
87
|
+
expect.arrayContaining(['--stage', 'qa']),
|
|
88
|
+
expect.any(Object)
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should spawn serverless with --force flag when force option is true', async () => {
|
|
93
|
+
await deployCommand({ stage: 'dev', force: true });
|
|
94
|
+
|
|
95
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
96
|
+
'osls',
|
|
97
|
+
['deploy', '--config', 'infrastructure.js', '--stage', 'dev', '--force'],
|
|
98
|
+
expect.objectContaining({
|
|
99
|
+
cwd: expect.any(String),
|
|
100
|
+
stdio: 'inherit'
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should spawn serverless without --force flag when force option is false', async () => {
|
|
106
|
+
await deployCommand({ stage: 'dev', force: false });
|
|
107
|
+
|
|
108
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
109
|
+
'osls',
|
|
110
|
+
['deploy', '--config', 'infrastructure.js', '--stage', 'dev'],
|
|
111
|
+
expect.objectContaining({
|
|
112
|
+
cwd: expect.any(String),
|
|
113
|
+
stdio: 'inherit'
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should spawn serverless without --force flag when force option is undefined', async () => {
|
|
119
|
+
await deployCommand({ stage: 'dev' });
|
|
120
|
+
|
|
121
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
122
|
+
'osls',
|
|
123
|
+
['deploy', '--config', 'infrastructure.js', '--stage', 'dev'],
|
|
124
|
+
expect.objectContaining({
|
|
125
|
+
cwd: expect.any(String),
|
|
126
|
+
stdio: 'inherit'
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should use process.cwd() as working directory', async () => {
|
|
132
|
+
await deployCommand({ stage: 'dev' });
|
|
133
|
+
|
|
134
|
+
const call = spawn.mock.calls[0];
|
|
135
|
+
const options = call[2];
|
|
136
|
+
|
|
137
|
+
expect(options.cwd).toBe(process.cwd());
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should use stdio inherit for output streaming', async () => {
|
|
141
|
+
await deployCommand({ stage: 'dev' });
|
|
142
|
+
|
|
143
|
+
const call = spawn.mock.calls[0];
|
|
144
|
+
const options = call[2];
|
|
145
|
+
|
|
146
|
+
expect(options.stdio).toBe('inherit');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should set SLS_STAGE environment variable to match stage option', async () => {
|
|
150
|
+
await deployCommand({ stage: 'qa' });
|
|
151
|
+
|
|
152
|
+
const call = spawn.mock.calls[0];
|
|
153
|
+
const options = call[2];
|
|
154
|
+
|
|
155
|
+
// Verify SLS_STAGE is set for discovery to use
|
|
156
|
+
expect(options.env.SLS_STAGE).toBe('qa');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should set SLS_STAGE for production stage', async () => {
|
|
160
|
+
await deployCommand({ stage: 'production' });
|
|
161
|
+
|
|
162
|
+
const call = spawn.mock.calls[0];
|
|
163
|
+
const options = call[2];
|
|
164
|
+
|
|
165
|
+
expect(options.env.SLS_STAGE).toBe('production');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should set SLS_STAGE for dev stage', async () => {
|
|
169
|
+
await deployCommand({ stage: 'dev' });
|
|
170
|
+
|
|
171
|
+
const call = spawn.mock.calls[0];
|
|
172
|
+
const options = call[2];
|
|
173
|
+
|
|
174
|
+
expect(options.env.SLS_STAGE).toBe('dev');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should use infrastructure.js as config file', async () => {
|
|
178
|
+
await deployCommand({ stage: 'dev' });
|
|
179
|
+
|
|
180
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
181
|
+
'osls',
|
|
182
|
+
expect.arrayContaining(['--config', 'infrastructure.js']),
|
|
183
|
+
expect.any(Object)
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should log deployment start messages', async () => {
|
|
188
|
+
await deployCommand({ stage: 'dev' });
|
|
189
|
+
|
|
190
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('Deploying the serverless application...');
|
|
191
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('🚀 Deploying serverless application...');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should include essential system environment variables', async () => {
|
|
195
|
+
process.env.PATH = '/usr/bin';
|
|
196
|
+
process.env.HOME = '/home/user';
|
|
197
|
+
process.env.USER = 'testuser';
|
|
198
|
+
|
|
199
|
+
await deployCommand({ stage: 'dev' });
|
|
200
|
+
|
|
201
|
+
const call = spawn.mock.calls[0];
|
|
202
|
+
const options = call[2];
|
|
203
|
+
|
|
204
|
+
expect(options.env.PATH).toBe('/usr/bin');
|
|
205
|
+
expect(options.env.HOME).toBe('/home/user');
|
|
206
|
+
expect(options.env.USER).toBe('testuser');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should include AWS environment variables', async () => {
|
|
210
|
+
process.env.AWS_REGION = 'us-east-1';
|
|
211
|
+
process.env.AWS_PROFILE = 'test-profile';
|
|
212
|
+
process.env.AWS_ACCESS_KEY_ID = 'test-key';
|
|
213
|
+
|
|
214
|
+
await deployCommand({ stage: 'dev' });
|
|
215
|
+
|
|
216
|
+
const call = spawn.mock.calls[0];
|
|
217
|
+
const options = call[2];
|
|
218
|
+
|
|
219
|
+
expect(options.env.AWS_REGION).toBe('us-east-1');
|
|
220
|
+
expect(options.env.AWS_PROFILE).toBe('test-profile');
|
|
221
|
+
expect(options.env.AWS_ACCESS_KEY_ID).toBe('test-key');
|
|
222
|
+
|
|
223
|
+
delete process.env.AWS_REGION;
|
|
224
|
+
delete process.env.AWS_PROFILE;
|
|
225
|
+
delete process.env.AWS_ACCESS_KEY_ID;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should register error handler', async () => {
|
|
229
|
+
await deployCommand({ stage: 'dev' });
|
|
230
|
+
|
|
231
|
+
expect(mockChildProcess.on).toHaveBeenCalledWith('error', expect.any(Function));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should register close handler', async () => {
|
|
235
|
+
await deployCommand({ stage: 'dev' });
|
|
236
|
+
|
|
237
|
+
expect(mockChildProcess.on).toHaveBeenCalledWith('close', expect.any(Function));
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('Environment Variable Filtering', () => {
|
|
242
|
+
it('should filter environment variables when app definition exists', async () => {
|
|
243
|
+
// Mock app definition with environment config
|
|
244
|
+
fs.existsSync.mockReturnValue(true);
|
|
245
|
+
jest.mock(
|
|
246
|
+
process.cwd() + '/index.js',
|
|
247
|
+
() => ({
|
|
248
|
+
Definition: {
|
|
249
|
+
environment: {
|
|
250
|
+
DATABASE_URL: true,
|
|
251
|
+
API_KEY: true
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}),
|
|
255
|
+
{ virtual: true }
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
process.env.DATABASE_URL = 'postgres://localhost';
|
|
259
|
+
process.env.API_KEY = 'test-key';
|
|
260
|
+
process.env.RANDOM_VAR = 'should-not-be-included';
|
|
261
|
+
|
|
262
|
+
await deployCommand({ stage: 'dev' });
|
|
263
|
+
|
|
264
|
+
const call = spawn.mock.calls[0];
|
|
265
|
+
const options = call[2];
|
|
266
|
+
|
|
267
|
+
// Should include app-defined variables
|
|
268
|
+
expect(options.env.DATABASE_URL).toBe('postgres://localhost');
|
|
269
|
+
expect(options.env.API_KEY).toBe('test-key');
|
|
270
|
+
|
|
271
|
+
// Should NOT include non-app-defined variables (except system/AWS)
|
|
272
|
+
expect(options.env.RANDOM_VAR).toBeUndefined();
|
|
273
|
+
|
|
274
|
+
delete process.env.DATABASE_URL;
|
|
275
|
+
delete process.env.API_KEY;
|
|
276
|
+
delete process.env.RANDOM_VAR;
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('Error Handling', () => {
|
|
281
|
+
it('should log error when spawn fails', async () => {
|
|
282
|
+
const testError = new Error('Spawn failed');
|
|
283
|
+
|
|
284
|
+
await deployCommand({ stage: 'dev' });
|
|
285
|
+
|
|
286
|
+
// Simulate error event
|
|
287
|
+
const errorHandler = mockChildProcess.on.mock.calls.find(
|
|
288
|
+
call => call[0] === 'error'
|
|
289
|
+
)[1];
|
|
290
|
+
errorHandler(testError);
|
|
291
|
+
|
|
292
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error executing command: Spawn failed');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should log when child process exits with non-zero code', async () => {
|
|
296
|
+
await deployCommand({ stage: 'dev' });
|
|
297
|
+
|
|
298
|
+
// Simulate close event with error code
|
|
299
|
+
const closeHandler = mockChildProcess.on.mock.calls.find(
|
|
300
|
+
call => call[0] === 'close'
|
|
301
|
+
)[1];
|
|
302
|
+
closeHandler(1);
|
|
303
|
+
|
|
304
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('Child process exited with code 1');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should NOT log when child process exits with zero code', async () => {
|
|
308
|
+
await deployCommand({ stage: 'dev' });
|
|
309
|
+
|
|
310
|
+
// Simulate close event with success code
|
|
311
|
+
const closeHandler = mockChildProcess.on.mock.calls.find(
|
|
312
|
+
call => call[0] === 'close'
|
|
313
|
+
)[1];
|
|
314
|
+
closeHandler(0);
|
|
315
|
+
|
|
316
|
+
// Should not log exit message for success
|
|
317
|
+
expect(consoleLogSpy).not.toHaveBeenCalledWith('Child process exited with code 0');
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
});
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for frigg doctor command
|
|
3
|
+
* Tests stack listing, selection, and health check orchestration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { describe, test, expect, jest, beforeEach } = require('@jest/globals');
|
|
7
|
+
|
|
8
|
+
describe('Doctor Command - Stack Listing and Selection', () => {
|
|
9
|
+
let mockCloudFormationClient;
|
|
10
|
+
let mockSelect;
|
|
11
|
+
let listStacks;
|
|
12
|
+
let promptForStackSelection;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
|
|
17
|
+
// Mock AWS SDK CloudFormation client
|
|
18
|
+
mockCloudFormationClient = {
|
|
19
|
+
send: jest.fn(),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Mock @inquirer/prompts select function
|
|
23
|
+
mockSelect = jest.fn();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('listStacks', () => {
|
|
27
|
+
test('should return array of stacks with name, status, and timestamps', async () => {
|
|
28
|
+
// Arrange
|
|
29
|
+
const mockResponse = {
|
|
30
|
+
StackSummaries: [
|
|
31
|
+
{
|
|
32
|
+
StackName: 'quo-frigg-production',
|
|
33
|
+
StackStatus: 'UPDATE_COMPLETE',
|
|
34
|
+
CreationTime: new Date('2024-01-15'),
|
|
35
|
+
LastUpdatedTime: new Date('2024-10-20'),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
StackName: 'test-app-dev',
|
|
39
|
+
StackStatus: 'CREATE_COMPLETE',
|
|
40
|
+
CreationTime: new Date('2024-10-01'),
|
|
41
|
+
LastUpdatedTime: null,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
mockCloudFormationClient.send.mockResolvedValue(mockResponse);
|
|
47
|
+
|
|
48
|
+
// Act
|
|
49
|
+
const { CloudFormationClient, ListStacksCommand } = require('@aws-sdk/client-cloudformation');
|
|
50
|
+
const client = new CloudFormationClient({ region: 'us-east-1' });
|
|
51
|
+
const command = new ListStacksCommand({
|
|
52
|
+
StackStatusFilter: ['CREATE_COMPLETE', 'UPDATE_COMPLETE', 'UPDATE_ROLLBACK_COMPLETE', 'ROLLBACK_COMPLETE'],
|
|
53
|
+
});
|
|
54
|
+
const response = await mockCloudFormationClient.send(command);
|
|
55
|
+
const stacks = response.StackSummaries.map(stack => ({
|
|
56
|
+
name: stack.StackName,
|
|
57
|
+
status: stack.StackStatus,
|
|
58
|
+
createdTime: stack.CreationTime,
|
|
59
|
+
updatedTime: stack.LastUpdatedTime,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
// Assert
|
|
63
|
+
expect(stacks).toHaveLength(2);
|
|
64
|
+
expect(stacks[0]).toEqual({
|
|
65
|
+
name: 'quo-frigg-production',
|
|
66
|
+
status: 'UPDATE_COMPLETE',
|
|
67
|
+
createdTime: new Date('2024-01-15'),
|
|
68
|
+
updatedTime: new Date('2024-10-20'),
|
|
69
|
+
});
|
|
70
|
+
expect(stacks[1]).toEqual({
|
|
71
|
+
name: 'test-app-dev',
|
|
72
|
+
status: 'CREATE_COMPLETE',
|
|
73
|
+
createdTime: new Date('2024-10-01'),
|
|
74
|
+
updatedTime: null,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('should filter stacks by completed statuses only', async () => {
|
|
79
|
+
// Arrange - command should request only completed stacks
|
|
80
|
+
const expectedFilter = [
|
|
81
|
+
'CREATE_COMPLETE',
|
|
82
|
+
'UPDATE_COMPLETE',
|
|
83
|
+
'UPDATE_ROLLBACK_COMPLETE',
|
|
84
|
+
'ROLLBACK_COMPLETE',
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
// Act
|
|
88
|
+
const { ListStacksCommand } = require('@aws-sdk/client-cloudformation');
|
|
89
|
+
const command = new ListStacksCommand({
|
|
90
|
+
StackStatusFilter: expectedFilter,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Assert
|
|
94
|
+
expect(command.input.StackStatusFilter).toEqual(expectedFilter);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('should handle empty stack list', async () => {
|
|
98
|
+
// Arrange
|
|
99
|
+
const mockResponse = {
|
|
100
|
+
StackSummaries: [],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
mockCloudFormationClient.send.mockResolvedValue(mockResponse);
|
|
104
|
+
|
|
105
|
+
// Act
|
|
106
|
+
const response = await mockCloudFormationClient.send();
|
|
107
|
+
const stacks = (response.StackSummaries || []).map(stack => ({
|
|
108
|
+
name: stack.StackName,
|
|
109
|
+
status: stack.StackStatus,
|
|
110
|
+
createdTime: stack.CreationTime,
|
|
111
|
+
updatedTime: stack.LastUpdatedTime,
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
// Assert
|
|
115
|
+
expect(stacks).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('should throw error with helpful message when API call fails', async () => {
|
|
119
|
+
// Arrange
|
|
120
|
+
const apiError = new Error('AccessDenied: User is not authorized');
|
|
121
|
+
mockCloudFormationClient.send.mockRejectedValue(apiError);
|
|
122
|
+
|
|
123
|
+
// Act & Assert
|
|
124
|
+
await expect(async () => {
|
|
125
|
+
try {
|
|
126
|
+
await mockCloudFormationClient.send();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw new Error(`Failed to list CloudFormation stacks: ${error.message}`);
|
|
129
|
+
}
|
|
130
|
+
}).rejects.toThrow('Failed to list CloudFormation stacks: AccessDenied: User is not authorized');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('promptForStackSelection', () => {
|
|
135
|
+
test('should display stacks with status icons and metadata', async () => {
|
|
136
|
+
// Arrange
|
|
137
|
+
const mockStacks = [
|
|
138
|
+
{
|
|
139
|
+
name: 'production-stack',
|
|
140
|
+
status: 'UPDATE_COMPLETE',
|
|
141
|
+
createdTime: new Date('2024-01-15'),
|
|
142
|
+
updatedTime: new Date('2024-10-20'),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'dev-stack',
|
|
146
|
+
status: 'CREATE_COMPLETE',
|
|
147
|
+
createdTime: new Date('2024-10-01'),
|
|
148
|
+
updatedTime: null,
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const expectedChoices = [
|
|
153
|
+
{
|
|
154
|
+
name: '✓ production-stack (UPDATE_COMPLETE) - Updated: 10/20/2024',
|
|
155
|
+
value: 'production-stack',
|
|
156
|
+
description: 'Status: UPDATE_COMPLETE',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: '✓ dev-stack (CREATE_COMPLETE) - Created: 10/1/2024',
|
|
160
|
+
value: 'dev-stack',
|
|
161
|
+
description: 'Status: CREATE_COMPLETE',
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
mockSelect.mockResolvedValue('production-stack');
|
|
166
|
+
|
|
167
|
+
// Act
|
|
168
|
+
const choices = mockStacks.map(stack => {
|
|
169
|
+
const statusIcon = stack.status.includes('COMPLETE') ? '✓' : '⚠';
|
|
170
|
+
const timeInfo = stack.updatedTime
|
|
171
|
+
? `Updated: ${stack.updatedTime.toLocaleDateString()}`
|
|
172
|
+
: `Created: ${stack.createdTime.toLocaleDateString()}`;
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
name: `${statusIcon} ${stack.name} (${stack.status}) - ${timeInfo}`,
|
|
176
|
+
value: stack.name,
|
|
177
|
+
description: `Status: ${stack.status}`,
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const selectedStack = await mockSelect({
|
|
182
|
+
message: 'Select a stack to run health check:',
|
|
183
|
+
choices,
|
|
184
|
+
pageSize: 15,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Assert
|
|
188
|
+
expect(choices).toEqual(expectedChoices);
|
|
189
|
+
expect(selectedStack).toBe('production-stack');
|
|
190
|
+
expect(mockSelect).toHaveBeenCalledWith({
|
|
191
|
+
message: 'Select a stack to run health check:',
|
|
192
|
+
choices: expectedChoices,
|
|
193
|
+
pageSize: 15,
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('should exit with error when no stacks are found', async () => {
|
|
198
|
+
// Arrange
|
|
199
|
+
const mockStacks = [];
|
|
200
|
+
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
201
|
+
const mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
202
|
+
const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
203
|
+
|
|
204
|
+
// Act
|
|
205
|
+
if (mockStacks.length === 0) {
|
|
206
|
+
console.error('\n✗ No CloudFormation stacks found in us-east-1');
|
|
207
|
+
console.log(' Make sure you have stacks deployed and the correct AWS credentials configured.');
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Assert
|
|
212
|
+
expect(mockConsoleError).toHaveBeenCalledWith('\n✗ No CloudFormation stacks found in us-east-1');
|
|
213
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(' Make sure you have stacks deployed and the correct AWS credentials configured.');
|
|
214
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
215
|
+
|
|
216
|
+
mockExit.mockRestore();
|
|
217
|
+
mockConsoleError.mockRestore();
|
|
218
|
+
mockConsoleLog.mockRestore();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('should return selected stack name', async () => {
|
|
222
|
+
// Arrange
|
|
223
|
+
const mockStacks = [
|
|
224
|
+
{ name: 'stack-a', status: 'UPDATE_COMPLETE', createdTime: new Date(), updatedTime: new Date() },
|
|
225
|
+
{ name: 'stack-b', status: 'CREATE_COMPLETE', createdTime: new Date(), updatedTime: null },
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
mockSelect.mockResolvedValue('stack-b');
|
|
229
|
+
|
|
230
|
+
// Act
|
|
231
|
+
const choices = mockStacks.map(stack => ({
|
|
232
|
+
name: `${stack.name} (${stack.status})`,
|
|
233
|
+
value: stack.name,
|
|
234
|
+
description: `Status: ${stack.status}`,
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
const selectedStack = await mockSelect({
|
|
238
|
+
message: 'Select a stack to run health check:',
|
|
239
|
+
choices,
|
|
240
|
+
pageSize: 15,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Assert
|
|
244
|
+
expect(selectedStack).toBe('stack-b');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('should handle user cancellation (Ctrl+C)', async () => {
|
|
248
|
+
// Arrange
|
|
249
|
+
mockSelect.mockRejectedValue(new Error('User cancelled'));
|
|
250
|
+
|
|
251
|
+
// Act & Assert
|
|
252
|
+
await expect(mockSelect({ message: 'Select a stack:', choices: [] }))
|
|
253
|
+
.rejects.toThrow('User cancelled');
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('doctorCommand integration with stack selection', () => {
|
|
258
|
+
test('should prompt for stack selection when stackName is not provided', async () => {
|
|
259
|
+
// Arrange
|
|
260
|
+
const mockPromptForStackSelection = jest.fn().mockResolvedValue('selected-stack');
|
|
261
|
+
const stackName = undefined;
|
|
262
|
+
const region = 'us-east-1';
|
|
263
|
+
|
|
264
|
+
// Act
|
|
265
|
+
let selectedStack = stackName;
|
|
266
|
+
if (!selectedStack) {
|
|
267
|
+
selectedStack = await mockPromptForStackSelection(region);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Assert
|
|
271
|
+
expect(mockPromptForStackSelection).toHaveBeenCalledWith('us-east-1');
|
|
272
|
+
expect(selectedStack).toBe('selected-stack');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('should use provided stackName when given', async () => {
|
|
276
|
+
// Arrange
|
|
277
|
+
const mockPromptForStackSelection = jest.fn();
|
|
278
|
+
const stackName = 'my-production-stack';
|
|
279
|
+
const region = 'us-east-1';
|
|
280
|
+
|
|
281
|
+
// Act
|
|
282
|
+
let selectedStack = stackName;
|
|
283
|
+
if (!selectedStack) {
|
|
284
|
+
selectedStack = await mockPromptForStackSelection(region);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Assert
|
|
288
|
+
expect(mockPromptForStackSelection).not.toHaveBeenCalled();
|
|
289
|
+
expect(selectedStack).toBe('my-production-stack');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('should use region from options or default to us-east-1', () => {
|
|
293
|
+
// Test with region provided
|
|
294
|
+
const options1 = { region: 'eu-west-1' };
|
|
295
|
+
const region1 = options1.region || process.env.AWS_REGION || 'us-east-1';
|
|
296
|
+
expect(region1).toBe('eu-west-1');
|
|
297
|
+
|
|
298
|
+
// Test with no region (and no env var)
|
|
299
|
+
const oldEnv = process.env.AWS_REGION;
|
|
300
|
+
delete process.env.AWS_REGION;
|
|
301
|
+
const options2 = {};
|
|
302
|
+
const region2 = options2.region || process.env.AWS_REGION || 'us-east-1';
|
|
303
|
+
expect(region2).toBe('us-east-1');
|
|
304
|
+
|
|
305
|
+
// Restore
|
|
306
|
+
if (oldEnv) process.env.AWS_REGION = oldEnv;
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
});
|