UNPKG

aws-cdk

Version:

CDK Toolkit, the command line tool for CDK apps

840 lines 104 kB
"use strict"; /* eslint-disable import/order */ Object.defineProperty(exports, "__esModule", { value: true }); const client_cloudformation_1 = require("@aws-sdk/client-cloudformation"); const api_1 = require("../../lib/api"); const mock_sdk_1 = require("../util/mock-sdk"); const client_s3_1 = require("@aws-sdk/client-s3"); const stack_refresh_1 = require("../../lib/api/garbage-collection/stack-refresh"); const client_ecr_1 = require("@aws-sdk/client-ecr"); let garbageCollector; let stderrMock; const cfnClient = mock_sdk_1.mockCloudFormationClient; const s3Client = mock_sdk_1.mockS3Client; const ecrClient = mock_sdk_1.mockECRClient; const DAY = 24 * 60 * 60 * 1000; // Number of milliseconds in a day beforeEach(() => { // By default, we'll return a non-found toolkit info jest.spyOn(api_1.ToolkitInfo, 'lookup').mockResolvedValue(api_1.ToolkitInfo.bootstrapStackNotFoundInfo('GarbageStack')); // Suppress stderr to not spam output during tests stderrMock = jest.spyOn(process.stderr, 'write').mockImplementation(() => { return true; }); prepareDefaultCfnMock(); prepareDefaultS3Mock(); prepareDefaultEcrMock(); }); afterEach(() => { stderrMock.mockReset(); }); function mockTheToolkitInfo(stackProps) { jest.spyOn(api_1.ToolkitInfo, 'lookup').mockResolvedValue(api_1.ToolkitInfo.fromStack((0, mock_sdk_1.mockBootstrapStack)(stackProps))); } function gc(props) { return new api_1.GarbageCollector({ sdkProvider: new mock_sdk_1.MockSdkProvider(), action: props.action, resolvedEnvironment: { account: '123456789012', region: 'us-east-1', name: 'mock', }, bootstrapStackName: 'GarbageStack', rollbackBufferDays: props.rollbackBufferDays ?? 0, createdBufferDays: props.createdAtBufferDays ?? 0, type: props.type, confirm: false, }); } describe('S3 Garbage Collection', () => { test('rollbackBufferDays = 0 -- assets to be deleted', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); garbageCollector = gc({ type: 's3', rollbackBufferDays: 0, action: 'full', }); await garbageCollector.garbageCollect(); expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1); expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.ListObjectsV2Command, 2); // no tagging expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.GetObjectTaggingCommand, 0); expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.PutObjectTaggingCommand, 0); // assets are to be deleted expect(s3Client).toHaveReceivedCommandWith(client_s3_1.DeleteObjectsCommand, { Bucket: 'BUCKET_NAME', Delete: { Objects: [ { Key: 'asset1' }, { Key: 'asset2' }, { Key: 'asset3' }, ], Quiet: true, }, }); }); test('rollbackBufferDays > 0 -- assets to be tagged', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); garbageCollector = gc({ type: 's3', rollbackBufferDays: 3, action: 'full', }); await garbageCollector.garbageCollect(); expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1); expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.ListObjectsV2Command, 2); // assets tagged expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.GetObjectTaggingCommand, 3); expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.PutObjectTaggingCommand, 2); // no deleting expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.DeleteObjectsCommand, 0); }); test('createdAtBufferDays > 0', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); garbageCollector = gc({ type: 's3', rollbackBufferDays: 0, createdAtBufferDays: 5, action: 'full', }); await garbageCollector.garbageCollect(); expect(s3Client).toHaveReceivedCommandWith(client_s3_1.DeleteObjectsCommand, { Bucket: 'BUCKET_NAME', Delete: { Objects: [ // asset1 not deleted because it is too young { Key: 'asset2' }, { Key: 'asset3' }, ], Quiet: true, }, }); }); test('action = print -- does not tag or delete', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); garbageCollector = gc({ type: 's3', rollbackBufferDays: 3, action: 'print', }); await garbageCollector.garbageCollect(); expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1); expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.ListObjectsV2Command, 2); // get tags, but dont put tags expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.GetObjectTaggingCommand, 3); expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.PutObjectTaggingCommand, 0); // no deleting expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.DeleteObjectsCommand, 0); }); test('action = tag -- does not delete', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); garbageCollector = gc({ type: 's3', rollbackBufferDays: 3, action: 'tag', }); await garbageCollector.garbageCollect(); expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1); expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.ListObjectsV2Command, 2); // tags objects expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.GetObjectTaggingCommand, 3); expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.PutObjectTaggingCommand, 2); // one object already has the tag // no deleting expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.DeleteObjectsCommand, 0); }); test('action = delete-tagged -- does not tag', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); garbageCollector = gc({ type: 's3', rollbackBufferDays: 3, action: 'delete-tagged', }); await garbageCollector.garbageCollect(); expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1); expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.ListObjectsV2Command, 2); // get tags, but dont put tags expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.GetObjectTaggingCommand, 3); expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.PutObjectTaggingCommand, 0); }); test('ignore objects that are modified after gc start', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); s3Client.on(client_s3_1.ListObjectsV2Command).resolves({ Contents: [ { Key: 'asset1', LastModified: new Date(0) }, { Key: 'asset2', LastModified: new Date(0) }, { Key: 'asset3', LastModified: new Date(new Date().setFullYear(new Date().getFullYear() + 1)) }, // future date ignored everywhere ], KeyCount: 3, }); garbageCollector = gc({ type: 's3', rollbackBufferDays: 0, action: 'full', }); await garbageCollector.garbageCollect(); // assets are to be deleted expect(s3Client).toHaveReceivedCommandWith(client_s3_1.DeleteObjectsCommand, { Bucket: 'BUCKET_NAME', Delete: { Objects: [ { Key: 'asset1' }, { Key: 'asset2' }, // no asset3 ], Quiet: true, }, }); }); }); describe('ECR Garbage Collection', () => { test('rollbackBufferDays = 0 -- assets to be deleted', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); garbageCollector = gc({ type: 'ecr', rollbackBufferDays: 0, action: 'full', }); await garbageCollector.garbageCollect(); expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.DescribeImagesCommand, 1); expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.ListImagesCommand, 2); // no tagging expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.PutImageCommand, 0); // assets are to be deleted expect(ecrClient).toHaveReceivedCommandWith(client_ecr_1.BatchDeleteImageCommand, { repositoryName: 'REPO_NAME', imageIds: [ { imageDigest: 'digest3' }, { imageDigest: 'digest2' }, ], }); }); test('rollbackBufferDays > 0 -- assets to be tagged', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); garbageCollector = gc({ type: 'ecr', rollbackBufferDays: 3, action: 'full', }); await garbageCollector.garbageCollect(); // assets tagged expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.PutImageCommand, 2); // no deleting expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.BatchDeleteImageCommand, 0); }); test('createdAtBufferDays > 0', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); garbageCollector = gc({ type: 'ecr', rollbackBufferDays: 0, createdAtBufferDays: 5, action: 'full', }); await garbageCollector.garbageCollect(); expect(ecrClient).toHaveReceivedCommandWith(client_ecr_1.BatchDeleteImageCommand, { repositoryName: 'REPO_NAME', imageIds: [ // digest3 is too young to be deleted { imageDigest: 'digest2' }, ], }); }); test('action = print -- does not tag or delete', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); garbageCollector = gc({ type: 'ecr', rollbackBufferDays: 3, action: 'print', }); await garbageCollector.garbageCollect(); expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1); // dont put tags expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.PutImageCommand, 0); // no deleting expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.BatchDeleteImageCommand, 0); }); test('action = tag -- does not delete', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); garbageCollector = gc({ type: 'ecr', rollbackBufferDays: 3, action: 'tag', }); await garbageCollector.garbageCollect(); expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1); // tags objects expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.PutImageCommand, 2); // no deleting expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.BatchDeleteImageCommand, 0); }); test('action = delete-tagged -- does not tag', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); garbageCollector = gc({ type: 'ecr', rollbackBufferDays: 3, action: 'delete-tagged', }); await garbageCollector.garbageCollect(); expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1); // dont put tags expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.PutImageCommand, 0); }); test('ignore images that are modified after gc start', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); prepareDefaultEcrMock(); ecrClient.on(client_ecr_1.DescribeImagesCommand).resolves({ imageDetails: [ { imageDigest: 'digest3', imageTags: ['klmno'], imagePushedAt: daysInThePast(2), imageSizeInBytes: 100, }, { imageDigest: 'digest2', imageTags: ['fghij'], imagePushedAt: yearsInTheFuture(1), imageSizeInBytes: 300000000, }, { imageDigest: 'digest1', imageTags: ['abcde'], imagePushedAt: daysInThePast(100), imageSizeInBytes: 1000000000, }, ], }); prepareDefaultCfnMock(); garbageCollector = gc({ type: 'ecr', rollbackBufferDays: 0, action: 'full', }); await garbageCollector.garbageCollect(); // assets are to be deleted expect(ecrClient).toHaveReceivedCommandWith(client_ecr_1.BatchDeleteImageCommand, { repositoryName: 'REPO_NAME', imageIds: [ { imageDigest: 'digest3' }, ], }); }); test('succeeds when no images are present', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); prepareDefaultEcrMock(); ecrClient.on(client_ecr_1.ListImagesCommand).resolves({ imageIds: [], }); garbageCollector = gc({ type: 'ecr', rollbackBufferDays: 0, action: 'full', }); // succeeds without hanging await garbageCollector.garbageCollect(); }); test('tags are unique', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); garbageCollector = gc({ type: 'ecr', rollbackBufferDays: 3, action: 'tag', }); await garbageCollector.garbageCollect(); expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1); // tags objects expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.PutImageCommand, 2); expect(ecrClient).toHaveReceivedCommandWith(client_ecr_1.PutImageCommand, { repositoryName: 'REPO_NAME', imageDigest: 'digest3', imageManifest: expect.any(String), imageTag: expect.stringContaining(`0-${api_1.ECR_ISOLATED_TAG}`), }); expect(ecrClient).toHaveReceivedCommandWith(client_ecr_1.PutImageCommand, { repositoryName: 'REPO_NAME', imageDigest: 'digest2', imageManifest: expect.any(String), imageTag: expect.stringContaining(`1-${api_1.ECR_ISOLATED_TAG}`), }); }); test('listImagesCommand returns nextToken', async () => { // This test is to ensure that the garbage collector can handle paginated responses from the ECR API // If not handled correctly, the garbage collector will continue to make requests to the ECR API mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); prepareDefaultEcrMock(); ecrClient.on(client_ecr_1.ListImagesCommand).resolves({ imageIds: [ { imageDigest: 'digest1', imageTag: 'abcde', }, { imageDigest: 'digest2', imageTag: 'fghij', }, ], nextToken: 'nextToken', }).on(client_ecr_1.ListImagesCommand, { repositoryName: 'REPO_NAME', nextToken: 'nextToken', }).resolves({ imageIds: [ { imageDigest: 'digest3', imageTag: 'klmno', }, ], }); ecrClient.on(client_ecr_1.BatchGetImageCommand).resolvesOnce({ images: [ { imageId: { imageDigest: 'digest1' } }, { imageId: { imageDigest: 'digest2' } }, ], }).resolvesOnce({ images: [ { imageId: { imageDigest: 'digest3' } }, ], }); ecrClient.on(client_ecr_1.DescribeImagesCommand).resolvesOnce({ imageDetails: [ { imageDigest: 'digest1', imageTags: ['abcde'], imagePushedAt: daysInThePast(100), imageSizeInBytes: 1000000000, }, { imageDigest: 'digest2', imageTags: ['fghij'], imagePushedAt: daysInThePast(10), imageSizeInBytes: 300000000 }, ], }).resolvesOnce({ imageDetails: [ { imageDigest: 'digest3', imageTags: ['klmno'], imagePushedAt: daysInThePast(2), imageSizeInBytes: 100 }, ], }); prepareDefaultCfnMock(); garbageCollector = gc({ type: 'ecr', rollbackBufferDays: 0, action: 'full', }); await garbageCollector.garbageCollect(); expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.DescribeImagesCommand, 2); expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.ListImagesCommand, 4); // no tagging expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.PutImageCommand, 0); expect(ecrClient).toHaveReceivedCommandWith(client_ecr_1.BatchDeleteImageCommand, { repositoryName: 'REPO_NAME', imageIds: [ { imageDigest: 'digest2' }, { imageDigest: 'digest3' }, ], }); }); }); describe('CloudFormation API calls', () => { test('bootstrap filters out other bootstrap versions', async () => { mockTheToolkitInfo({ Parameters: [{ ParameterKey: 'Qualifier', ParameterValue: 'zzzzzz', }], Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); garbageCollector = gc({ type: 's3', rollbackBufferDays: 3, action: 'full', }); await garbageCollector.garbageCollect(); expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.GetTemplateSummaryCommand, 2); expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.GetTemplateCommand, 0); }); test('parameter hashes are included', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); cfnClient.on(client_cloudformation_1.GetTemplateSummaryCommand).resolves({ Parameters: [{ ParameterKey: 'AssetParametersasset1', DefaultValue: 'asset1', }], }); garbageCollector = gc({ type: 's3', rollbackBufferDays: 0, action: 'full', }); await garbageCollector.garbageCollect(); expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1); expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.ListObjectsV2Command, 2); // no tagging expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.GetObjectTaggingCommand, 0); expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.PutObjectTaggingCommand, 0); // assets are to be deleted expect(s3Client).toHaveReceivedCommandWith(client_s3_1.DeleteObjectsCommand, { Bucket: 'BUCKET_NAME', Delete: { Objects: [ // no 'asset1' { Key: 'asset2' }, { Key: 'asset3' }, ], Quiet: true, }, }); }); }); function prepareDefaultCfnMock() { const client = cfnClient; client.reset(); client.on(client_cloudformation_1.ListStacksCommand).resolves({ StackSummaries: [ { StackName: 'Stack1', StackStatus: 'CREATE_COMPLETE', CreationTime: new Date() }, { StackName: 'Stack2', StackStatus: 'UPDATE_COMPLETE', CreationTime: new Date() }, ], }); client.on(client_cloudformation_1.GetTemplateSummaryCommand).resolves({ Parameters: [{ ParameterKey: 'BootstrapVersion', DefaultValue: '/cdk-bootstrap/abcde/version', }], }); client.on(client_cloudformation_1.GetTemplateCommand).resolves({ TemplateBody: 'abcde', }); return client; } function prepareDefaultS3Mock() { const client = s3Client; client.reset(); client.on(client_s3_1.ListObjectsV2Command).resolves({ Contents: [ { Key: 'asset1', LastModified: new Date(Date.now() - (2 * DAY)) }, { Key: 'asset2', LastModified: new Date(Date.now() - (10 * DAY)) }, { Key: 'asset3', LastModified: new Date(Date.now() - (100 * DAY)) }, ], KeyCount: 3, }); client.on(client_s3_1.GetObjectTaggingCommand).callsFake((params) => ({ TagSet: params.Key === 'asset2' ? [{ Key: api_1.S3_ISOLATED_TAG, Value: new Date().toISOString() }] : [], })); return client; } function prepareDefaultEcrMock() { const client = ecrClient; client.reset(); client.on(client_ecr_1.BatchGetImageCommand).resolves({ images: [ { imageId: { imageDigest: 'digest1' } }, { imageId: { imageDigest: 'digest2' } }, { imageId: { imageDigest: 'digest3' } }, ], }); client.on(client_ecr_1.DescribeImagesCommand).resolves({ imageDetails: [ { imageDigest: 'digest3', imageTags: ['klmno'], imagePushedAt: daysInThePast(2), imageSizeInBytes: 100 }, { imageDigest: 'digest2', imageTags: ['fghij'], imagePushedAt: daysInThePast(10), imageSizeInBytes: 300000000 }, { imageDigest: 'digest1', imageTags: ['abcde'], imagePushedAt: daysInThePast(100), imageSizeInBytes: 1000000000, }, ], }); client.on(client_ecr_1.ListImagesCommand).resolves({ imageIds: [ { imageDigest: 'digest1', imageTag: 'abcde' }, // inuse { imageDigest: 'digest2', imageTag: 'fghij' }, { imageDigest: 'digest3', imageTag: 'klmno' }, ], }); return client; } describe('Garbage Collection with large # of objects', () => { const keyCount = 10000; test('tag only', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); mockClientsForLargeObjects(); garbageCollector = gc({ type: 's3', rollbackBufferDays: 1, action: 'tag', }); await garbageCollector.garbageCollect(); expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1); expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.ListObjectsV2Command, 2); // tagging is performed expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.GetObjectTaggingCommand, keyCount); expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.DeleteObjectTaggingCommand, 1000); // 1000 in use assets are erroneously tagged expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.PutObjectTaggingCommand, 5000); // 8000-4000 assets need to be tagged, + 1000 (since untag also calls this) }); test('delete-tagged only', async () => { mockTheToolkitInfo({ Outputs: [ { OutputKey: 'BootstrapVersion', OutputValue: '999', }, ], }); mockClientsForLargeObjects(); garbageCollector = gc({ type: 's3', rollbackBufferDays: 1, action: 'delete-tagged', }); await garbageCollector.garbageCollect(); expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1); expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.ListObjectsV2Command, 2); // delete previously tagged objects expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.GetObjectTaggingCommand, keyCount); expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.DeleteObjectsCommand, 4); // 4000 isolated assets are already tagged, deleted in batches of 1000 }); function mockClientsForLargeObjects() { cfnClient.on(client_cloudformation_1.ListStacksCommand).resolves({ StackSummaries: [ { StackName: 'Stack1', StackStatus: 'CREATE_COMPLETE', CreationTime: new Date() }, ], }); cfnClient.on(client_cloudformation_1.GetTemplateSummaryCommand).resolves({ Parameters: [{ ParameterKey: 'BootstrapVersion', DefaultValue: '/cdk-bootstrap/abcde/version', }], }); // add every 5th asset hash to the mock template body: 8000 assets are isolated const mockTemplateBody = []; for (let i = 0; i < keyCount; i += 5) { mockTemplateBody.push(`asset${i}hash`); } cfnClient.on(client_cloudformation_1.GetTemplateCommand).resolves({ TemplateBody: mockTemplateBody.join('-'), }); const contents = []; for (let i = 0; i < keyCount; i++) { contents.push({ Key: `asset${i}hash`, LastModified: new Date(0), }); } s3Client.on(client_s3_1.ListObjectsV2Command).resolves({ Contents: contents, KeyCount: keyCount, }); // every other object has the isolated tag: of the 8000 isolated assets, 4000 already are tagged. // of the 2000 in use assets, 1000 are tagged. s3Client.on(client_s3_1.GetObjectTaggingCommand).callsFake((params) => ({ TagSet: Number(params.Key[params.Key.length - 5]) % 2 === 0 ? [{ Key: api_1.S3_ISOLATED_TAG, Value: new Date(2000, 1, 1).toISOString() }] : [], })); } }); describe('BackgroundStackRefresh', () => { let backgroundRefresh; let refreshProps; let setTimeoutSpy; beforeEach(() => { jest.useFakeTimers(); setTimeoutSpy = jest.spyOn(global, 'setTimeout'); const foo = new mock_sdk_1.MockSdk(); refreshProps = { cfn: foo.cloudFormation(), activeAssets: new stack_refresh_1.ActiveAssetCache(), }; backgroundRefresh = new stack_refresh_1.BackgroundStackRefresh(refreshProps); }); afterEach(() => { jest.clearAllTimers(); setTimeoutSpy.mockRestore(); }); test('should start after a delay', () => { void backgroundRefresh.start(); expect(setTimeoutSpy).toHaveBeenCalledTimes(1); expect(setTimeoutSpy).toHaveBeenLastCalledWith(expect.any(Function), 300000); }); test('should refresh stacks and schedule next refresh', async () => { void backgroundRefresh.start(); // Run the first timer (which should trigger the first refresh) await jest.runOnlyPendingTimersAsync(); expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1); expect(setTimeoutSpy).toHaveBeenCalledTimes(2); // Once for start, once for next refresh expect(setTimeoutSpy).toHaveBeenLastCalledWith(expect.any(Function), 300000); // Run the first timer (which triggers the first refresh) await jest.runOnlyPendingTimersAsync(); expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 2); expect(setTimeoutSpy).toHaveBeenCalledTimes(3); // Two refreshes plus one more scheduled }); test('should wait for the next refresh if called within time frame', async () => { void backgroundRefresh.start(); // Run the first timer (which triggers the first refresh) await jest.runOnlyPendingTimersAsync(); const waitPromise = backgroundRefresh.noOlderThan(180000); // 3 minutes jest.advanceTimersByTime(120000); // Advance time by 2 minutes await expect(waitPromise).resolves.toBeUndefined(); }); test('should wait for the next refresh if refresh lands before the timeout', async () => { void backgroundRefresh.start(); // Run the first timer (which triggers the first refresh) await jest.runOnlyPendingTimersAsync(); jest.advanceTimersByTime(24000); // Advance time by 4 minutes const waitPromise = backgroundRefresh.noOlderThan(300000); // 5 minutes jest.advanceTimersByTime(120000); // Advance time by 2 minutes, refresh should fire await expect(waitPromise).resolves.toBeUndefined(); }); test('should reject if the refresh takes too long', async () => { void backgroundRefresh.start(); // Run the first timer (which triggers the first refresh) await jest.runOnlyPendingTimersAsync(); jest.advanceTimersByTime(120000); // Advance time by 2 minutes const waitPromise = backgroundRefresh.noOlderThan(0); // 0 seconds jest.advanceTimersByTime(120000); // Advance time by 2 minutes await expect(waitPromise).rejects.toThrow('refreshStacks took too long; the background thread likely threw an error'); }); }); function daysInThePast(days) { const d = new Date(); d.setDate(d.getDate() - days); return d; } function yearsInTheFuture(years) { const d = new Date(); d.setFullYear(d.getFullYear() + years); return d; } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ2FyYmFnZS1jb2xsZWN0aW9uLnRlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJnYXJiYWdlLWNvbGxlY3Rpb24udGVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUEsaUNBQWlDOztBQUVqQywwRUFLd0M7QUFDeEMsdUNBQWlHO0FBQ2pHLCtDQUF1STtBQUN2SSxrREFNNEI7QUFDNUIsa0ZBSXdEO0FBQ3hELG9EQU02QjtBQUU3QixJQUFJLGdCQUFrQyxDQUFDO0FBRXZDLElBQUksVUFBNEIsQ0FBQztBQUNqQyxNQUFNLFNBQVMsR0FBRyxtQ0FBd0IsQ0FBQztBQUMzQyxNQUFNLFFBQVEsR0FBRyx1QkFBWSxDQUFDO0FBQzlCLE1BQU0sU0FBUyxHQUFHLHdCQUFhLENBQUM7QUFFaEMsTUFBTSxHQUFHLEdBQUcsRUFBRSxHQUFHLEVBQUUsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFDLENBQUMsa0NBQWtDO0FBRW5FLFVBQVUsQ0FBQyxHQUFHLEVBQUU7SUFDZCxvREFBb0Q7SUFDcEQsSUFBSSxDQUFDLEtBQUssQ0FBQyxpQkFBVyxFQUFFLFFBQVEsQ0FBQyxDQUFDLGlCQUFpQixDQUFDLGlCQUFXLENBQUMsMEJBQTBCLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQztJQUU1RyxrREFBa0Q7SUFDbEQsVUFBVSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLE1BQU0sRUFBRSxPQUFPLENBQUMsQ0FBQyxrQkFBa0IsQ0FBQyxHQUFHLEVBQUU7UUFDdkUsT0FBTyxJQUFJLENBQUM7SUFDZCxDQUFDLENBQUMsQ0FBQztJQUVILHFCQUFxQixFQUFFLENBQUM7SUFDeEIsb0JBQW9CLEVBQUUsQ0FBQztJQUN2QixxQkFBcUIsRUFBRSxDQUFDO0FBQzFCLENBQUMsQ0FBQyxDQUFDO0FBRUgsU0FBUyxDQUFDLEdBQUcsRUFBRTtJQUNiLFVBQVUsQ0FBQyxTQUFTLEVBQUUsQ0FBQztBQUN6QixDQUFDLENBQUMsQ0FBQztBQUVILFNB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