diff --git a/tests/Request.test.ts b/tests/Request.test.ts index 5b0c345..b3b3f14 100644 --- a/tests/Request.test.ts +++ b/tests/Request.test.ts @@ -3,13 +3,13 @@ import { expect } from 'chai'; import { Request, Application } from '../src'; import { RequestEvent } from '../src/request-response-types'; import { - apiGatewayRequest, handlerContext, albRequest, albMultiValHeadersRequest, albRequestRawQuery, apiGatewayRequestRawQuery, albMultiValHeadersRawQuery, + makeAPIGatewayRequestEvent, } from './samples'; import { isKeyValueStringObject, Optional } from '@silvermine/toolbox'; import ConsoleLogger from '../src/logging/ConsoleLogger'; @@ -21,9 +21,9 @@ describe('Request', () => { beforeEach(() => { app = new Application(); - allEventTypes = [ apiGatewayRequest(), albRequest(), albMultiValHeadersRequest() ]; + allEventTypes = [ makeAPIGatewayRequestEvent(), albRequest(), albMultiValHeadersRequest() ]; allRequestTypes = [ - new Request(app, apiGatewayRequest(), handlerContext()), + new Request(app, makeAPIGatewayRequestEvent(), handlerContext()), new Request(app, albRequest(), handlerContext()), new Request(app, albMultiValHeadersRequest(), handlerContext()), ]; @@ -113,7 +113,7 @@ describe('Request', () => { describe('header functionality', () => { it('works with multi-value headers provided in the event (and is case-insensitive)', () => { - let apigw = new Request(app, apiGatewayRequest(), handlerContext()), + let apigw = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()), albmv = new Request(app, albMultiValHeadersRequest(), handlerContext()); _.each([ apigw, albmv ], (req) => { @@ -250,15 +250,16 @@ describe('Request', () => { it('parses proper values - APIGW', () => { _.each(testCases, (testCase) => { - let evt: RequestEvent = apiGatewayRequest(), - req; + const evt: RequestEvent = makeAPIGatewayRequestEvent({ + headers: { + 'X-Forwarded-Host': testCase.xForwardedHost, + 'Host': testCase.host, + }, + }); - evt.headers.Host = testCase.host; + let req; - if (testCase.xForwardedHost) { - evt.headers['X-Forwarded-Host'] = testCase.xForwardedHost; - evt.multiValueHeaders['X-Forwarded-Host'] = [ testCase.xForwardedHost ]; - } else { + if (!testCase.xForwardedHost) { delete evt.headers['X-Forwarded-Host']; delete evt.multiValueHeaders['X-Forwarded-Host']; } @@ -311,7 +312,7 @@ describe('Request', () => { describe('ip property', () => { it('parses correctly', () => { - let req = new Request(app, apiGatewayRequest(), handlerContext()), + let req = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()), evt: RequestEvent; expect(req.ip).to.eql('12.12.12.12'); @@ -319,11 +320,11 @@ describe('Request', () => { // API Gateway requests always use the one from the request context, so it // shouldn't matter what the 'trust proxy' setting is set to. app.enable('trust proxy'); - req = new Request(app, apiGatewayRequest(), handlerContext()); + req = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()); expect(req.ip).to.eql('12.12.12.12'); app.disable('trust proxy'); - req = new Request(app, apiGatewayRequest(), handlerContext()); + req = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()); expect(req.ip).to.eql('12.12.12.12'); // ALB requests don't have the IP in the request context, so it's dependent on @@ -360,7 +361,7 @@ describe('Request', () => { it('properly detects event source', () => { let alb = new Request(app, albRequest(), handlerContext()), albmv = new Request(app, albMultiValHeadersRequest(), handlerContext()), - apigw = new Request(app, apiGatewayRequest(), handlerContext()); + apigw = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()); expect(alb.eventSourceType).to.eql(Request.SOURCE_ALB); expect(alb.isALB()).to.eql(true); @@ -379,10 +380,9 @@ describe('Request', () => { describe('protocol / secure properties', () => { it('parses proper values - APIGW', () => { - let evt, req; - // APIGW should always be HTTPS, and not care about headers - req = new Request(app, apiGatewayRequest(), handlerContext()); + const req = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()); + app.disable('trust proxy'); expect(req.protocol).to.eql('https'); expect(req.secure).to.eql(true); @@ -390,9 +390,6 @@ describe('Request', () => { expect(req.protocol).to.eql('https'); expect(req.secure).to.eql(true); - evt = apiGatewayRequest(); - evt.headers['X-Forwarded-Proto'] = 'http'; - evt.multiValueHeaders['X-Forwarded-Proto'] = [ 'http' ]; app.disable('trust proxy'); expect(req.protocol).to.eql('https'); expect(req.secure).to.eql(true); @@ -479,7 +476,7 @@ describe('Request', () => { }); it('parses arrays of values correctly - when multi-value is supported', () => { - _.each([ apiGatewayRequest(), albMultiValHeadersRequest() ], (evt) => { + _.each([ makeAPIGatewayRequestEvent(), albMultiValHeadersRequest() ], (evt) => { const req = new Request(app, evt, handlerContext()); expect(req.query.x).to.eql([ '1', '2' ]); @@ -503,7 +500,7 @@ describe('Request', () => { } }; - test(apiGatewayRequest(), [ 'bar b', 'baz c' ]); + test(makeAPIGatewayRequestEvent(), [ 'bar b', 'baz c' ]); test(albRequest(), 'baz c'); test(albMultiValHeadersRequest(), [ 'bar b', 'baz c' ]); }); @@ -537,7 +534,7 @@ describe('Request', () => { }); it('does not throw an error when bad values supplied', () => { - const apigwReq = apiGatewayRequest(); + const apigwReq = makeAPIGatewayRequestEvent(); // Simulate a bad value (from an injection attack) being supplied apigwReq.queryStringParameters = { @@ -558,7 +555,7 @@ describe('Request', () => { }); it('only contains the expected data', () => { - let req = new Request(app, apiGatewayRequest(), handlerContext()); + let req = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()); expect(req.query).to.eql({ foo: { a: [ 'bar b', 'baz c' ] }, @@ -589,7 +586,7 @@ describe('Request', () => { it('sets body to null for empty values', () => { _.each([ null, undefined, '' ], (body) => { - let req = new Request(app, _.extend(apiGatewayRequest(), { body }), handlerContext()); + let req = new Request(app, makeAPIGatewayRequestEvent({ body }), handlerContext()); expect(req.body).to.strictlyEqual(null); }); @@ -604,7 +601,7 @@ describe('Request', () => { _.each(bodies, (o) => { let ext = { body: JSON.stringify(o), multiValueHeaders: { 'Content-Type': [ 'application/json; charset=utf-8' ] } }, - req = new Request(app, _.extend(apiGatewayRequest(), ext), handlerContext()); + req = new Request(app, makeAPIGatewayRequestEvent(ext), handlerContext()); expect(req.body).to.eql(o); }); @@ -615,7 +612,7 @@ describe('Request', () => { _.each(bodies, (body) => { let ext = { body, multiValueHeaders: { 'Content-Type': [ 'application/json; charset=utf-8' ] } }, - req = new Request(app, _.extend(apiGatewayRequest(), ext), handlerContext()); + req = new Request(app, makeAPIGatewayRequestEvent(ext), handlerContext()); expect(req.body).to.strictlyEqual(null); }); @@ -627,7 +624,7 @@ describe('Request', () => { _.each(bodies, (body) => { let ext = { body, multiValueHeaders: { 'Content-Type': [ 'foo/bar; charset=utf-8' ] } }, - req = new Request(app, _.extend(apiGatewayRequest(), ext), handlerContext()); + req = new Request(app, makeAPIGatewayRequestEvent(ext), handlerContext()); expect(req.body).to.strictlyEqual(body); }); @@ -638,7 +635,7 @@ describe('Request', () => { describe('`url` property', () => { it('should be able to be updated', () => { - let req = new Request(app, apiGatewayRequest(), handlerContext()), + let req = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()), newURL = '/test'; // Assert that we have a valid test @@ -649,7 +646,7 @@ describe('Request', () => { }); it('should accept blank values', () => { - let req = new Request(app, apiGatewayRequest(), handlerContext()), + let req = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()), newURL = ''; // Assert that we have a valid test @@ -660,7 +657,7 @@ describe('Request', () => { }); it('should update `path` when `url` changes', () => { - let req = new Request(app, apiGatewayRequest(), handlerContext()), + let req = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()), newURL = '/test'; // Assert that we have a valid test @@ -671,7 +668,7 @@ describe('Request', () => { }); it('should update the parent request\'s `url` and related properties when a sub-request\'s `url` is updated', () => { - let event = apiGatewayRequest(), + let event = makeAPIGatewayRequestEvent(), req, subReq, subSubReq; // Assert that we have a valid test @@ -724,13 +721,13 @@ describe('Request', () => { } it('exists and logs messages', () => { - let req = new Request(app, apiGatewayRequest(), handlerContext()); + let req = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()); testLog(req); }); it('is inherited from parent requests to sub-requests', () => { - let req = new Request(app, apiGatewayRequest(), handlerContext()), + let req = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()), subReq = req.makeSubRequest(''); testLog(subReq); diff --git a/tests/Response.test.ts b/tests/Response.test.ts index c94f614..1a0e612 100644 --- a/tests/Response.test.ts +++ b/tests/Response.test.ts @@ -2,7 +2,12 @@ import _ from 'underscore'; import { expect } from 'chai'; import { fail } from 'assert'; import { Request, Response, Application } from '../src'; -import { apiGatewayRequest, handlerContext, albRequest, albMultiValHeadersRequest } from './samples'; +import { + handlerContext, + albRequest, + albMultiValHeadersRequest, + makeAPIGatewayRequestEvent, +} from './samples'; import sinon, { spy, assert, SinonSpy, SinonSandbox, SinonFakeTimers } from 'sinon'; import { StringArrayOfStringsMap, StringMap } from '@silvermine/toolbox'; import { RequestEvent, CookieOpts } from '../src/request-response-types'; @@ -28,7 +33,7 @@ describe('Response', () => { beforeEach(() => { app = new Application(); - sampleReq = new Request(app, apiGatewayRequest(), handlerContext()); + sampleReq = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()); sampleResp = new Response(app, sampleReq, EMPTY_CB); }); @@ -152,7 +157,7 @@ describe('Response', () => { // This is for JS-only functionality. TypeScript users will be safeguarded from // doing this by type safety. const val: any = true, - req = new Request(app, apiGatewayRequest(), handlerContext()), + req = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()), logger = new ConsoleLogger({ interface: 'ALB', getTimeUntilFnTimeout: () => { return 0; } }), errorFnSpy = sinon.spy(); @@ -479,16 +484,15 @@ describe('Response', () => { if (referer === null) { // use the existing header - evt = apiGatewayRequest(); + evt = makeAPIGatewayRequestEvent(); } else if (referer === false) { // remove existing header - evt = apiGatewayRequest(); + evt = makeAPIGatewayRequestEvent(); delete evt.multiValueHeaders.Referer; delete evt.headers.Referer; } else { // override the existing one - evt = _.extend(apiGatewayRequest(), { - multiValueHeaders: { 'Referer': [ referer ] }, + evt = makeAPIGatewayRequestEvent({ headers: { 'Referer': referer }, }); } @@ -619,7 +623,7 @@ describe('Response', () => { describe('body property', () => { it('contains what was sent in the response', () => { - const resp = new Response(app, new Request(app, apiGatewayRequest(), handlerContext()), spy()); + const resp = new Response(app, new Request(app, makeAPIGatewayRequestEvent(), handlerContext()), spy()); resp.send({ foo: 'bar' }); expect(resp.body).to.eql(JSON.stringify({ foo: 'bar' })); @@ -680,10 +684,10 @@ describe('Response', () => { expect(cb.firstCall.args).to.eql([ undefined, output ]); }; - it('sends immediately - APIGW', () => { test(apiGatewayRequest(), false, false); }); + it('sends immediately - APIGW', () => { test(makeAPIGatewayRequestEvent(), false, false); }); it('sends immediately - ALB', () => { test(albRequest(), 'OK', false); }); it('sends immediately - ALB MV', () => { test(albMultiValHeadersRequest(), 'OK', false); }); - it('sends after setting headers - APIGW', () => { test(apiGatewayRequest(), false, true); }); + it('sends after setting headers - APIGW', () => { test(makeAPIGatewayRequestEvent(), false, true); }); it('sends after setting headers - ALB', () => { test(albRequest(), 'OK', true); }); it('sends after setting headers - ALB MV', () => { test(albMultiValHeadersRequest(), 'OK', true); }); }); @@ -715,10 +719,10 @@ describe('Response', () => { expect(cb.firstCall.args).to.eql([ undefined, output ]); }; - it('sends immediately - APIGW', () => { test(apiGatewayRequest(), false, false); }); + it('sends immediately - APIGW', () => { test(makeAPIGatewayRequestEvent(), false, false); }); it('sends immediately - ALB', () => { test(albRequest(), 'OK', false); }); it('sends immediately - ALB MV', () => { test(albMultiValHeadersRequest(), 'OK', false); }); - it('sends after headers - APIGW', () => { test(apiGatewayRequest(), false, true); }); + it('sends after headers - APIGW', () => { test(makeAPIGatewayRequestEvent(), false, true); }); it('sends after headers - ALB', () => { test(albRequest(), 'OK', true); }); it('sends after headers - ALB MV', () => { test(albMultiValHeadersRequest(), 'OK', true); }); @@ -730,7 +734,7 @@ describe('Response', () => { } }; - it('sends with alternate status code - APIGW', () => { test(apiGatewayRequest(), false, false, ext1); }); + it('sends with alternate status code - APIGW', () => { test(makeAPIGatewayRequestEvent(), false, false, ext1); }); it('sends with alternate status code - ALB', () => { test(albRequest(), 'OK', false, ext1); }); it('sends with alternate status code - ALB MV', () => { test(albMultiValHeadersRequest(), 'OK', false, ext1); }); @@ -743,7 +747,7 @@ describe('Response', () => { } }; - it('overrides previously-set content type - APIGW', () => { test(apiGatewayRequest(), false, false, ext2); }); + it('overrides previously-set content type - APIGW', () => { test(makeAPIGatewayRequestEvent(), false, false, ext2); }); it('overrides previously-set content type - ALB', () => { test(albRequest(), 'OK', false, ext2); }); it('overrides previously-set content type - ALB MV', () => { test(albMultiValHeadersRequest(), 'OK', false, ext2); }); }); @@ -794,7 +798,7 @@ describe('Response', () => { expect(cb.firstCall.args).to.eql([ undefined, output ]); }; - it('sends immediately - APIGW', () => { test(apiGatewayRequest(), false); }); + it('sends immediately - APIGW', () => { test(makeAPIGatewayRequestEvent(), false); }); it('sends immediately - ALB', () => { test(albRequest(), 'OK'); }); it('sends immediately - ALB MV', () => { test(albMultiValHeadersRequest(), 'OK'); }); @@ -806,7 +810,7 @@ describe('Response', () => { } }; - it('sends after headers - APIGW', () => { test(apiGatewayRequest(), false, ext1); }); + it('sends after headers - APIGW', () => { test(makeAPIGatewayRequestEvent(), false, ext1); }); it('sends after headers - ALB', () => { test(albRequest(), 'OK', ext1); }); it('sends after headers - ALB MV', () => { test(albMultiValHeadersRequest(), 'OK', ext1); }); @@ -815,7 +819,7 @@ describe('Response', () => { }; it('works with custom callback param name - APIGW', () => { - test(apiGatewayRequest(), false, ext2, { queryParamName: 'cbFnName' }); + test(makeAPIGatewayRequestEvent(), false, ext2, { queryParamName: 'cbFnName' }); }); it('works with custom callback param name - ALB', () => { test(albRequest(), 'OK', ext2, { queryParamName: 'cbFnName' }); @@ -831,7 +835,7 @@ describe('Response', () => { }; it('works like JSON when no callback in query - APIGW', () => { - test(apiGatewayRequest(), false, expectJSON, { queryParamName: false }); + test(makeAPIGatewayRequestEvent(), false, expectJSON, { queryParamName: false }); }); it('works like JSON when no callback in query - ALB', () => { test(albRequest(), 'OK', expectJSON, { queryParamName: false }); @@ -841,7 +845,7 @@ describe('Response', () => { }); it('works like JSON when non-callback param is in query - APIGW', () => { - test(apiGatewayRequest(), false, expectJSON, { queryParamName: 'notcallback' }); + test(makeAPIGatewayRequestEvent(), false, expectJSON, { queryParamName: 'notcallback' }); }); it('works like JSON when non-callback param is in query - ALB', () => { test(albRequest(), 'OK', expectJSON, { queryParamName: 'notcallback' }); @@ -851,7 +855,7 @@ describe('Response', () => { }); it('uses the first callback param value listed - APIGW', () => { - test(apiGatewayRequest(), false, undefined, { + test(makeAPIGatewayRequestEvent(), false, undefined, { queryParamValues: [ 'callbackOne', 'callbackTwo' ], }); }); @@ -867,7 +871,7 @@ describe('Response', () => { }); it('allows for the callback param value to contain an array index - APIGW', () => { - test(apiGatewayRequest(), false, undefined, { + test(makeAPIGatewayRequestEvent(), false, undefined, { queryParamValues: [ 'callbacks[123]' ], }); }); @@ -883,7 +887,7 @@ describe('Response', () => { }); it('returns JSON when callback param value contains invalid characters - APIGW', () => { - test(apiGatewayRequest(), false, expectJSON, { + test(makeAPIGatewayRequestEvent(), false, expectJSON, { queryParamValues: [ 'bad;fn()' ], }); }); @@ -905,7 +909,7 @@ describe('Response', () => { }; it('escapes UTF newline and paragraph separators - APIGW', () => { - test(apiGatewayRequest(), false, ext3, { responseObject: utfInput }); + test(makeAPIGatewayRequestEvent(), false, ext3, { responseObject: utfInput }); }); it('escapes UTF newline and paragraph separators - ALB', () => { test(albRequest(), 'OK', ext3, { responseObject: utfInput }); @@ -1040,7 +1044,7 @@ describe('Response', () => { o.multiValueHeaders['Content-Type'] = [ 'text/html' ]; }; - it('sends string immediately - APIGW', () => { test(apiGatewayRequest(), 200, false, 'body', ext1); }); + it('sends string immediately - APIGW', () => { test(makeAPIGatewayRequestEvent(), 200, false, 'body', ext1); }); it('sends string immediately - ALB', () => { test(albRequest(), 200, 'OK', 'body', ext1); }); it('sends string immediately - ALB MV', () => { test(albMultiValHeadersRequest(), 200, 'OK', 'body', ext1); }); @@ -1049,7 +1053,7 @@ describe('Response', () => { o.multiValueHeaders['Content-Type'] = [ 'foo/bar' ]; }; - it('sends string with existing content type - APIGW', () => { test(apiGatewayRequest(), 200, false, 'body', ext2); }); + it('sends string with existing content type - APIGW', () => { test(makeAPIGatewayRequestEvent(), 200, false, 'body', ext2); }); it('sends string with existing content type - ALB', () => { test(albRequest(), 200, 'OK', 'body', ext2); }); it('sends string with existing content type - ALB MV', () => { test(albMultiValHeadersRequest(), 200, 'OK', 'body', ext2); }); @@ -1060,7 +1064,7 @@ describe('Response', () => { o.multiValueHeaders['Content-Type'] = [ 'application/json; charset=utf-8' ]; }; - it('sends JSON immediately - APIGW', () => { test(apiGatewayRequest(), 200, false, bodyObj, ext3); }); + it('sends JSON immediately - APIGW', () => { test(makeAPIGatewayRequestEvent(), 200, false, bodyObj, ext3); }); it('sends JSON immediately - ALB', () => { test(albRequest(), 200, 'OK', bodyObj, ext3); }); it('sends JSON immediately - ALB MV', () => { test(albMultiValHeadersRequest(), 200, 'OK', bodyObj, ext3); }); }); @@ -1085,10 +1089,10 @@ describe('Response', () => { }; _.each({ 'OK': 200, 'Created': 201, 'Not Found': 404, 'I\'m a teapot': 418 }, (code, msg) => { - it(`sends immediately - APIGW - ${code} ${msg}`, () => { test(apiGatewayRequest(), code, false, false); }); + it(`sends immediately - APIGW - ${code} ${msg}`, () => { test(makeAPIGatewayRequestEvent(), code, false, false); }); it(`sends immediately - ALB - ${code} ${msg}`, () => { test(albRequest(), code, msg, false); }); it(`sends immediately - ALB MV - ${code} ${msg}`, () => { test(albMultiValHeadersRequest(), code, msg, false); }); - it(`sends after headers - APIGW - ${code} ${msg}`, () => { test(apiGatewayRequest(), code, false, true); }); + it(`sends after headers - APIGW - ${code} ${msg}`, () => { test(makeAPIGatewayRequestEvent(), code, false, true); }); it(`sends after headers - ALB - ${code} ${msg}`, () => { test(albRequest(), code, msg, true); }); it(`sends after headers - ALB MV - ${code} ${msg}`, () => { test(albMultiValHeadersRequest(), code, msg, true); }); }); diff --git a/tests/chains/MatchAllRequestsProcessorChain.test.ts b/tests/chains/MatchAllRequestsProcessorChain.test.ts index a698f18..f7a2a0b 100644 --- a/tests/chains/MatchAllRequestsProcessorChain.test.ts +++ b/tests/chains/MatchAllRequestsProcessorChain.test.ts @@ -1,5 +1,5 @@ import { Application, Request } from '../../src'; -import { apiGatewayRequest, handlerContext } from '../samples'; +import { handlerContext, makeAPIGatewayRequestEvent } from '../samples'; import { IRequestMatchingProcessorChain } from '../../src/chains/ProcessorChain'; import { MatchAllRequestsProcessorChain } from '../../src/chains/MatchAllRequestsProcessorChain'; import { expect } from 'chai'; @@ -10,7 +10,7 @@ describe('MatchAllRequestsProcessorChain', () => { beforeEach(() => { app = new Application(); - req = new Request(app, apiGatewayRequest(), handlerContext()); + req = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()); }); it('always says yes', () => { diff --git a/tests/chains/ProcessorChain.test.ts b/tests/chains/ProcessorChain.test.ts index 3f492cf..c442b51 100644 --- a/tests/chains/ProcessorChain.test.ts +++ b/tests/chains/ProcessorChain.test.ts @@ -5,7 +5,7 @@ import { wrapRequestProcessors } from '../../src/utils/wrapRequestProcessor'; import { SinonSpy, spy, stub, assert } from 'sinon'; import makeRequestProcessor from '../test-utils/makeRequestProcessor'; import { Application, Request, Response } from '../../src'; -import { apiGatewayRequest, handlerContext } from '../samples'; +import { makeAPIGatewayRequestEvent, handlerContext } from '../samples'; function assertAllCalledOnceInOrder(...spies: SinonSpy[]): void { _.each(spies, (s) => { assert.calledOnce(s); }); @@ -38,7 +38,7 @@ describe('ProcessorChain', () => { beforeEach(() => { app = new Application(); - req = new Request(app, apiGatewayRequest(), handlerContext()); + req = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()); resp = new Response(app, req, spy()); done = spy(); }); diff --git a/tests/chains/RouteMatchingProcessorChain.test.ts b/tests/chains/RouteMatchingProcessorChain.test.ts index f2512fa..56ecbe4 100644 --- a/tests/chains/RouteMatchingProcessorChain.test.ts +++ b/tests/chains/RouteMatchingProcessorChain.test.ts @@ -1,6 +1,5 @@ -import _ from 'underscore'; import { Application, Request, Response } from '../../src'; -import { apiGatewayRequest, handlerContext } from '../samples'; +import { handlerContext, makeAPIGatewayRequestEvent } from '../samples'; import { expect } from 'chai'; import { RouteMatchingProcessorChain } from '../../src/chains/RouteMatchingProcessorChain'; import { PathParams } from '../../src/interfaces'; @@ -22,7 +21,7 @@ describe('RouteMatchingProcessorChain', () => { describe('matches', () => { const test = (method: string | undefined, routes: PathParams, path: string, expectation: boolean, caseSensitive = false): void => { let app = new Application(), - req = new Request(app, _.extend(apiGatewayRequest(), { path: path }), handlerContext()), + req = new Request(app, makeAPIGatewayRequestEvent({ path: path }), handlerContext()), chain = new RouteMatchingProcessorChain([], routes, method, caseSensitive); expect(chain.matches(req)).to.eql(expectation); @@ -138,7 +137,7 @@ describe('RouteMatchingProcessorChain', () => { describe('makeSubRequest', () => { const test = (routes: PathParams, path: string, expectation: StringMap): void => { let app = new Application(), - req = new Request(app, _.extend(apiGatewayRequest(), { path: path }), handlerContext()), + req = new Request(app, makeAPIGatewayRequestEvent({ path: path }), handlerContext()), chain = new TestRouteMatchingProcessorChain([], routes), subReq = chain._makeSubRequest(req); @@ -154,7 +153,7 @@ describe('RouteMatchingProcessorChain', () => { // sending this character using UTF-8 encoding (i.e. %C3%AA) const path = '/hello/%EA', app = new Application(), - req = new Request(app, _.extend(apiGatewayRequest(), { path }), handlerContext()), + req = new Request(app, makeAPIGatewayRequestEvent({ path }), handlerContext()), chain = new RouteMatchingProcessorChain([], '/hello/:name'), resp = new Response(app, req, spy()), done = spy(); diff --git a/tests/chains/SubRouterProcessorChain.test.ts b/tests/chains/SubRouterProcessorChain.test.ts index f857b71..f8ac860 100644 --- a/tests/chains/SubRouterProcessorChain.test.ts +++ b/tests/chains/SubRouterProcessorChain.test.ts @@ -1,6 +1,5 @@ -import _ from 'underscore'; import { Application, Request, Router, Response } from '../../src'; -import { apiGatewayRequest, handlerContext } from '../samples'; +import { handlerContext, makeAPIGatewayRequestEvent } from '../samples'; import { expect } from 'chai'; import { PathParams, NextCallback } from '../../src/interfaces'; import { SinonSpy, spy, assert } from 'sinon'; @@ -27,7 +26,7 @@ describe('SubRouterProcessorChain', () => { describe('matches', () => { const test = (routes: PathParams, path: string, expectation: boolean): void => { let app = new Application(), - req = new Request(app, _.extend(apiGatewayRequest(), { path: path }), handlerContext()), + req = new Request(app, makeAPIGatewayRequestEvent({ path: path }), handlerContext()), router = new TestRouter(spy()), chain = new SubRouterProcessorChain(routes, router, app.routerOptions); @@ -53,7 +52,7 @@ describe('SubRouterProcessorChain', () => { describe('run', () => { const test = (routes: PathParams, path: string, baseURL: string): void => { let app = new Application(), - req = new Request(app, _.extend(apiGatewayRequest(), { path: path }), handlerContext()), + req = new Request(app, makeAPIGatewayRequestEvent({ path: path }), handlerContext()), resp = new Response(app, req, spy()), done = spy(), handle = spy(), diff --git a/tests/integration-tests.test.ts b/tests/integration-tests.test.ts index 3d463b2..3e4d7b3 100644 --- a/tests/integration-tests.test.ts +++ b/tests/integration-tests.test.ts @@ -1,10 +1,10 @@ import _ from 'underscore'; import { - apiGatewayRequest, handlerContext, albRequest, albMultiValHeadersRequest, apiGatewayRequestRawQuery, + makeAPIGatewayRequestEvent, } from './samples'; import { spy, SinonSpy, assert } from 'sinon'; import { Application, Request, Response, Router } from '../src'; @@ -21,8 +21,8 @@ describe('integration tests', () => { app = new Application(); }); - const makeRequestEvent = (path: string, base?: RequestEvent, method?: string): RequestEvent => { - return _.extend(base || apiGatewayRequest(), { path: path, httpMethod: (method || 'GET') }); + const makeRequestEvent = (path: string, base: RequestEvent, method?: string): RequestEvent => { + return _.extend(base, { path: path, httpMethod: (method || 'GET') }); }; const testWithLastResortHandler = (code: number, desc: string, testHeaders: string[] = [], expectedBody = ''): void => { @@ -54,7 +54,7 @@ describe('integration tests', () => { const testOutcome = (method: string, path: string, expectedBody: string): void => { const cb = spy(), - evt = makeRequestEvent(path, apiGatewayRequest(), method); + evt = makeAPIGatewayRequestEvent({ path, httpMethod: method }); app.run(evt, handlerContext(), cb); @@ -121,7 +121,7 @@ describe('integration tests', () => { }; it('works - APIGW', () => { - const cb = test(makeRequestEvent('/hello/world')); + const cb = test(makeAPIGatewayRequestEvent({ path: '/hello/world' })); assert.calledWithExactly(cb, undefined, { statusCode: 200, @@ -342,7 +342,10 @@ describe('integration tests', () => { // eslint-disable-next-line max-len,max-params const addTestsForMethod = (method: string, code: number, desc: string, hdrName: string, hdrVal: string, expectedBody: string, prep: () => void, contentType?: string): void => { const baseEvents = { - 'APIGW': apiGatewayRequest(), + 'APIGW': makeAPIGatewayRequestEvent({ + path: '/hello/world', + httpMethod: method, + }), 'ALB': albRequest(), 'ALBMV': albMultiValHeadersRequest(), }; @@ -760,7 +763,7 @@ describe('integration tests', () => { describe('request object', () => { it('has an immutable context property', () => { - let evt = makeRequestEvent('/test', apiGatewayRequest(), 'GET'), + let evt = makeAPIGatewayRequestEvent({ path: '/test', httpMethod: 'GET' }), ctx = handlerContext(true), handler; diff --git a/tests/samples.ts b/tests/samples.ts index 4f6c6a8..85299a4 100644 --- a/tests/samples.ts +++ b/tests/samples.ts @@ -4,7 +4,44 @@ import { ApplicationLoadBalancerEventRequestContext, APIGatewayRequestEvent, ApplicationLoadBalancerRequestEvent } from '../src/request-response-types'; -import { Context } from 'aws-lambda'; +import { APIGatewayEventIdentity, Context } from 'aws-lambda'; +import withDefault from './test-utils/withDefault'; + +interface MakeAPIGatewayRequestEventContextParams { + httpMethod?: string; +} + +interface MakeAPIGatewayRequestEventHeadersParams { + [name: string]: string | undefined; +} + +interface MakeAPIGatewayRequestEventMultiValueHeaderParams { + [name: string]: string[] | undefined; +} + +interface MakeAPIGatewayRequestEventMultiValueHeaderParamsInput { + headers?: MakeAPIGatewayRequestEventHeadersParams; + multiValueHeaders?: MakeAPIGatewayRequestEventMultiValueHeaderParams; +} + +interface MakeAPIGatewayRequestEventParams { + httpMethod?: string; + path?: string; + body?: string | null; + + /** `headers` is preferrable for defining both, `headers` and `multiValueHeaders` since + * it automatically mirrors the same keys and values in both. But, in some cases, the + * test may define specific `multiValueHeaders`, so it is also possible to manually + * assign values directly to `multiValueHeaders` when needed. + * If both are provided, the objects are merged. `multiValueHeaders` has precedence + * over `headers`, so if the same field name is provided in both, `multiValueHeaders` + * will overwrite `headers` for that field. + */ + headers?: MakeAPIGatewayRequestEventHeadersParams; + + /** See comment for the headers attribute */ + multiValueHeaders?: MakeAPIGatewayRequestEventMultiValueHeaderParams; +} export const handlerContext = (fillAllFields: boolean = false): Context => { let ctx: Context; @@ -51,103 +88,8 @@ export const handlerContext = (fillAllFields: boolean = false): Context => { return ctx; }; -export const apiGatewayRequestContext = (): APIGatewayEventRequestContext => { - return { - accountId: '123456789012', - apiId: 'someapi', - authorizer: null, - httpMethod: 'GET', - identity: { - accessKey: null, - accountId: null, - apiKey: null, - apiKeyId: null, - caller: null, - clientCert: null, - cognitoAuthenticationProvider: null, - cognitoAuthenticationType: null, - cognitoIdentityId: null, - cognitoIdentityPoolId: null, - principalOrgId: null, - sourceIp: '12.12.12.12', - user: null, - userAgent: 'curl/7.54.0', - userArn: null, - }, - path: '/prd', - protocol: 'HTTP/1.1', - stage: 'prd', - requestId: 'a507736b-259e-11e9-8fcf-4f1f08c4591e', - requestTimeEpoch: 1548969891530, - resourceId: 'reas23acc', - resourcePath: '/', - }; -}; - export const apiGatewayRequestRawQuery = '?&foo[a]=bar%20b&foo[a]=baz%20c&x=1&x=2&y=z'; -export const apiGatewayRequest = (): APIGatewayRequestEvent => { - return { - body: null, - httpMethod: 'GET', - isBase64Encoded: false, - path: '/echo/asdf/a', - resource: '/{proxy+}', - pathParameters: { proxy: 'echo/asdf/a' }, - stageVariables: null, - requestContext: apiGatewayRequestContext(), - headers: { - Accept: '*/*', - 'CloudFront-Forwarded-Proto': 'https', - 'CloudFront-Is-Desktop-Viewer': 'true', - 'CloudFront-Is-Mobile-Viewer': 'false', - 'CloudFront-Is-SmartTV-Viewer': 'false', - 'CloudFront-Is-Tablet-Viewer': 'false', - 'CloudFront-Viewer-Country': 'US', - Host: 'b5gee6dacf.execute-api.us-east-1.amazonaws.com', - 'User-Agent': 'curl/7.54.0', - Via: '2.0 4ee511e558a0400aa4b9c1d34d92af5a.cloudfront.net (CloudFront)', - 'X-Amz-Cf-Id': 'xn-ohXlUAed-32bae2cfb7164fd690ffffb87d36b032==', - 'X-Amzn-Trace-Id': 'Root=1-4b5398e2-a7fbe4f92f2e911013cba76b', - 'X-Forwarded-For': '8.8.8.8, 2.3.4.5', - 'X-Forwarded-Port': '443', - 'X-Forwarded-Proto': 'https', - Referer: 'https://en.wikipedia.org/wiki/HTTP_referer', - Cookie: 'uid=abc; ga=1234; foo=bar; baz=foo%5Ba%5D; obj=j%3A%7B%22abc%22%3A123%7D; onechar=j; bad=j%3A%7Ba%7D', - }, - multiValueHeaders: { - Accept: [ '*/*' ], - Foo: [ 'bar', 'baz' ], - 'CloudFront-Forwarded-Proto': [ 'https' ], - 'CloudFront-Is-Desktop-Viewer': [ 'true' ], - 'CloudFront-Is-Mobile-Viewer': [ 'false' ], - 'CloudFront-Is-SmartTV-Viewer': [ 'false' ], - 'CloudFront-Is-Tablet-Viewer': [ 'false' ], - 'CloudFront-Viewer-Country': [ 'US' ], - Host: [ 'b5gee6dacf.execute-api.us-east-1.amazonaws.com' ], - 'User-Agent': [ 'curl/7.54.0' ], - Via: [ '2.0 4ee511e558a0400aa4b9c1d34d92af5a.cloudfront.net (CloudFront)' ], - 'X-Amz-Cf-Id': [ 'xn-ohXlUAed-32bae2cfb7164fd690ffffb87d36b032==' ], - 'X-Amzn-Trace-Id': [ 'Root=1-4b5398e2-a7fbe4f92f2e911013cba76b' ], - 'X-Forwarded-For': [ '8.8.8.8, 2.3.4.5' ], - 'X-Forwarded-Port': [ '443' ], - 'X-Forwarded-Proto': [ 'https' ], - Referer: [ 'https://en.wikipedia.org/wiki/HTTP_referer' ], - Cookie: [ 'uid=abc; ga=1234; foo=bar; baz=foo%5Ba%5D; obj=j%3A%7B%22abc%22%3A123%7D; onechar=j; bad=j%3A%7Ba%7D' ], - }, - queryStringParameters: { - 'foo[a]': 'bar b', - x: '2', - y: 'z', - }, - multiValueQueryStringParameters: { - 'foo[a]': [ 'bar b', 'baz c' ], - x: [ '1', '2' ], - y: [ 'z' ], - }, - }; -}; - export const albRequestContext = (): ApplicationLoadBalancerEventRequestContext => { return { elb: { @@ -215,3 +157,112 @@ export const albMultiValHeadersRequest = (): ApplicationLoadBalancerRequestEvent }, }); }; + +export function makeAPIGatewayRequestContextIdentity(): APIGatewayEventIdentity { + return { + accessKey: null, + accountId: null, + apiKey: null, + apiKeyId: null, + caller: null, + clientCert: null, + cognitoAuthenticationProvider: null, + cognitoAuthenticationType: null, + cognitoIdentityId: null, + cognitoIdentityPoolId: null, + principalOrgId: null, + sourceIp: '12.12.12.12', + user: null, + userAgent: 'curl/7.54.0', + userArn: null, + }; +} + +export function makeAPIGatewayRequestContext(params?: MakeAPIGatewayRequestEventContextParams): APIGatewayEventRequestContext { + return { + accountId: '123456789012', + apiId: 'someapi', + authorizer: null, + httpMethod: withDefault(params?.httpMethod, 'GET'), + path: '/prd', + protocol: 'HTTP/1.1', + stage: 'prd', + requestId: 'a507736b-259e-11e9-8fcf-4f1f08c4591e', + requestTimeEpoch: 1548969891530, + resourceId: 'reas23acc', + identity: makeAPIGatewayRequestContextIdentity(), + resourcePath: '/', + }; +} + +export function makeAPIGatewayRequestEventHeaders(params?: MakeAPIGatewayRequestEventHeadersParams): APIGatewayRequestEvent['headers'] { + return { + Accept: '*/*', + 'CloudFront-Forwarded-Proto': 'https', + 'CloudFront-Is-Desktop-Viewer': 'true', + 'CloudFront-Is-Mobile-Viewer': 'false', + 'CloudFront-Is-SmartTV-Viewer': 'false', + 'CloudFront-Is-Tablet-Viewer': 'false', + 'CloudFront-Viewer-Country': 'US', + Host: 'b5gee6dacf.execute-api.us-east-1.amazonaws.com', + 'User-Agent': 'curl/7.54.0', + Via: '2.0 4ee511e558a0400aa4b9c1d34d92af5a.cloudfront.net (CloudFront)', + 'X-Amz-Cf-Id': 'xn-ohXlUAed-32bae2cfb7164fd690ffffb87d36b032==', + 'X-Amzn-Trace-Id': 'Root=1-4b5398e2-a7fbe4f92f2e911013cba76b', + 'X-Forwarded-For': '8.8.8.8, 2.3.4.5', + 'X-Forwarded-Port': '443', + 'X-Forwarded-Proto': 'https', + Referer: 'https://en.wikipedia.org/wiki/HTTP_referer', + Cookie: 'uid=abc; ga=1234; foo=bar; baz=foo%5Ba%5D; obj=j%3A%7B%22abc%22%3A123%7D; onechar=j; bad=j%3A%7Ba%7D', + ...params, + }; +} + +// The headers parameter is what is normally used for generating `multiValueHeaders`, but +// it is possible to provide a `multiValueHeaders` parameter. If multiValueHeaders +// parameter is provided, its data will override intersecting attributes. +export function makeAPIGatewayRequestEventMultiValueHeader(params?: MakeAPIGatewayRequestEventMultiValueHeaderParamsInput): APIGatewayRequestEvent['multiValueHeaders'] { + return { + Foo: [ 'bar', 'baz' ], + ...Object.fromEntries( + Object.entries(makeAPIGatewayRequestEventHeaders(params?.headers)) + .map(([ key, value ]): Array> => { + return [ key, [ value ] ]; + }) + ), + ...params?.multiValueHeaders, + }; +} + +export function makeAPIGatewayRequestEvent(params?: MakeAPIGatewayRequestEventParams): APIGatewayRequestEvent { + const body = withDefault(params?.body, null), + httpMethod = withDefault(params?.httpMethod, 'GET'), + path = withDefault(params?.path, '/echo/asdf/a'), + headers = makeAPIGatewayRequestEventHeaders(params?.headers), + multiValueHeaders = makeAPIGatewayRequestEventMultiValueHeader(params); + + return { + body, + httpMethod, + path, + headers, + multiValueHeaders, + pathParameters: { proxy: path }, + requestContext: makeAPIGatewayRequestContext({ + httpMethod, + }), + isBase64Encoded: false, + resource: '/{proxy+}', + stageVariables: null, + queryStringParameters: { + 'foo[a]': 'bar b', + x: '2', + y: 'z', + }, + multiValueQueryStringParameters: { + 'foo[a]': [ 'bar b', 'baz c' ], + x: [ '1', '2' ], + y: [ 'z' ], + }, + }; +} diff --git a/tests/test-utils/withDefault.ts b/tests/test-utils/withDefault.ts new file mode 100644 index 0000000..d9c83b9 --- /dev/null +++ b/tests/test-utils/withDefault.ts @@ -0,0 +1,9 @@ +import { isUndefined } from '@silvermine/toolbox'; + +export default function withDefault(value: T | undefined, defaultValue: T): T { + if (isUndefined(value)) { + return defaultValue; + } + + return value; +} diff --git a/tests/utils/wrapRequestProcessor.test.ts b/tests/utils/wrapRequestProcessor.test.ts index 34cdbb7..d031900 100644 --- a/tests/utils/wrapRequestProcessor.test.ts +++ b/tests/utils/wrapRequestProcessor.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { Request, Response, Application } from '../../src'; import { NextCallback } from '../../src/interfaces'; import { wrapRequestProcessor } from '../../src/utils/wrapRequestProcessor'; -import { apiGatewayRequest, handlerContext } from '../samples'; +import { handlerContext, makeAPIGatewayRequestEvent } from '../samples'; import { spy, assert } from 'sinon'; /* eslint-disable @typescript-eslint/no-unused-vars */ @@ -13,7 +13,7 @@ describe('wrapRequestProcessor', () => { beforeEach(() => { app = new Application(); - req = new Request(app, apiGatewayRequest(), handlerContext()); + req = new Request(app, makeAPIGatewayRequestEvent(), handlerContext()); resp = new Response(app, req, cb); });