|
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