|
1 | 1 | import * as path from 'node:path';
|
| 2 | +import { EnvironmentUtils } from '@aws-cdk/cx-api'; |
2 | 3 | import type { Stack } from '@aws-sdk/client-cloudformation';
|
3 | 4 | import {
|
4 | 5 | CreateChangeSetCommand,
|
|
8 | 9 | ExecuteChangeSetCommand,
|
9 | 10 | } from '@aws-sdk/client-cloudformation';
|
10 | 11 | import { bold } from 'chalk';
|
| 12 | + |
11 | 13 | import type { BootstrapOptions } from '../../lib/actions/bootstrap';
|
12 | 14 | import { BootstrapEnvironments, BootstrapSource, BootstrapStackParameters } from '../../lib/actions/bootstrap';
|
13 | 15 | import { SdkProvider } from '../../lib/api/aws-cdk';
|
@@ -92,6 +94,11 @@ async function runBootstrap(options?: {
|
92 | 94 | });
|
93 | 95 | }
|
94 | 96 |
|
| 97 | +function expectValidBootstrapResult(result: any) { |
| 98 | + expect(result).toHaveProperty('environments'); |
| 99 | + expect(Array.isArray(result.environments)).toBe(true); |
| 100 | +} |
| 101 | + |
95 | 102 | function expectSuccessfulBootstrap() {
|
96 | 103 | expect(mockCloudFormationClient.calls().length).toBeGreaterThan(0);
|
97 | 104 | expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
|
@@ -132,9 +139,12 @@ describe('bootstrap', () => {
|
132 | 139 | setupMockCloudFormationClient(mockStack2);
|
133 | 140 |
|
134 | 141 | // WHEN
|
135 |
| - await runBootstrap({ environments: ['aws://123456789012/us-east-1', 'aws://234567890123/eu-west-1'] }); |
| 142 | + const result = await runBootstrap({ environments: ['aws://123456789012/us-east-1', 'aws://234567890123/eu-west-1'] }); |
136 | 143 |
|
137 | 144 | // THEN
|
| 145 | + expectValidBootstrapResult(result); |
| 146 | + expect(result.environments.length).toBe(2); |
| 147 | + |
138 | 148 | expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
|
139 | 149 | message: expect.stringContaining(`${bold('aws://123456789012/us-east-1')}: bootstrapping...`),
|
140 | 150 | }));
|
@@ -168,35 +178,20 @@ describe('bootstrap', () => {
|
168 | 178 |
|
169 | 179 | test('handles errors in user-specified environments', async () => {
|
170 | 180 | // GIVEN
|
171 |
| - const mockStack = createMockStack([ |
172 |
| - { OutputKey: 'BucketName', OutputValue: 'BUCKET_NAME' }, |
173 |
| - { OutputKey: 'BucketDomainName', OutputValue: 'BUCKET_ENDPOINT' }, |
174 |
| - { OutputKey: 'BootstrapVersion', OutputValue: '1' }, |
175 |
| - ]); |
176 |
| - setupMockCloudFormationClient(mockStack); |
177 |
| - |
178 |
| - // Mock an access denied error |
179 |
| - const accessDeniedError = new Error('Access Denied'); |
180 |
| - accessDeniedError.name = 'AccessDeniedException'; |
| 181 | + const error = new Error('Access Denied'); |
| 182 | + error.name = 'AccessDeniedException'; |
181 | 183 | mockCloudFormationClient
|
182 | 184 | .on(CreateChangeSetCommand)
|
183 |
| - .rejects(accessDeniedError); |
| 185 | + .rejects(error); |
184 | 186 |
|
185 | 187 | // WHEN/THEN
|
186 | 188 | await expect(runBootstrap({ environments: ['aws://123456789012/us-east-1'] }))
|
187 | 189 | .rejects.toThrow('Access Denied');
|
188 |
| - |
189 |
| - // Get all error notifications |
190 |
| - const errorCalls = ioHost.notifySpy.mock.calls |
191 |
| - .filter(call => call[0].level === 'error') |
192 |
| - .map(call => call[0]); |
193 |
| - |
194 |
| - // Verify error notifications |
195 |
| - expect(errorCalls).toContainEqual(expect.objectContaining({ |
| 190 | + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ |
196 | 191 | level: 'error',
|
197 | 192 | message: expect.stringContaining('❌'),
|
198 | 193 | }));
|
199 |
| - expect(errorCalls).toContainEqual(expect.objectContaining({ |
| 194 | + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ |
200 | 195 | level: 'error',
|
201 | 196 | message: expect.stringContaining(`${bold('aws://123456789012/us-east-1')} failed: Access Denied`),
|
202 | 197 | }));
|
@@ -492,37 +487,111 @@ describe('bootstrap', () => {
|
492 | 487 | });
|
493 | 488 |
|
494 | 489 | describe('error handling', () => {
|
495 |
| - test('handles generic bootstrap errors', async () => { |
| 490 | + test('returns correct BootstrapResult for successful bootstraps', async () => { |
496 | 491 | // GIVEN
|
497 |
| - mockCloudFormationClient.onAnyCommand().rejects(new Error('Bootstrap failed')); |
| 492 | + const mockStack = createMockStack([ |
| 493 | + { OutputKey: 'BucketName', OutputValue: 'BUCKET_NAME' }, |
| 494 | + { OutputKey: 'BucketDomainName', OutputValue: 'BUCKET_ENDPOINT' }, |
| 495 | + { OutputKey: 'BootstrapVersion', OutputValue: '1' }, |
| 496 | + ]); |
| 497 | + setupMockCloudFormationClient(mockStack); |
498 | 498 |
|
499 | 499 | // WHEN
|
500 |
| - await expect(runBootstrap()).rejects.toThrow('Bootstrap failed'); |
| 500 | + const result = await runBootstrap({ environments: ['aws://123456789012/us-east-1'] }); |
501 | 501 |
|
502 | 502 | // THEN
|
| 503 | + expectValidBootstrapResult(result); |
| 504 | + expect(result.environments.length).toBe(1); |
| 505 | + expect(result.environments[0].status).toBe('success'); |
| 506 | + expect(result.environments[0].environment).toStrictEqual(EnvironmentUtils.make('123456789012', 'us-east-1')); |
| 507 | + expect(result.environments[0].duration).toBeGreaterThan(0); |
| 508 | + }); |
| 509 | + |
| 510 | + test('returns correct BootstrapResult for no-op scenarios', async () => { |
| 511 | + // GIVEN |
| 512 | + const mockExistingStack = { |
| 513 | + StackId: 'mock-stack-id', |
| 514 | + StackName: 'CDKToolkit', |
| 515 | + StackStatus: 'CREATE_COMPLETE', |
| 516 | + CreationTime: new Date(), |
| 517 | + LastUpdatedTime: new Date(), |
| 518 | + Outputs: [ |
| 519 | + { OutputKey: 'BucketName', OutputValue: 'BUCKET_NAME' }, |
| 520 | + { OutputKey: 'BucketDomainName', OutputValue: 'BUCKET_ENDPOINT' }, |
| 521 | + { OutputKey: 'BootstrapVersion', OutputValue: '1' }, |
| 522 | + ], |
| 523 | + } as Stack; |
| 524 | + |
| 525 | + mockCloudFormationClient |
| 526 | + .on(DescribeStacksCommand) |
| 527 | + .resolves({ Stacks: [mockExistingStack] }) |
| 528 | + .on(CreateChangeSetCommand) |
| 529 | + .resolves({ Id: 'CHANGESET_ID' }) |
| 530 | + .on(DescribeChangeSetCommand) |
| 531 | + .resolves({ |
| 532 | + Status: 'FAILED', |
| 533 | + StatusReason: 'No updates are to be performed.', |
| 534 | + Changes: [], |
| 535 | + }); |
| 536 | + |
| 537 | + // WHEN |
| 538 | + const result = await runBootstrap({ environments: ['aws://123456789012/us-east-1'] }); |
| 539 | + |
| 540 | + // THEN |
| 541 | + expectValidBootstrapResult(result); |
| 542 | + expect(result.environments.length).toBe(1); |
| 543 | + expect(result.environments[0].status).toBe('no-op'); |
| 544 | + expect(result.environments[0].environment).toStrictEqual(EnvironmentUtils.make('123456789012', 'us-east-1')); |
| 545 | + expect(result.environments[0].duration).toBeGreaterThan(0); |
| 546 | + }); |
| 547 | + |
| 548 | + test('returns correct BootstrapResult for failure', async () => { |
| 549 | + // GIVEN |
| 550 | + const error = new Error('Access Denied'); |
| 551 | + error.name = 'AccessDeniedException'; |
| 552 | + mockCloudFormationClient |
| 553 | + .on(DescribeStacksCommand) |
| 554 | + .rejects(error); |
| 555 | + |
| 556 | + // WHEN/THEN |
| 557 | + await expect(runBootstrap({ environments: ['aws://123456789012/us-east-1'] })) |
| 558 | + .rejects.toThrow('Access Denied'); |
503 | 559 | expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
|
504 | 560 | level: 'error',
|
505 | 561 | message: expect.stringContaining('❌'),
|
506 | 562 | }));
|
507 | 563 | });
|
508 | 564 |
|
509 |
| - test('handles permission errors', async () => { |
| 565 | + test('handles generic bootstrap errors', async () => { |
510 | 566 | // GIVEN
|
511 |
| - const permissionError = new Error('Access Denied'); |
512 |
| - permissionError.name = 'AccessDeniedException'; |
513 |
| - mockCloudFormationClient.onAnyCommand().rejects(permissionError); |
514 |
| - |
515 |
| - // WHEN |
516 |
| - await expect(runBootstrap()).rejects.toThrow('Access Denied'); |
| 567 | + const error = new Error('Bootstrap failed'); |
| 568 | + mockCloudFormationClient |
| 569 | + .on(DescribeStacksCommand) |
| 570 | + .rejects(error); |
517 | 571 |
|
518 |
| - // THEN |
| 572 | + // WHEN/THEN |
| 573 | + await expect(runBootstrap({ environments: ['aws://123456789012/us-east-1'] })) |
| 574 | + .rejects.toThrow('Bootstrap failed'); |
519 | 575 | expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
|
520 | 576 | level: 'error',
|
521 | 577 | message: expect.stringContaining('❌'),
|
522 | 578 | }));
|
| 579 | + }); |
| 580 | + |
| 581 | + test('handles permission errors', async () => { |
| 582 | + // GIVEN |
| 583 | + const error = new Error('Access Denied'); |
| 584 | + error.name = 'AccessDeniedException'; |
| 585 | + mockCloudFormationClient |
| 586 | + .on(DescribeStacksCommand) |
| 587 | + .rejects(error); |
| 588 | + |
| 589 | + // WHEN/THEN |
| 590 | + await expect(runBootstrap({ environments: ['aws://123456789012/us-east-1'] })) |
| 591 | + .rejects.toThrow('Access Denied'); |
523 | 592 | expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
|
524 | 593 | level: 'error',
|
525 |
| - message: expect.stringContaining('Access Denied'), |
| 594 | + message: expect.stringContaining('❌'), |
526 | 595 | }));
|
527 | 596 | });
|
528 | 597 | });
|
|
0 commit comments