-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: improve fake api gateway request events creation #91
base: master
Are you sure you want to change the base?
feat: improve fake api gateway request events creation #91
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MatheusBaldi Thanks for submitting this! On first pass, a few observations:
- This is a fairly large MR that is fundamentally changing how the test events are generated in this project. Instead of changing everything, would it be possible to just change the API Gateway test event in this initial PR and then submit a follow up PR for the ALB events? This way the initial pattern can be set while keeping the set of changes small.
- By convention we lean towards the OR operator (
||
) over the null coalescing operator (??
). Eventually we want to reconsider that stance, but for now can we keep with||
to maintain consistency. - This PR is adding a bunch of types which basically mirror the AWS provided types. The goal of these "generator" functions is to make developer's lives better. Rather than taking a 1-to-1 input from the event, can we abstract the internal workings of the events some? For example:
// in tests/samples.ts...
interface MakeAPIGatewayRequestParams {
// Only defining the overrides that are needed
httpMethod?: string;
headers?: StringMap;
}
function makeAPIGatewayRequest(params: MakeAPIGatewayRequestParams): APIGatewayRequestEvent {
return {
// ... omitted for brevity ...
httpMethod: params.httpMethod || 'GET',
requestContext: makeAPIGatewayRequestContext({
httpMethod: params.httpMethod,
}),
headers: {
// ...
...(params.headers || {})
},
multiValueHeaders: {
// ...
...Object.fromEntries(Object.entries(params.headers || {}).map(([ key, value ]), () => {
return [ key, [ value ] ];
}))
},
// ...
};
}
interface MakeAPIGatewayRequestContextParams {
httpMethod?: string;
}
function makeAPIGatewayRequestContext(params: MakeAPIGatewayRequestContextParams): APIGatewayEventRequestContext {
return {
// ...
httpMethod: params.httpMethod || 'GET',
// ...
};
};
@onebytegone Thank you for your observations.
function makeAPIGatewayRequest(params?: MakeAPIGatewayRequestParams): APIGatewayRequestEvent {
const defaultHttpMethod = params.httpMethod || 'GET';
const defaultHeader = makeAPIGatewayRequestHeader(params.headers);
return {
// ... omitted for brevity ...
httpMethod: defaultHttpMethod,
// if requestContext not specified, define a default as we want, otherwise, use the input
requestContext: params?.requestContext === undefined
? makeAPIGatewayRequestContext({ // this function will know what other fields to populate by default
httpMethod: defaultHttpMethod,
})
: params.requestContext,
headers: params?.headers === undefined
? defaultHeader
: params.headers,
multiValueHeaders: params?.multiValueHeaders === undefined
? makeAPIGatewayRequestMultivalueHeader(defaultHeader) //
: params.multiValueHeaders,
// ...
};
} Also, we could have other helper functions that builds parts of the object before passing them to the generator, in that way, the responsibility of being specific goes to where the generator function is called. Now two more questions
|
1 Feel free to update this PR interface MakeAPIGatewayRequestEventParams {
httpMethod?: string | null;
}
function makeAPIGatewayRequestEvent(params?: MakeAPIGatewayRequestEventParams): APIGatewayRequestEvent {
const httpMethod = withDefault(params?.httpMethod, 'GET');
return {
// ... omitted for brevity ...
httpMethod,
requestContext: makeAPIGatewayRequestContext({
httpMethod,
}),
};
}
function withDefault<T>(value: T, defaultValue: T): T | undefined {
if (isUndefined(value)) {
return defaultValue;
}
if (value === null) {
return undefined;
}
return value;
}
|
23bd78e
to
06ef2c0
Compare
tests/samples.ts
Outdated
export const makeAPIGatewayRequestEvent = (params?: MakeAPIGatewayRequestEventParams): APIGatewayRequestEvent => { | ||
const httpMethod = withDefault(params?.httpMethod, 'GET'), | ||
path = withDefault(params?.path, '/echo/asdf/a'), | ||
headers = withDefault(params?.headers, makeAPIGatewayRequestEventHeaders()); | ||
|
||
const multiValueHeaders = withDefault( | ||
params?.multiValueHeaders, | ||
makeAPIGatewayRequestEventMultiValueHeader() | ||
); | ||
|
||
return { | ||
path, | ||
httpMethod, | ||
body: null, | ||
isBase64Encoded: false, | ||
resource: '/{proxy+}', | ||
pathParameters: { proxy: path }, | ||
stageVariables: null, | ||
requestContext: makeAPIGatewayRequestContext({ | ||
httpMethod, | ||
}), | ||
headers, | ||
multiValueHeaders, | ||
queryStringParameters: { | ||
'foo[a]': 'bar b', | ||
x: '2', | ||
y: 'z', | ||
}, | ||
multiValueQueryStringParameters: { | ||
'foo[a]': [ 'bar b', 'baz c' ], | ||
x: [ '1', '2' ], | ||
y: [ 'z' ], | ||
}, | ||
}; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've also thought of another approach for handling default values by using the object behavior of making keys that comes at last overwrite the preceding ones:
// say params is:
const params = {
path: '/another/path/a',
httpMethod: 'POST',
};
export const makeAPIGatewayRequestEvent = (params?: MakeAPIGatewayRequestEventParams): APIGatewayRequestEvent => {
// still treating these values because in the default case they must be reflected in other
// parts of the object
const httpMethod = withDefault(params?.httpMethod, 'GET'),
path = withDefault(params?.path, '/echo/asdf/a');
return {
path,
httpMethod,
body: null,
isBase64Encoded: false,
resource: '/{proxy+}',
pathParameters: { proxy: path },
stageVariables: null,
requestContext: makeAPIGatewayRequestContext({
httpMethod,
}),
headers: makeAPIGatewayRequestEventHeaders(),
multiValueHeaders: makeAPIGatewayRequestEventMultiValueHeader(),
queryStringParameters: {
'foo[a]': 'bar b',
x: '2',
y: 'z',
},
multiValueQueryStringParameters: {
'foo[a]': [ 'bar b', 'baz c' ],
x: [ '1', '2' ],
y: [ 'z' ],
},
...params,
};
};
It seems to have a similar behavior and it is easier for developers to add more of the dynamic fields in the future, since they only need to update the interface if no further treatment is needed like in the cases of path
and httpMethod
. What do you think?
@onebytegone the amount of changed files increased because the refactorings demanded some other files that used the old |
tests/samples.ts
Outdated
} | ||
interface MakeAPIGatewayRequestEventHeadersParams { | ||
[name: string]: string | undefined; | ||
} | ||
|
||
interface MakeAPIGatewayRequestEventMultiValueHeadersParams { | ||
[name: string]: string[] | undefined; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | |
interface MakeAPIGatewayRequestEventHeadersParams { | |
[name: string]: string | undefined; | |
} | |
interface MakeAPIGatewayRequestEventMultiValueHeadersParams { | |
[name: string]: string[] | undefined; | |
} | |
} | |
interface MakeAPIGatewayRequestEventHeadersParams { | |
[name: string]: string | undefined; | |
} | |
interface MakeAPIGatewayRequestEventMultiValueHeadersParams { | |
[name: string]: string[] | undefined; | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be a good idea to add it as a lint rule? This behavior exists for const, let and var in the padding-line-between-statements rule
tests/samples.ts
Outdated
accountId: '123456789012', | ||
apiId: 'someapi', | ||
authorizer: null, | ||
httpMethod: params?.httpMethod ?? 'GET', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See prior note about the null coalescing operator.
httpMethod: params?.httpMethod ?? 'GET', | |
httpMethod: params?.httpMethod || 'GET', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Corrected with the withDefault function
tests/samples.ts
Outdated
export const makeAPIGatewayRequestEventHeaders = | ||
(params?: MakeAPIGatewayRequestEventHeadersParams): APIGatewayRequestEvent['headers'] => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For functions, we prefer the function
declaration style. The other functions in this file were from before we had a set standard.
export const makeAPIGatewayRequestEventHeaders = | |
(params?: MakeAPIGatewayRequestEventHeadersParams): APIGatewayRequestEvent['headers'] => { | |
export function makeAPIGatewayRequestEventHeaders(params?: MakeAPIGatewayRequestEventHeadersParams): APIGatewayRequestEvent['headers'] { |
tests/samples.ts
Outdated
return withDefault<APIGatewayRequestEvent['headers']>( | ||
params, | ||
{ | ||
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', | ||
} | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the developer only wants to edit a single header?
return withDefault<APIGatewayRequestEvent['headers']>( | |
params, | |
{ | |
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', | |
} | |
); | |
return compact({ | |
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, | |
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@onebytegone I like this approach. I've also left a question about applying it for the base object, what do you think?
I didn't understand the compact
function here. Why would we define falsy values if they're going to be removed? Wouldn't these values be expected to be returned, even as falsy? Is it the same compact from the silvermine/toolbox? It seems to me that it is meant for arrays.
After applying the suggested changes, the easiest way for the developer to completely override those values, if needed, will be to use the spread operator or the _.extends function in the level of the parent object.
tests/samples.ts
Outdated
headers = withDefault(params?.headers, makeAPIGatewayRequestEventHeaders()); | ||
|
||
const multiValueHeaders = withDefault( | ||
params?.multiValueHeaders, | ||
makeAPIGatewayRequestEventMultiValueHeader() | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this approach we're putting the burden of ensuring headers
and multiValueHeaders
are in sync on the developer writing the tests. Do we have tests which need to override only multiValueHeaders
? If so, what if multiValueHeaders
is normally generated dynamically from headers
but can have overrides provided via multiValueHeaders
. If we go that route MakeAPIGatewayRequestEventParams
will need a comment clearly explaining that headers
should normally be used but multiValueHeaders
can be used to selectively override multi-value header values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@onebytegone If we implement it to generate multiValueHeaders
from headers
, how things should work when the developer only need to think about multiValueHeaders
? If he tries changing it through the maker function params, using the multiValueHeaders
, which is what interests him, he'll need to check implementation details of the function. Do you consider it a problem?
tests/samples.ts
Outdated
import withDefault from './test-utils/withDefault'; | ||
|
||
interface MakeAPIGatewayRequestEventContextParams { | ||
httpMethod?: string | null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't look like the aws-lambda types allow null for httpMethod
.
httpMethod?: string | null; | |
httpMethod?: string; |
tests/Response.test.ts
Outdated
evt = _.extend(makeAPIGatewayRequestEvent(), { | ||
multiValueHeaders: { 'Referer': [ referer ] }, | ||
headers: { 'Referer': referer }, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the purpose of makeAPIGatewayRequestEvent
is to make an event, allowing specific values to be overridden, should we still be using _.extend
here? There are a number of places this is happening in the in PR, only commenting in this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't, thanks.
6e4ad20
to
637362a
Compare
637362a
to
f1507e2
Compare
tests/integration-tests.test.ts
Outdated
const makeRequestEvent = (path: string, base?: RequestEvent, method?: string): RequestEvent => { | ||
return _.extend(base || apiGatewayRequest(), { path: path, httpMethod: (method || 'GET') }); | ||
return _.extend(base, { path: path, httpMethod: (method || 'GET') }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if makeRequestEvent
doesn't have a base
provided? If we should always provide base
, can we remove the optional flag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense, thanks. This is going to be removed when the alb makers are introduced in a follow up PR, the places where it is used now are providing a base object, but, if not provided, that wouldn't work properly.
tests/Request.test.ts
Outdated
evt.headers['X-Forwarded-Proto'] = 'http'; | ||
evt.multiValueHeaders['X-Forwarded-Proto'] = [ 'http' ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the X-Forwarded-Proto
header be passed into makeAPIGatewayRequestEvent
instead of modifying the event after creation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This evt
is actually not even used here. I'll remove it.
tests/Request.test.ts
Outdated
@@ -250,7 +250,7 @@ describe('Request', () => { | |||
|
|||
it('parses proper values - APIGW', () => { | |||
_.each(testCases, (testCase) => { | |||
let evt: RequestEvent = apiGatewayRequest(), | |||
let evt: RequestEvent = makeAPIGatewayRequestEvent(), | |||
req; | |||
|
|||
evt.headers.Host = testCase.host; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these headers be passed into makeAPIGatewayRequestEvent
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! @onebytegone I tried to find some similar cases and found one in the test 'does not throw an error when bad values supplied'
, around line 539. It is changing queryStringParameters
and multiValueQueryStringParameters
. Do you think the maker function should allow these attributes to be changed as well?
tests/samples.ts
Outdated
headers?: MakeAPIGatewayRequestEventHeadersParams; | ||
multiValueHeaders?: MakeAPIGatewayRequestEventMultiValueHeaderParams; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Able to add comments explaining why using headers
is preferred, but multiValueHeaders
can be used to set multiple values for specific headers?
f1507e2
to
623ffe8
Compare
receives a value in its first parameter `value` and a default value in its second parameter `defaultValue`. If `value` is undefined, returns `defaultValue`, otherwise returns `value`
- makeAPIGatewayRequestEvent accepts an input object, allowing the developer to change values of desired fields when needed, while apiGatewayRequest would always return the same object
- makeAPIGatewayRequestEvent accepts an input object, allowing the developer to change values of desired fields when needed, while apiGatewayRequest would always return the same object
- makeAPIGatewayRequestEvent accepts an input object, allowing the developer to change values of desired fields when needed, while apiGatewayRequest would always return the same object
623ffe8
to
b9fecef
Compare
No description provided.