diff --git a/src/__tests__/__snapshots__/construct-hub.test.ts.snap b/src/__tests__/__snapshots__/construct-hub.test.ts.snap index d0ed10181..69fe5eec5 100644 --- a/src/__tests__/__snapshots__/construct-hub.test.ts.snap +++ b/src/__tests__/__snapshots__/construct-hub.test.ts.snap @@ -1982,7 +1982,7 @@ Direct link to the function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "ea3bfaa031c0e35e007cf9b1cccc4de0e79a5c7c2dab10ec7f0489246a0c7c2e.zip", + "S3Key": "c2b804973791668a31a3359fedc40ffe6b54a85b0c19238e5027d7236941ce63.zip", }, "Description": "Release note RSS feed updater", "Environment": { @@ -5867,7 +5867,7 @@ Warning: State Machines executions that sent messages to the DLQ will not show a "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "e7b84cc7f34e84cd14b6df84bbf590d644c6bbef900e68943b0b8efbad2ca166.zip", + "S3Key": "a11d7eb83cc26dc9b771f4bd6a7a0d288c97cf306a6e477775ef96be1e9c47a5.zip", }, "Description": "[ConstructHub/Orchestration/NeedsCatalogUpdate] Determines whether a package version requires a catalog update", "Environment": { @@ -10568,7 +10568,7 @@ Direct link to Lambda function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "1854ecb6f125284eabd0fc07139332e7bf50a68003644eb857a277fc1cf11786.zip", + "S3Key": "dce7ea3e58a63f0486d731bdb3ea921fd232e2ea5b3808312323af4c4eabdb95.zip", }, "Description": { "Fn::Join": [ @@ -14810,7 +14810,7 @@ Direct link to the function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "ea3bfaa031c0e35e007cf9b1cccc4de0e79a5c7c2dab10ec7f0489246a0c7c2e.zip", + "S3Key": "c2b804973791668a31a3359fedc40ffe6b54a85b0c19238e5027d7236941ce63.zip", }, "Description": "Release note RSS feed updater", "Environment": { @@ -18793,7 +18793,7 @@ Warning: State Machines executions that sent messages to the DLQ will not show a "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "e7b84cc7f34e84cd14b6df84bbf590d644c6bbef900e68943b0b8efbad2ca166.zip", + "S3Key": "a11d7eb83cc26dc9b771f4bd6a7a0d288c97cf306a6e477775ef96be1e9c47a5.zip", }, "Description": "[ConstructHub/Orchestration/NeedsCatalogUpdate] Determines whether a package version requires a catalog update", "Environment": { @@ -23567,7 +23567,7 @@ Direct link to Lambda function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "1854ecb6f125284eabd0fc07139332e7bf50a68003644eb857a277fc1cf11786.zip", + "S3Key": "dce7ea3e58a63f0486d731bdb3ea921fd232e2ea5b3808312323af4c4eabdb95.zip", }, "Description": { "Fn::Join": [ @@ -27552,7 +27552,7 @@ Direct link to the function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "ea3bfaa031c0e35e007cf9b1cccc4de0e79a5c7c2dab10ec7f0489246a0c7c2e.zip", + "S3Key": "c2b804973791668a31a3359fedc40ffe6b54a85b0c19238e5027d7236941ce63.zip", }, "Description": "Release note RSS feed updater", "Environment": { @@ -31437,7 +31437,7 @@ Warning: State Machines executions that sent messages to the DLQ will not show a "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "e7b84cc7f34e84cd14b6df84bbf590d644c6bbef900e68943b0b8efbad2ca166.zip", + "S3Key": "a11d7eb83cc26dc9b771f4bd6a7a0d288c97cf306a6e477775ef96be1e9c47a5.zip", }, "Description": "[ConstructHub/Orchestration/NeedsCatalogUpdate] Determines whether a package version requires a catalog update", "Environment": { @@ -36138,7 +36138,7 @@ Direct link to Lambda function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "1854ecb6f125284eabd0fc07139332e7bf50a68003644eb857a277fc1cf11786.zip", + "S3Key": "dce7ea3e58a63f0486d731bdb3ea921fd232e2ea5b3808312323af4c4eabdb95.zip", }, "Description": { "Fn::Join": [ @@ -40263,7 +40263,7 @@ Direct link to the function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "ea3bfaa031c0e35e007cf9b1cccc4de0e79a5c7c2dab10ec7f0489246a0c7c2e.zip", + "S3Key": "c2b804973791668a31a3359fedc40ffe6b54a85b0c19238e5027d7236941ce63.zip", }, "Description": "Release note RSS feed updater", "Environment": { @@ -44157,7 +44157,7 @@ Warning: State Machines executions that sent messages to the DLQ will not show a "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "e7b84cc7f34e84cd14b6df84bbf590d644c6bbef900e68943b0b8efbad2ca166.zip", + "S3Key": "a11d7eb83cc26dc9b771f4bd6a7a0d288c97cf306a6e477775ef96be1e9c47a5.zip", }, "Description": "[ConstructHub/Orchestration/NeedsCatalogUpdate] Determines whether a package version requires a catalog update", "Environment": { @@ -48845,7 +48845,7 @@ Direct link to Lambda function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "1854ecb6f125284eabd0fc07139332e7bf50a68003644eb857a277fc1cf11786.zip", + "S3Key": "dce7ea3e58a63f0486d731bdb3ea921fd232e2ea5b3808312323af4c4eabdb95.zip", }, "Description": { "Fn::Join": [ @@ -53157,7 +53157,7 @@ Direct link to the function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "ea3bfaa031c0e35e007cf9b1cccc4de0e79a5c7c2dab10ec7f0489246a0c7c2e.zip", + "S3Key": "c2b804973791668a31a3359fedc40ffe6b54a85b0c19238e5027d7236941ce63.zip", }, "Description": "Release note RSS feed updater", "Environment": { @@ -56790,7 +56790,7 @@ Warning: State Machines executions that sent messages to the DLQ will not show a "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "e7b84cc7f34e84cd14b6df84bbf590d644c6bbef900e68943b0b8efbad2ca166.zip", + "S3Key": "a11d7eb83cc26dc9b771f4bd6a7a0d288c97cf306a6e477775ef96be1e9c47a5.zip", }, "Description": "[ConstructHub/Orchestration/NeedsCatalogUpdate] Determines whether a package version requires a catalog update", "Environment": { @@ -61559,7 +61559,7 @@ Direct link to Lambda function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "1854ecb6f125284eabd0fc07139332e7bf50a68003644eb857a277fc1cf11786.zip", + "S3Key": "dce7ea3e58a63f0486d731bdb3ea921fd232e2ea5b3808312323af4c4eabdb95.zip", }, "Description": { "Fn::Join": [ diff --git a/src/__tests__/backend/catalog-builder/client.test.ts b/src/__tests__/backend/catalog-builder/client.test.ts index 2d6b0ece4..724aada13 100644 --- a/src/__tests__/backend/catalog-builder/client.test.ts +++ b/src/__tests__/backend/catalog-builder/client.test.ts @@ -1,12 +1,16 @@ -import * as AWS from 'aws-sdk'; -import { AWSError } from 'aws-sdk'; -import * as AWSMock from 'aws-sdk-mock'; +import { + GetObjectCommand, + NoSuchBucket, + NoSuchKey, + S3Client, +} from '@aws-sdk/client-s3'; +import { mockClient } from 'aws-sdk-client-mock'; import type { PackageInfo } from '../../../backend/catalog-builder'; import { CatalogClient, CatalogNotFoundError, } from '../../../backend/catalog-builder/client.lambda-shared'; -import * as aws from '../../../backend/shared/aws.lambda-shared'; +import { stringToStream } from '../../streams'; const samplePackages: Partial[] = [ { @@ -35,25 +39,26 @@ const samplePackages: Partial[] = [ }, ]; +const mockS3 = mockClient(S3Client); + beforeEach(() => { process.env.CATALOG_BUCKET_NAME = 'catalog-bucket-name'; process.env.CATALOG_OBJECT_KEY = 'catalog.json'; - AWSMock.setSDKInstance(AWS); + mockS3.reset(); }); afterEach(() => { delete process.env.CATALOG_BUCKET_NAME; delete process.env.CATALOG_OBJECT_KEY; - AWSMock.restore(); - aws.reset(); }); test('s3 object not found error', async () => { - AWSMock.mock('S3', 'getObject', (_, callback) => { - const err = new Error('NoSuchKey'); - (err as any).code = 'NoSuchKey'; - callback(err as AWSError, undefined); - }); + mockS3.on(GetObjectCommand).rejects( + new NoSuchKey({ + $metadata: {}, + message: 'The specified key does not exist.', + }) + ); const expected = new CatalogNotFoundError('catalog-bucket-name/catalog.json'); return expect(async () => CatalogClient.newClient()).rejects.toEqual( @@ -62,11 +67,12 @@ test('s3 object not found error', async () => { }); test('s3 bucket not found error', async () => { - AWSMock.mock('S3', 'getObject', (_, callback) => { - const err = new Error('NoSuchBucket'); - (err as any).code = 'NoSuchBucket'; - callback(err as AWSError, undefined); - }); + mockS3.on(GetObjectCommand).rejects( + new NoSuchBucket({ + $metadata: {}, + message: 'The specified bucket does not exist.', + }) + ); const expected = new CatalogNotFoundError('catalog-bucket-name/catalog.json'); return expect(async () => CatalogClient.newClient()).rejects.toEqual( @@ -75,11 +81,30 @@ test('s3 bucket not found error', async () => { }); test('empty file', async () => { - AWSMock.mock('S3', 'getObject', (params, callback) => { - expect(params.Bucket).toBe('catalog-bucket-name'); - expect(params.Key).toBe('catalog.json'); - callback(undefined, { Body: '' }); - }); + mockS3 + .on(GetObjectCommand, { + Bucket: 'catalog-bucket-name', + Key: 'catalog.json', + }) + .resolves({ + Body: stringToStream(''), + }); + + const expected = new Error( + 'Catalog body is empty at catalog-bucket-name/catalog.json' + ); + return expect(async () => CatalogClient.newClient()).rejects.toEqual( + expected + ); +}); + +test('no body', async () => { + mockS3 + .on(GetObjectCommand, { + Bucket: 'catalog-bucket-name', + Key: 'catalog.json', + }) + .resolves({}); const expected = new Error( 'Catalog body is empty at catalog-bucket-name/catalog.json' @@ -90,11 +115,12 @@ test('empty file', async () => { }); test('json parsing error', async () => { - AWSMock.mock('S3', 'getObject', (params, callback) => { - expect(params.Bucket).toBe('catalog-bucket-name'); - expect(params.Key).toBe('catalog.json'); - callback(undefined, { Body: '09x{}' }); - }); + mockS3 + .on(GetObjectCommand, { + Bucket: 'catalog-bucket-name', + Key: 'catalog.json', + }) + .resolves({ Body: stringToStream('09x{}') }); const expected = new Error( 'Unable to parse catalog file catalog-bucket-name/catalog.json: SyntaxError: Unexpected number in JSON at position 1' @@ -105,11 +131,14 @@ test('json parsing error', async () => { }); test('happy path - get packages', async () => { - AWSMock.mock('S3', 'getObject', (params, callback) => { - expect(params.Bucket).toBe('catalog-bucket-name'); - expect(params.Key).toBe('catalog.json'); - callback(undefined, { Body: JSON.stringify({ packages: samplePackages }) }); - }); + mockS3 + .on(GetObjectCommand, { + Bucket: 'catalog-bucket-name', + Key: 'catalog.json', + }) + .resolves({ + Body: stringToStream(JSON.stringify({ packages: samplePackages })), + }); const client = await CatalogClient.newClient(); expect(client.packages).toStrictEqual(samplePackages); diff --git a/src/__tests__/backend/feed-builder/update-feed.lambda.test.ts b/src/__tests__/backend/feed-builder/update-feed.lambda.test.ts index 7b75d9840..529a9b0fb 100644 --- a/src/__tests__/backend/feed-builder/update-feed.lambda.test.ts +++ b/src/__tests__/backend/feed-builder/update-feed.lambda.test.ts @@ -1,9 +1,17 @@ -import * as AWS from 'aws-sdk'; -import * as AWSMock from 'aws-sdk-mock'; +import { + GetObjectCommand, + NotFound, + PutObjectCommand, + S3Client, + S3ServiceException, +} from '@aws-sdk/client-s3'; +import { StreamingBlobPayloadOutputTypes } from '@smithy/types'; +import { mockClient } from 'aws-sdk-client-mock'; +import 'aws-sdk-client-mock-jest'; import * as feed from 'feed'; - import { handler } from '../../../backend/feed-builder/update-feed.lambda'; import * as constants from '../../../backend/shared/constants'; +import { stringToStream } from '../../streams'; // Hack to spy on methods of es5 class by wrapping the class jest.mock('feed', () => { @@ -55,10 +63,11 @@ const MOCK_CATALOG = { }; }), }; +const mockS3 = mockClient(S3Client); +const s3ObjectMap: Map = new Map(); -const s3ObjectMap: Map = new Map(); -let putObjectMock: jest.Mock; beforeEach(() => { + mockS3.reset(); process.env.CATALOG_BUCKET_NAME = MOCK_CATALOG_BUCKET_NAME; process.env.CATALOG_OBJECT_KEY = MOCK_CATALOG_OBJECT_KEY; process.env.FEED_ENTRY_COUNT = MAX_ENTRIES_IN_FEED; @@ -70,37 +79,40 @@ beforeEach(() => { const { releaseNotesKey } = constants.getObjectKeys(name, version); s3ObjectMap.set( releaseNotesKey, - Buffer.from(`Release notes for ${pkgName}`) + stringToStream(`Release notes for ${pkgName}`) ); }); s3ObjectMap.set( constants.CATALOG_KEY, - Buffer.from(JSON.stringify(MOCK_CATALOG)) + stringToStream(JSON.stringify(MOCK_CATALOG)) ); - setupS3GetObjectMock().mockImplementation( - (req: AWS.S3.GetObjectRequest, cb) => { - expect(req.Bucket).toEqual(MOCK_CATALOG_BUCKET_NAME); - if (s3ObjectMap.has(req.Key)) { - const response = { Body: s3ObjectMap.get(req.Key) }; - cb(null, response); - } - - cb({ statusCode: 404 }); + mockS3.on(GetObjectCommand).callsFake((request) => { + expect(request.Bucket).toEqual(MOCK_CATALOG_BUCKET_NAME); + if (s3ObjectMap.has(request.Key)) { + return { Body: s3ObjectMap.get(request.Key) }; } - ); - putObjectMock = setupPutObjectMock().mockImplementation( - (req: AWS.S3.PutObjectRequest, cb) => { - console.log(req.Key); - expect(req.Bucket).toEqual(MOCK_CATALOG_BUCKET_NAME); - if (req.Key === 'atom' || req.Key === 'rss') { - return cb(null, {}); - } - cb({ error: 'NotSaved' }, null); + throw new NotFound({ + message: `NotFound GET request: ${request.Key}`, + $metadata: {}, + }); + }); + + mockS3.on(PutObjectCommand).callsFake((request) => { + expect(request.Bucket).toEqual(MOCK_CATALOG_BUCKET_NAME); + if (request.Key === 'atom' || request.Key === 'rss') { + return {}; } - ); + + throw new S3ServiceException({ + $metadata: {}, + name: 'NotSaved', + message: 'NotSaved', + $fault: 'server', + }); + }); }); afterEach(() => { @@ -112,35 +124,41 @@ afterEach(() => { test(`generate feed latest ${MAX_ENTRIES_IN_FEED} packages`, async () => { await handler(); - expect(putObjectMock).toHaveBeenCalledTimes(2); - expect(putObjectMock.mock.calls[0][0].Key).toEqual('atom'); - expect(putObjectMock.mock.calls[1][0].Key).toEqual('rss'); + expect(mockS3).toHaveReceivedCommandTimes(PutObjectCommand, 2); + expect(mockS3).toHaveReceivedNthSpecificCommandWith(1, PutObjectCommand, { + Bucket: MOCK_CATALOG_BUCKET_NAME, + Key: 'atom', + Body: expect.anything(), + CacheControl: expect.anything(), + ContentType: 'application/atom+xml', + }); + expect(mockS3).toHaveReceivedNthSpecificCommandWith(2, PutObjectCommand, { + Bucket: MOCK_CATALOG_BUCKET_NAME, + Key: 'rss', + Body: expect.anything(), + CacheControl: expect.anything(), + ContentType: 'application/xml', + }); expect((feed as any).mocks.addItem).toHaveBeenCalledTimes(2); + const firstCallArgs = (feed as any).mocks.addItem.mock.calls[0][0]; + const secondCallArgs = (feed as any).mocks.addItem.mock.calls[1][0]; - expect((feed as any).mocks.addItem.mock.calls[0][0].title).toEqual( - 'pkg3@1.1.1' - ); - expect( - (feed as any).mocks.addItem.mock.calls[0][0].date.toISOString() - ).toEqual('2022-10-19T17:10:30.008Z'); - expect((feed as any).mocks.addItem.mock.calls[0][0].link).toEqual( + expect(firstCallArgs.title).toEqual('pkg3@1.1.1'); + expect(firstCallArgs.date.toISOString()).toEqual('2022-10-19T17:10:30.008Z'); + expect(firstCallArgs.link).toEqual( 'https://myconstruct.dev/packages/pkg3/v/1.1.1' ); - expect((feed as any).mocks.addItem.mock.calls[0][0].content).toEqual( + expect(firstCallArgs.content).toEqual( '

Release notes for pkg3@1.1.1

\n' ); - expect((feed as any).mocks.addItem.mock.calls[1][0].title).toEqual( - 'pkg2@1.24.0' - ); - expect( - (feed as any).mocks.addItem.mock.calls[1][0].date.toISOString() - ).toEqual('2019-06-19T17:10:16.140Z'); - expect((feed as any).mocks.addItem.mock.calls[1][0].link).toEqual( + expect(secondCallArgs.title).toEqual('pkg2@1.24.0'); + expect(secondCallArgs.date.toISOString()).toEqual('2019-06-19T17:10:16.140Z'); + expect(secondCallArgs.link).toEqual( 'https://myconstruct.dev/packages/pkg2/v/1.24.0' ); - expect((feed as any).mocks.addItem.mock.calls[1][0].content).toEqual( + expect(secondCallArgs.content).toEqual( '

Release notes for pkg2@1.24.0

\n' ); }); @@ -149,30 +167,24 @@ test('packages are sorted in reverse chronological order', async () => { await handler(); expect((feed as any).mocks.addItem).toHaveBeenCalledTimes(2); + const firstCallArgs = (feed as any).mocks.addItem.mock.calls[0][0]; + const secondCallArgs = (feed as any).mocks.addItem.mock.calls[1][0]; - expect((feed as any).mocks.addItem.mock.calls[0][0].title).toEqual( - 'pkg3@1.1.1' - ); - expect( - (feed as any).mocks.addItem.mock.calls[0][0].date.toISOString() - ).toEqual('2022-10-19T17:10:30.008Z'); - expect((feed as any).mocks.addItem.mock.calls[0][0].link).toEqual( + expect(firstCallArgs.title).toEqual('pkg3@1.1.1'); + expect(firstCallArgs.date.toISOString()).toEqual('2022-10-19T17:10:30.008Z'); + expect(firstCallArgs.link).toEqual( 'https://myconstruct.dev/packages/pkg3/v/1.1.1' ); - expect((feed as any).mocks.addItem.mock.calls[0][0].content).toEqual( + expect(firstCallArgs.content).toEqual( '

Release notes for pkg3@1.1.1

\n' ); - expect((feed as any).mocks.addItem.mock.calls[1][0].title).toEqual( - 'pkg2@1.24.0' - ); - expect( - (feed as any).mocks.addItem.mock.calls[1][0].date.toISOString() - ).toEqual('2019-06-19T17:10:16.140Z'); - expect((feed as any).mocks.addItem.mock.calls[1][0].link).toEqual( + expect(secondCallArgs.title).toEqual('pkg2@1.24.0'); + expect(secondCallArgs.date.toISOString()).toEqual('2019-06-19T17:10:16.140Z'); + expect(secondCallArgs.link).toEqual( 'https://myconstruct.dev/packages/pkg2/v/1.24.0' ); - expect((feed as any).mocks.addItem.mock.calls[1][0].content).toEqual( + expect(secondCallArgs.content).toEqual( '

Release notes for pkg2@1.24.0

\n' ); }); @@ -186,28 +198,3 @@ test('handle missing release-notes', async () => { 'No release notes' ); }); - -type Response = (err: AWS.AWSError | null, data?: T) => void; -const setupS3GetObjectMock = () => { - const spy = jest.fn(); - AWSMock.mock( - 'S3', - 'getObject', - (req: AWS.S3.GetObjectRequest, cb: Response) => { - return spy(req, cb); - } - ); - return spy; -}; - -function setupPutObjectMock() { - const spy = jest.fn(); - AWSMock.mock( - 'S3', - 'putObject', - (req: AWS.S3.PutObjectRequest, cb: Response) => { - return spy(req, cb); - } - ); - return spy; -} diff --git a/src/__tests__/backend/package-stats/package-stats.lambda.test.ts b/src/__tests__/backend/package-stats/package-stats.lambda.test.ts index 87e2b2757..f88012f38 100644 --- a/src/__tests__/backend/package-stats/package-stats.lambda.test.ts +++ b/src/__tests__/backend/package-stats/package-stats.lambda.test.ts @@ -1,12 +1,19 @@ import { randomBytes } from 'crypto'; - -import * as AWS from 'aws-sdk'; -import type { AWSError } from 'aws-sdk'; -import * as AWSMock from 'aws-sdk-mock'; +import { + GetObjectCommand, + NoSuchKey, + NotFound, + PutObjectCommand, + PutObjectCommandInput, + S3Client, + S3ServiceException, +} from '@aws-sdk/client-s3'; +import { mockClient } from 'aws-sdk-client-mock'; import type { Got } from 'got'; - import { handler } from '../../../backend/package-stats/package-stats.lambda'; -import * as aws from '../../../backend/shared/aws.lambda-shared'; +import { stringToStream } from '../../streams'; + +const mockS3 = mockClient(S3Client); let mockCatalogBucket: string | undefined; let mockCatalogKey: string | undefined; @@ -14,25 +21,21 @@ let mockBucketName: string | undefined; let mockStatsKey: string | undefined; jest.mock('got'); -beforeEach((done) => { +beforeEach(() => { + mockS3.reset(); process.env.CATALOG_BUCKET_NAME = mockCatalogBucket = randomBytes(16).toString('base64'); process.env.CATALOG_OBJECT_KEY = mockCatalogKey = 'my-catalog.json'; process.env.STATS_BUCKET_NAME = mockBucketName = randomBytes(16).toString('base64'); process.env.STATS_OBJECT_KEY = mockStatsKey = 'my-stats.json'; - AWSMock.setSDKInstance(AWS); - done(); }); -afterEach((done) => { - AWSMock.restore(); - aws.reset(); +afterEach(() => { process.env.CATALOG_BUCKET_NAME = mockCatalogBucket = undefined; process.env.CATALOG_OBJECT_KEY = mockCatalogKey = undefined; process.env.STATS_BUCKET_NAME = mockBucketName = undefined; process.env.STATS_OBJECT_KEY = mockStatsKey = undefined; - done(); }); const initialScopePackageV2 = { @@ -64,26 +67,31 @@ describe('full build', () => { test('is successful', () => { // GIVEN - AWSMock.mock( - 'S3', - 'getObject', - (req: AWS.S3.GetObjectRequest, cb: Response) => { - try { - expect(req.Bucket).toBe(mockCatalogBucket); - } catch (e) { - return cb(e as AWSError); - } + mockS3.on(GetObjectCommand).callsFake((req) => { + try { + expect(req.Bucket).toBe(mockCatalogBucket); + } catch (e) { + throw new NotFound({ + $metadata: {}, + message: `Unexpected bucket in test, got: ${req.Bucket}`, + }); + } - if (req.Key === mockCatalogKey) { - return cb(null, { Body: JSON.stringify(initialCatalog) }); - } else if (req.Key === mockStatsKey) { - // suppose we are building for the first time - return cb(new NoSuchKeyError()); - } else { - return cb(new NoSuchKeyError()); - } + if (req.Key === mockCatalogKey) { + return { Body: stringToStream(JSON.stringify(initialCatalog)) }; + } else if (req.Key === mockStatsKey) { + // suppose we are building for the first time + throw new NoSuchKey({ + $metadata: {}, + message: `Pretend ${mockStatsKey} does not exist at the first build`, + }); + } else { + throw new NoSuchKey({ + $metadata: {}, + message: `Unexpected key in test, got: ${req.Key}`, + }); } - ); + }); // two API calls to NPM downloads API since one of the packages is scoped // so the bulk API can't be used @@ -111,38 +119,39 @@ describe('full build', () => { ); const mockPutObjectResult: AWS.S3.PutObjectOutput = {}; - AWSMock.mock( - 'S3', - 'putObject', - (req: AWS.S3.PutObjectRequest, cb: Response) => { - try { - expect(req.Bucket).toBe(mockBucketName); - expect(req.Key).toBe(mockStatsKey); - expect(req.ContentType).toBe('application/json'); - expect(req.Metadata).toHaveProperty('Package-Stats-Count', '2'); - const body = JSON.parse(req.Body?.toString('utf-8') ?? 'null'); - expect(body).toEqual({ - packages: { - '@scope/package': { - downloads: { - npm: 1000, - }, + mockS3.on(PutObjectCommand).callsFake((req: PutObjectCommandInput) => { + try { + expect(req.Bucket).toBe(mockBucketName); + expect(req.Key).toBe(mockStatsKey); + expect(req.ContentType).toBe('application/json'); + expect(req.Metadata).toHaveProperty('Package-Stats-Count', '2'); + const body = JSON.parse(req.Body?.toString('utf-8') ?? 'null'); + expect(body).toEqual({ + packages: { + '@scope/package': { + downloads: { + npm: 1000, }, - name: { - downloads: { - npm: 2000, - }, + }, + name: { + downloads: { + npm: 2000, }, }, - updated: expect.anything(), - }); - expect(Date.parse(body.updated)).toBeDefined(); - } catch (e) { - return cb(e as AWSError); - } - return cb(null, mockPutObjectResult); + }, + updated: expect.anything(), + }); + expect(Date.parse(body.updated)).toBeDefined(); + } catch (e) { + throw new S3ServiceException({ + name: 'UnexpectedInput', + $fault: 'client', + $metadata: {}, + message: `Validation of test input failed`, + }); } - ); + return mockPutObjectResult; + }); // WHEN const result = handler({}, { @@ -156,19 +165,12 @@ describe('full build', () => { test('errors if no catalog found', async () => { // GIVEN - AWSMock.mock( - 'S3', - 'getObject', - (req: AWS.S3.GetObjectRequest, cb: Response) => { - try { - expect(req.Bucket).toBe(mockCatalogBucket); - expect(req.Key).toBe(mockCatalogKey); - return cb(new NoSuchKeyError()); - } catch (e) { - return cb(e as AWSError); - } - } - ); + mockS3 + .on(GetObjectCommand, { + Bucket: mockCatalogBucket, + Key: mockCatalogKey, + }) + .rejects(new NoSuchKey({ $metadata: {}, message: 'No such key' })); // THEN return expect( @@ -177,21 +179,3 @@ test('errors if no catalog found', async () => { } as any) ).rejects.toThrow(/No catalog was found/); }); - -type Response = (err: AWS.AWSError | null, data?: T) => void; - -class NoSuchKeyError extends Error implements AWS.AWSError { - public code = 'NoSuchKey'; - public time = new Date(); - public message = 'NoSuchKey'; - - public retryable?: boolean | undefined; - public statusCode?: number | undefined; - public hostname?: string | undefined; - public region?: string | undefined; - public retryDelay?: number | undefined; - public requestId?: string | undefined; - public extendedRequestId?: string | undefined; - public cfId?: string | undefined; - public originalError?: Error | undefined; -} diff --git a/src/__tests__/backend/transliterator/transliterator.ecstask.test.ts b/src/__tests__/backend/transliterator/transliterator.ecstask.test.ts index 4f7bc2140..851a357e0 100644 --- a/src/__tests__/backend/transliterator/transliterator.ecstask.test.ts +++ b/src/__tests__/backend/transliterator/transliterator.ecstask.test.ts @@ -8,7 +8,6 @@ import { } from '@aws-sdk/client-s3'; import * as spec from '@jsii/spec'; import { sdkStreamMixin } from '@smithy/util-stream'; - import { mockClient } from 'aws-sdk-client-mock'; import { LanguageNotSupportedError, diff --git a/src/__tests__/devapp/__snapshots__/snapshot.test.ts.snap b/src/__tests__/devapp/__snapshots__/snapshot.test.ts.snap index 41d8346bc..e57d62616 100644 --- a/src/__tests__/devapp/__snapshots__/snapshot.test.ts.snap +++ b/src/__tests__/devapp/__snapshots__/snapshot.test.ts.snap @@ -2435,7 +2435,7 @@ Direct link to the function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "ea3bfaa031c0e35e007cf9b1cccc4de0e79a5c7c2dab10ec7f0489246a0c7c2e.zip", + "S3Key": "c2b804973791668a31a3359fedc40ffe6b54a85b0c19238e5027d7236941ce63.zip", }, "Description": "Release note RSS feed updater", "Environment": { @@ -6079,7 +6079,7 @@ Warning: State Machines executions that sent messages to the DLQ will not show a "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "e7b84cc7f34e84cd14b6df84bbf590d644c6bbef900e68943b0b8efbad2ca166.zip", + "S3Key": "a11d7eb83cc26dc9b771f4bd6a7a0d288c97cf306a6e477775ef96be1e9c47a5.zip", }, "Description": "[ConstructHub/Orchestration/NeedsCatalogUpdate] Determines whether a package version requires a catalog update", "Environment": { @@ -11844,7 +11844,7 @@ Direct link to Lambda function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "1854ecb6f125284eabd0fc07139332e7bf50a68003644eb857a277fc1cf11786.zip", + "S3Key": "dce7ea3e58a63f0486d731bdb3ea921fd232e2ea5b3808312323af4c4eabdb95.zip", }, "Description": { "Fn::Join": [ diff --git a/src/backend/catalog-builder/client.lambda-shared.ts b/src/backend/catalog-builder/client.lambda-shared.ts index f40dd95e6..e3aa6f7fd 100644 --- a/src/backend/catalog-builder/client.lambda-shared.ts +++ b/src/backend/catalog-builder/client.lambda-shared.ts @@ -1,6 +1,6 @@ -import * as AWS from 'aws-sdk'; +import { GetObjectCommand, type S3Client } from '@aws-sdk/client-s3'; import { CatalogModel, PackageInfo } from '.'; -import { s3 } from '../shared/aws.lambda-shared'; +import { S3_CLIENT } from '../shared/aws.lambda-shared'; import { requireEnv } from '../shared/env.lambda-shared'; export interface ICatalogClient { @@ -26,7 +26,7 @@ export class CatalogClient implements ICatalogClient { return client; } - private readonly s3: AWS.S3; + private readonly s3Client: S3Client; private readonly bucketName: string; private readonly objectKey: string; @@ -35,7 +35,7 @@ export class CatalogClient implements ICatalogClient { private constructor() { this.bucketName = requireEnv('CATALOG_BUCKET_NAME'); this.objectKey = requireEnv('CATALOG_OBJECT_KEY'); - this.s3 = s3(); + this.s3Client = S3_CLIENT; } /** @@ -53,22 +53,20 @@ export class CatalogClient implements ICatalogClient { Key: this.objectKey, }; - let body: AWS.S3.Body | undefined; + let contents: string | undefined; try { - const data = await this.s3.getObject(params).promise(); - body = data.Body; + const res = await this.s3Client.send(new GetObjectCommand(params)); + contents = await res.Body?.transformToString('utf-8'); } catch (e) { throw new CatalogNotFoundError(`${this.bucketName}/${this.objectKey}`); } - if (!body) { + if (!contents) { throw new Error( `Catalog body is empty at ${this.bucketName}/${this.objectKey}` ); } - const contents = body.toString('utf-8'); - try { const data = JSON.parse(contents) as CatalogModel; if (typeof data != 'object') { diff --git a/src/backend/feed-builder/update-feed.lambda.ts b/src/backend/feed-builder/update-feed.lambda.ts index fa04ec0ec..8d1b2a3bb 100644 --- a/src/backend/feed-builder/update-feed.lambda.ts +++ b/src/backend/feed-builder/update-feed.lambda.ts @@ -1,10 +1,14 @@ -import { AWSError } from 'aws-sdk'; +import { + GetObjectCommand, + NotFound, + PutObjectCommand, +} from '@aws-sdk/client-s3'; import { Feed } from 'feed'; import type * as MarkdownIt from 'markdown-it'; import { CacheStrategy } from '../../caching'; import { CatalogClient } from '../catalog-builder/client.lambda-shared'; -import { s3 } from '../shared/aws.lambda-shared'; +import { S3_CLIENT } from '../shared/aws.lambda-shared'; import * as constants from '../shared/constants'; import { requireEnv } from '../shared/env.lambda-shared'; @@ -77,25 +81,25 @@ export const handler = async () => { console.log('Saving feed data to s3 bucket'); try { - await s3() - .putObject({ + await S3_CLIENT.send( + new PutObjectCommand({ Bucket: bucket, Key: constants.FEED_ATOM_KEY, Body: feed.atom1(), ContentType: 'application/atom+xml', CacheControl: CacheStrategy.default().toString(), }) - .promise(); + ); - await s3() - .putObject({ + await S3_CLIENT.send( + new PutObjectCommand({ Bucket: bucket, Key: constants.FEED_RSS_KEY, Body: feed.rss2(), ContentType: 'application/xml', CacheControl: CacheStrategy.default().toString(), }) - .promise(); + ); console.log('Done saving feed to s3'); } catch (e) { throw new Error(`Unable to save feed to S3: ${e}`); @@ -119,22 +123,26 @@ export const getPackageReleaseNotes = async ( ); try { console.log(`Getting release notes for ${packageName}@${packageVersion}`); - const releaseNotesContent = await s3() - .getObject({ + const releaseNotesContent = await S3_CLIENT.send( + new GetObjectCommand({ Bucket: bucket, Key: releaseNotesKey, }) - .promise(); + ); console.log(`Done reading ${packageName}@${packageVersion}`); const releaseNotes = markdown.render( - releaseNotesContent.Body?.toString() || '' + (await releaseNotesContent.Body?.transformToString()) || '' ); console.log('release notes:', releaseNotes); return releaseNotes; - } catch (e) { - if ((e as AWSError).statusCode === 404) { + } catch (error: any) { + if ( + error instanceof NotFound || + error.name === 'NotFound' || + error.statusCode === 404 + ) { return 'No release notes'; } - throw e; + throw error; } }; diff --git a/src/backend/package-stats/package-stats.lambda.ts b/src/backend/package-stats/package-stats.lambda.ts index 825f45a37..ca0af09df 100644 --- a/src/backend/package-stats/package-stats.lambda.ts +++ b/src/backend/package-stats/package-stats.lambda.ts @@ -1,3 +1,4 @@ +import { PutObjectCommand } from '@aws-sdk/client-s3'; import { metricScope, Unit } from 'aws-embedded-metrics'; import type { Context } from 'aws-lambda'; import got from 'got'; @@ -9,7 +10,7 @@ import { } from './npm-downloads.lambda-shared'; import { CacheStrategy } from '../../caching'; import { CatalogClient } from '../catalog-builder/client.lambda-shared'; -import * as aws from '../shared/aws.lambda-shared'; +import { S3_CLIENT } from '../shared/aws.lambda-shared'; import { requireEnv } from '../shared/env.lambda-shared'; /** @@ -72,9 +73,8 @@ export async function handler(event: any, context: Context) { })(); // Upload the result to S3 and exit. - return aws - .s3() - .putObject({ + return S3_CLIENT.send( + new PutObjectCommand({ Bucket: STATS_BUCKET_NAME, Key: STATS_OBJECT_KEY, Body: JSON.stringify(stats, null, 2), @@ -87,7 +87,7 @@ export async function handler(event: any, context: Context) { 'Package-Stats-Count': `${statsCount}`, }, }) - .promise(); + ); } function updateStats(