diff --git a/src/v2/adapters/aws/dynamodb.adapter.ts b/src/v2/adapters/aws/dynamodb.adapter.ts index b6fb0dea..d933ac1a 100644 --- a/src/v2/adapters/aws/dynamodb.adapter.ts +++ b/src/v2/adapters/aws/dynamodb.adapter.ts @@ -70,7 +70,7 @@ export class DynamoDBAdapter public canHandle(event: unknown): event is DynamoDBStreamEvent { const dynamoDBevent = event as Partial; - if (!Array.isArray(dynamoDBevent.Records)) return false; + if (!Array.isArray(dynamoDBevent?.Records)) return false; const eventSource = dynamoDBevent.Records[0]?.eventSource; @@ -89,8 +89,17 @@ export class DynamoDBAdapter this.options?.dynamoDBForwardMethod, 'POST' ); - const headers = { host: 'dynamodb.amazonaws.com' }; - const [body] = getEventBodyAsBuffer(JSON.stringify(event), false); + + const [body, contentLength] = getEventBodyAsBuffer( + JSON.stringify(event), + false + ); + + const headers = { + host: 'dynamodb.amazonaws.com', + 'content-type': 'application/json', + 'content-length': String(contentLength), + }; return { method, diff --git a/test/adapters/aws/alb.adapter.spec.ts b/test/adapters/aws/alb.adapter.spec.ts index 79c05481..eccdc864 100644 --- a/test/adapters/aws/alb.adapter.spec.ts +++ b/test/adapters/aws/alb.adapter.spec.ts @@ -8,11 +8,11 @@ import { getPathWithQueryStringParams, ILogger, } from '../../../src/v2/core'; +import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createAlbEvent, createAlbEventWithMultiValueHeaders, } from './utils/alb-event'; -import { allAWSEvents } from './utils/events'; describe(AlbAdapter.name, () => { let adapter!: AlbAdapter; @@ -27,27 +27,7 @@ describe(AlbAdapter.name, () => { }); }); - describe('canHandle', () => { - it('should return true when is valid alb event', () => { - const events = allAWSEvents.filter( - ([adapterName]) => adapterName === adapter.getAdapterName() - )!; - - for (const [, albEvent] of events) { - expect(adapter.canHandle(albEvent)).toBe(true); - } - }); - - it('should return false when is not a valid alb event', () => { - const events = allAWSEvents.filter( - ([adapterName]) => adapterName !== adapter.getAdapterName() - ); - - for (const [, event] of events) { - expect(adapter.canHandle(event)).toBe(false); - } - }); - }); + createCanHandleTestsForAdapter(() => new AlbAdapter(), undefined); describe('getRequest', () => { it('should return the correct mapping for the request', () => { @@ -85,7 +65,7 @@ describe(AlbAdapter.name, () => { const resultPath = getPathWithQueryStringParams( path, - event.queryStringParameters! + event.queryStringParameters ); expect(result).toHaveProperty('path', resultPath); }); @@ -125,7 +105,7 @@ describe(AlbAdapter.name, () => { const resultPath = getPathWithQueryStringParams( path, - event.multiValueQueryStringParameters! + event.multiValueQueryStringParameters ); expect(result).toHaveProperty('path', resultPath); }); @@ -159,7 +139,7 @@ describe(AlbAdapter.name, () => { const resultPath = getPathWithQueryStringParams( path, - event.queryStringParameters! + event.queryStringParameters ); expect(result).toHaveProperty('path', resultPath); }); @@ -203,7 +183,7 @@ describe(AlbAdapter.name, () => { const resultPath = getPathWithQueryStringParams( path.replace(stripBasePath, ''), - event.queryStringParameters! + event.queryStringParameters ); expect(result).toHaveProperty('path', resultPath); }); diff --git a/test/adapters/aws/api-gateway-v1.adapter.spec.ts b/test/adapters/aws/api-gateway-v1.adapter.spec.ts index a5f1fd48..96f5e6c0 100644 --- a/test/adapters/aws/api-gateway-v1.adapter.spec.ts +++ b/test/adapters/aws/api-gateway-v1.adapter.spec.ts @@ -10,8 +10,8 @@ import { ILogger, } from '../../../src/v2/core'; import { ServerlessResponse } from '../../../src/v2/network'; +import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createApiGatewayV1 } from './utils/api-gateway-v1'; -import { allAWSEvents } from './utils/events'; describe(ApiGatewayV1Adapter.name, () => { let adapter!: ApiGatewayV1Adapter; @@ -26,27 +26,7 @@ describe(ApiGatewayV1Adapter.name, () => { }); }); - describe('canHandle', () => { - it('should return true when is valid event', () => { - const events = allAWSEvents.filter( - ([adapterName]) => adapterName === adapter.getAdapterName() - )!; - - for (const [, event] of events) { - expect(adapter.canHandle(event)).toBe(true); - } - }); - - it('should return false when is not a valid event', () => { - const events = allAWSEvents.filter( - ([adapterName]) => adapterName !== adapter.getAdapterName() - ); - - for (const [, event] of events) { - expect(adapter.canHandle(event)).toBe(false); - } - }); - }); + createCanHandleTestsForAdapter(() => new ApiGatewayV1Adapter(), undefined); describe('getRequest', () => { it('should return the correct mapping for the request', () => { @@ -77,7 +57,7 @@ describe(ApiGatewayV1Adapter.name, () => { const resultPath = getPathWithQueryStringParams( path, - event.queryStringParameters! + event.queryStringParameters ); expect(result).toHaveProperty('path', resultPath); }); @@ -103,7 +83,7 @@ describe(ApiGatewayV1Adapter.name, () => { const resultPath = getPathWithQueryStringParams( path, - event.queryStringParameters! + event.queryStringParameters ); expect(result).toHaveProperty('path', resultPath); }); @@ -133,7 +113,7 @@ describe(ApiGatewayV1Adapter.name, () => { const resultPath = getPathWithQueryStringParams( path.replace('/prod', ''), - event.queryStringParameters! + event.queryStringParameters ); expect(result).toHaveProperty('path', resultPath); }); diff --git a/test/adapters/aws/api-gateway-v2.adapter.spec.ts b/test/adapters/aws/api-gateway-v2.adapter.spec.ts index 2e80af05..280981de 100644 --- a/test/adapters/aws/api-gateway-v2.adapter.spec.ts +++ b/test/adapters/aws/api-gateway-v2.adapter.spec.ts @@ -10,8 +10,8 @@ import { ILogger, } from '../../../src/v2/core'; import { ServerlessResponse } from '../../../src/v2/network'; +import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createApiGatewayV2 } from './utils/api-gateway-v2'; -import { allAWSEvents } from './utils/events'; describe(ApiGatewayV2Adapter.name, () => { let adapter!: ApiGatewayV2Adapter; @@ -26,27 +26,7 @@ describe(ApiGatewayV2Adapter.name, () => { }); }); - describe('canHandle', () => { - it('should return true when is valid event', () => { - const events = allAWSEvents.filter( - ([adapterName]) => adapterName === adapter.getAdapterName() - )!; - - for (const [, event] of events) { - expect(adapter.canHandle(event)).toBe(true); - } - }); - - it('should return false when is not a valid event', () => { - const events = allAWSEvents.filter( - ([adapterName]) => adapterName !== adapter.getAdapterName() - ); - - for (const [, event] of events) { - expect(adapter.canHandle(event)).toBe(false); - } - }); - }); + createCanHandleTestsForAdapter(() => new ApiGatewayV2Adapter(), undefined); describe('getRequest', () => { it('should return the correct mapping for the request', () => { diff --git a/test/adapters/aws/dynamodb.adapter.spec.ts b/test/adapters/aws/dynamodb.adapter.spec.ts new file mode 100644 index 00000000..932c40dc --- /dev/null +++ b/test/adapters/aws/dynamodb.adapter.spec.ts @@ -0,0 +1,129 @@ +import { DynamoDBStreamEvent } from 'aws-lambda'; +import { DynamoDBAdapter } from '../../../src/v2/adapters/aws'; +import { Resolver } from '../../../src/v2/contracts'; +import { + EmptyResponse, + getEventBodyAsBuffer, + IEmptyResponse, + ILogger, +} from '../../../src/v2/core'; +import { createCanHandleTestsForAdapter } from '../utils/can-handle'; +import { createDynamoDBEvent } from './utils/dynamodb'; + +describe(DynamoDBAdapter.name, () => { + let adapter!: DynamoDBAdapter; + + beforeEach(() => { + adapter = new DynamoDBAdapter(); + }); + + describe('getAdapterName', () => { + it('should be the same name of the class', () => { + expect(adapter.getAdapterName()).toBe(DynamoDBAdapter.name); + }); + }); + + createCanHandleTestsForAdapter(() => new DynamoDBAdapter(), undefined); + + describe('getRequest', () => { + it('should return the correct mapping for the request', () => { + const event = createDynamoDBEvent(); + + const result = adapter.getRequest(event); + + expect(result.method).toBe('POST'); + expect(result.path).toBe('/dynamo'); + expect(result.headers).toHaveProperty('host', 'dynamodb.amazonaws.com'); + expect(result.headers).toHaveProperty('content-type', 'application/json'); + + const [bodyBuffer, contentLength] = getEventBodyAsBuffer( + JSON.stringify(event), + false + ); + + expect(result.body).toBeInstanceOf(Buffer); + expect(result.body).toStrictEqual(bodyBuffer); + + expect(result.headers).toHaveProperty( + 'content-length', + String(contentLength) + ); + }); + + it('should return the correct mapping for the request with custom path and method', () => { + const event = createDynamoDBEvent(); + + const method = 'PUT'; + const path = '/custom/dynamo'; + + const customAdapter = new DynamoDBAdapter({ + dynamoDBForwardMethod: method, + dynamoDBForwardPath: path, + }); + + const result = customAdapter.getRequest(event); + + expect(result.method).toBe(method); + expect(result.path).toBe(path); + expect(result.headers).toHaveProperty('host', 'dynamodb.amazonaws.com'); + expect(result.headers).toHaveProperty('content-type', 'application/json'); + + const [bodyBuffer, contentLength] = getEventBodyAsBuffer( + JSON.stringify(event), + false + ); + + expect(result.body).toBeInstanceOf(Buffer); + expect(result.body).toStrictEqual(bodyBuffer); + + expect(result.headers).toHaveProperty( + 'content-length', + String(contentLength) + ); + }); + }); + + describe('getResponse', () => { + it('should return the correct mapping for the response', () => { + const result = adapter.getResponse(); + + expect(result).toBe(EmptyResponse); + }); + }); + + describe('onErrorWhileForwarding', () => { + it('should resolver call succeed', () => { + const event = createDynamoDBEvent(); + + const error = new Error('fail because I need to test.'); + const resolver: Resolver = { + fail: jest.fn(), + succeed: jest.fn(), + }; + + const oldGetResponse = adapter.getResponse.bind(adapter); + + let getResponseResult: IEmptyResponse; + + adapter.getResponse = jest.fn(() => { + getResponseResult = oldGetResponse(); + + return getResponseResult; + }); + + adapter.onErrorWhileForwarding({ + event, + error, + resolver, + log: {} as ILogger, + respondWithErrors: false, + }); + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(adapter.getResponse).toHaveBeenCalledTimes(0); + + expect(resolver.fail).toHaveBeenCalledTimes(1); + expect(resolver.succeed).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/test/adapters/aws/utils/dynamodb.ts b/test/adapters/aws/utils/dynamodb.ts new file mode 100644 index 00000000..97f408f3 --- /dev/null +++ b/test/adapters/aws/utils/dynamodb.ts @@ -0,0 +1,68 @@ +import { DynamoDBStreamEvent } from 'aws-lambda'; + +export function createDynamoDBEvent(): DynamoDBStreamEvent { + return { + Records: [ + { + eventID: '1', + eventVersion: '1.0', + dynamodb: { + Keys: { + Id: { + N: '101', + }, + }, + NewImage: { + Message: { + S: 'New item!', + }, + Id: { + N: '101', + }, + }, + StreamViewType: 'NEW_AND_OLD_IMAGES', + SequenceNumber: '111', + SizeBytes: 26, + }, + awsRegion: 'us-west-2', + eventName: 'INSERT', + eventSourceARN: 'arn:aws:dynamodb:us-east-1:0000000000:mytable', + eventSource: 'aws:dynamodb', + }, + { + eventID: '2', + eventVersion: '1.0', + dynamodb: { + OldImage: { + Message: { + S: 'New item!', + }, + Id: { + N: '101', + }, + }, + SequenceNumber: '222', + Keys: { + Id: { + N: '101', + }, + }, + SizeBytes: 59, + NewImage: { + Message: { + S: 'This item has changed', + }, + Id: { + N: '101', + }, + }, + StreamViewType: 'NEW_AND_OLD_IMAGES', + }, + awsRegion: 'us-west-2', + eventName: 'MODIFY', + eventSourceARN: 'arn:aws:dynamodb:us-east-1:0000000000:mytable', + eventSource: 'aws:dynamodb', + }, + ], + }; +} diff --git a/test/adapters/aws/utils/events.ts b/test/adapters/aws/utils/events.ts index 3036fd66..84613fe2 100644 --- a/test/adapters/aws/utils/events.ts +++ b/test/adapters/aws/utils/events.ts @@ -2,6 +2,7 @@ import { AlbAdapter, ApiGatewayV1Adapter, ApiGatewayV2Adapter, + DynamoDBAdapter, } from '../../../../src/v2/adapters/aws'; import { createAlbEvent, @@ -9,6 +10,7 @@ import { } from './alb-event'; import { createApiGatewayV1 } from './api-gateway-v1'; import { createApiGatewayV2 } from './api-gateway-v2'; +import { createDynamoDBEvent } from './dynamodb'; export const allAWSEvents: Array<[string, any]> = [ ['fake-to-test-undefined-event', undefined], @@ -48,4 +50,5 @@ export const allAWSEvents: Array<[string, any]> = [ createApiGatewayV2('GET', '/collaborators', undefined, {}, { page: '2' }), ], [ApiGatewayV2Adapter.name, createApiGatewayV2('collaborators', '/users')], + [DynamoDBAdapter.name, createDynamoDBEvent()], ]; diff --git a/test/adapters/utils/can-handle.ts b/test/adapters/utils/can-handle.ts new file mode 100644 index 00000000..74c8ba86 --- /dev/null +++ b/test/adapters/utils/can-handle.ts @@ -0,0 +1,41 @@ +import { AdapterContract } from '../../../src/v2/contracts'; +import { ILogger } from '../../../src/v2/core'; +import { allEvents } from './events'; + +export function createCanHandleTestsForAdapter( + adapterFactory: () => AdapterContract, + context: TContext, + logger: ILogger = {} as ILogger +): void { + let adapter!: AdapterContract; + + beforeEach(() => { + adapter = adapterFactory(); + }); + + describe('canHandle', () => { + it('should return true when is valid event', () => { + const events = allEvents.filter( + ([adapterName]) => adapterName === adapter.getAdapterName() + )!; + + expect(events.length).toBeGreaterThan(0); + + for (const [, event] of events) { + expect(adapter.canHandle(event, context, logger)).toBe(true); + } + }); + + it('should return false when is not a valid event', () => { + const events = allEvents.filter( + ([adapterName]) => adapterName !== adapter.getAdapterName() + ); + + expect(events.length).toBeGreaterThan(0); + + for (const [, event] of events) { + expect(adapter.canHandle(event, context, logger)).toBe(false); + } + }); + }); +}