Skip to content

Commit 4c94f61

Browse files
committed
finish api wrapper unit tests
1 parent 19a84a2 commit 4c94f61

File tree

7 files changed

+202
-129
lines changed

7 files changed

+202
-129
lines changed

src/api/api-parser.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { stringify } from 'querystring';
2+
import createEvent from '@serverless/event-mocks';
3+
import { Request, Body } from './api-parser';
4+
5+
describe('Body parsing', () => {
6+
it('Parses json body', () => {
7+
const json = { hello: 'world' };
8+
const headers = { 'content-type': 'application/json' };
9+
const body = new Body(JSON.stringify(json), headers).getParsedBody();
10+
expect(body).toEqual(json);
11+
});
12+
13+
it('Parses form url encoded body', () => {
14+
const form = { hello: 'world' };
15+
const headers = { 'content-type': 'application/x-www-form-urlencoded' };
16+
const body = new Body(stringify(form), headers).getParsedBody();
17+
expect(body).toEqual(form);
18+
});
19+
20+
it("Passes body through when content type isn't specified", () => {
21+
const json = { hello: 'world' };
22+
const headers = {};
23+
const body = new Body(JSON.stringify(json), headers).getParsedBody();
24+
expect(body).toEqual(JSON.stringify(json));
25+
});
26+
27+
it("Errors when encoding and content-type don't match", () => {
28+
const invalid = '2["test" : 123]4}{';
29+
const headers = { 'content-type': 'application/json' };
30+
const body = new Body(invalid, headers).getParsedBody();
31+
expect(body).toEqual(invalid);
32+
});
33+
34+
it('Returns empty body when none given', () => {
35+
let empty;
36+
const headers = { 'content-type': 'application/x-www-form-urlencoded' };
37+
const body = new Body(empty, headers).getParsedBody();
38+
expect(body).toEqual(empty);
39+
});
40+
});
41+
42+
describe('Request parsing', () => {
43+
it('Gets all fields with optional parameters', () => {
44+
// @ts-ignore
45+
const event = createEvent('aws:apiGateway', {
46+
body: JSON.stringify({ hello: 'world' }),
47+
pathParameters: { proxy: 'not today' },
48+
queryStringParameters: { name: 'a test' },
49+
headers: { 'content-type': 'application/json', 'Test-Request': 'true' }
50+
});
51+
const { body, path, query, auth, headers, testRequest } = new Request(event).getProperties();
52+
53+
expect(body).toEqual({ hello: 'world' });
54+
expect(path).toEqual({ proxy: 'not today' });
55+
expect(query).toEqual({ name: 'a test' });
56+
expect(headers['content-type']).toEqual('application/json');
57+
expect(testRequest).toEqual(true);
58+
expect(auth).toBeFalsy();
59+
});
60+
61+
it("Get's falsy fields when optional parameters not used", () => {
62+
// @ts-ignore
63+
const event = createEvent('aws:apiGateway', {});
64+
delete event.body;
65+
delete event.headers;
66+
delete event.pathParameters;
67+
delete event.queryStringParameters;
68+
const { body, path, query, auth, headers, testRequest } = new Request(event).getProperties();
69+
70+
expect(body).toBeFalsy();
71+
expect(path).toBeFalsy();
72+
expect(query).toBeFalsy();
73+
expect(auth).toBeFalsy();
74+
expect(headers).toBeFalsy();
75+
expect(testRequest).toBeFalsy();
76+
});
77+
});

src/api/body.ts renamed to src/api/api-parser.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
1+
import { APIGatewayEvent } from 'aws-lambda';
12
import { parse } from 'querystring';
2-
import { logger } from '../common/log';
3+
import { Metrics, logger } from '../common';
4+
5+
const metrics = new Metrics('API Gateway');
6+
7+
export class Request {
8+
constructor(private event: APIGatewayEvent) {}
9+
10+
getProperties(): any {
11+
const event = this.event;
12+
const path = event.pathParameters ? event.pathParameters : null;
13+
const query = event.queryStringParameters ? event.queryStringParameters : null;
14+
const auth = event.requestContext && event.requestContext.authorizer ? event.requestContext.authorizer : null;
15+
const headers = event.headers ? event.headers : null;
16+
const body = new Body(event.body, headers).getParsedBody();
17+
const TEST_REQUEST_HEADER = process.env.TEST_REQUEST_HEADER || 'Test-Request';
18+
const testRequest = headers && headers[TEST_REQUEST_HEADER] ? JSON.parse(headers[TEST_REQUEST_HEADER]) : false;
19+
metrics.common({ body, path, query, auth, headers, testRequest });
20+
return { body, path, query, auth, headers, testRequest };
21+
}
22+
}
323

424
export class Body {
525
constructor(private body: any, private headers: { [name: string]: string }) {}

src/api/api-responses.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import createEvent from '@serverless/event-mocks';
2+
import { success, error, invalid, redirect } from './api-responses';
3+
4+
describe('API responses', () => {
5+
// @ts-ignore
6+
const event = createEvent('aws:apiGateway', {});
7+
8+
it('Handles success response', () => {
9+
expect(success('success')).toEqual({
10+
body: JSON.stringify('success'),
11+
headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': true },
12+
statusCode: 200
13+
});
14+
});
15+
16+
it('Handles error response', () => {
17+
expect(() => error('error')).toThrow('error');
18+
});
19+
20+
it('Handles invalid response', () => {
21+
expect(invalid(['invalid'])).toEqual({
22+
body: JSON.stringify({ errors: ['invalid'] }),
23+
headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': true },
24+
statusCode: 400
25+
});
26+
});
27+
28+
it('Handles redirect response', () => {
29+
expect(redirect('url')).toEqual({
30+
headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': true, Location: 'url' },
31+
statusCode: 302
32+
});
33+
});
34+
});

src/api/api-responses.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Metrics } from '../common';
2+
3+
const metrics = new Metrics('API Gateway');
4+
5+
const HEADERS = {
6+
'Access-Control-Allow-Origin': '*',
7+
'Access-Control-Allow-Credentials': true
8+
};
9+
10+
export function success(payload: any = null) {
11+
const response = { statusCode: 200, headers: HEADERS };
12+
if (payload) {
13+
response['body'] = JSON.stringify(payload);
14+
}
15+
metrics.success(payload);
16+
return response;
17+
}
18+
19+
export function invalid(errors: string[] = []) {
20+
const response = { statusCode: 400, headers: HEADERS, body: JSON.stringify({ errors }) };
21+
metrics.invalid(response);
22+
return response;
23+
}
24+
25+
export function redirect(url: string) {
26+
HEADERS['Location'] = url;
27+
const response = { statusCode: 302, headers: HEADERS };
28+
metrics.redirect(response);
29+
return response;
30+
}
31+
32+
export function error(error: any = '') {
33+
metrics.error(error);
34+
throw new Error(error);
35+
}

src/api/api-wrapper.test.ts

Lines changed: 31 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,39 @@ import { apiWrapper, ApiSignature } from './api-wrapper';
33

44
describe('API wrapper', () => {
55
// @ts-ignore
6-
const event = createEvent('aws:apiGateway', {});
7-
8-
it('Handles success callback', () => {
9-
function mockHandler({ success }: ApiSignature) {
10-
return success('success');
11-
}
12-
expect(apiWrapper(mockHandler)(event)).toEqual({
13-
body: JSON.stringify('success'),
14-
headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': true },
15-
statusCode: 200
16-
});
17-
});
18-
19-
it('Handles error callback', () => {
20-
function mockHandler({ error }: ApiSignature) {
21-
return error('error');
22-
}
23-
expect(() => {
24-
apiWrapper(mockHandler)(event);
25-
}).toThrow('error');
26-
});
27-
28-
it('Handles invalid callback', () => {
29-
function mockHandler({ invalid }: ApiSignature) {
30-
return invalid(['invalid']);
31-
}
32-
expect(apiWrapper(mockHandler)(event)).toEqual({
33-
body: JSON.stringify({ errors: ['invalid'], event }),
34-
headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': true },
35-
statusCode: 400
36-
});
6+
const requestEvent = createEvent('aws:apiGateway', {
7+
body: JSON.stringify({ hello: 'world' }),
8+
pathParameters: { proxy: 'not today' },
9+
queryStringParameters: { name: 'a test' },
10+
headers: { 'content-type': 'application/json', 'Test-Request': 'true' }
3711
});
3812

39-
it('Handles redirect callback', () => {
40-
function mockHandler({ redirect }: ApiSignature) {
41-
return redirect('url');
13+
it('Has expected properties and response functions', () => {
14+
function mockHandler({
15+
event,
16+
body,
17+
path,
18+
query,
19+
headers,
20+
testRequest,
21+
auth,
22+
success,
23+
invalid,
24+
redirect,
25+
error
26+
}: ApiSignature) {
27+
expect(event).toEqual(requestEvent);
28+
expect(body).toEqual({ hello: 'world' });
29+
expect(path).toEqual({ proxy: 'not today' });
30+
expect(query).toEqual({ name: 'a test' });
31+
expect(headers['content-type']).toEqual('application/json');
32+
expect(testRequest).toEqual(true);
33+
expect(auth).toBeFalsy();
34+
expect(success).toBeInstanceOf(Function);
35+
expect(invalid).toBeInstanceOf(Function);
36+
expect(redirect).toBeInstanceOf(Function);
37+
expect(error).toBeInstanceOf(Function);
4238
}
43-
expect(apiWrapper(mockHandler)(event)).toEqual({
44-
headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': true, Location: 'url' },
45-
statusCode: 302
46-
});
39+
apiWrapper(mockHandler)(requestEvent);
4740
});
4841
});

src/api/api-wrapper.ts

Lines changed: 4 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,10 @@
1-
import { APIGatewayEvent, Context, Callback } from 'aws-lambda';
2-
import { Metrics } from '../common';
3-
import { Body } from './body';
4-
5-
const metrics = new Metrics('API Gateway');
6-
7-
const HEADERS = {
8-
'Access-Control-Allow-Origin': '*',
9-
'Access-Control-Allow-Credentials': true
10-
};
1+
import { APIGatewayEvent } from 'aws-lambda';
2+
import { Request } from './api-parser';
3+
import { success, invalid, redirect, error } from './api-responses';
114

125
export function apiWrapper<T extends Function>(fn: T): T {
136
return <any>function(event: APIGatewayEvent) {
14-
const { body, path, query, request, auth, headers, testRequest } = getRequestFields(event);
15-
metrics.common(request);
16-
17-
function success(payload: any = null) {
18-
const response = { statusCode: 200, headers: HEADERS };
19-
if (payload) {
20-
response['body'] = JSON.stringify(payload);
21-
}
22-
metrics.success(payload);
23-
return response;
24-
}
25-
26-
function invalid(errors: string[] = []) {
27-
const response = { statusCode: 400, headers: HEADERS, body: JSON.stringify({ errors, event }) };
28-
metrics.invalid(response);
29-
return response;
30-
}
31-
32-
function redirect(url: string) {
33-
HEADERS['Location'] = url;
34-
const response = { statusCode: 302, headers: HEADERS };
35-
metrics.redirect(response);
36-
return response;
37-
}
38-
39-
function error(error: any = '') {
40-
metrics.error(error);
41-
throw new Error(error);
42-
}
7+
const { body, path, query, auth, headers, testRequest } = new Request(event).getProperties();
438

449
const signature: ApiSignature = {
4510
event,
@@ -58,18 +23,6 @@ export function apiWrapper<T extends Function>(fn: T): T {
5823
};
5924
}
6025

61-
function getRequestFields(event: APIGatewayEvent): any {
62-
const path = event.pathParameters ? event.pathParameters : null;
63-
const query = event.queryStringParameters ? event.queryStringParameters : null;
64-
const auth = event.requestContext && event.requestContext.authorizer ? event.requestContext.authorizer : null;
65-
const headers = event.headers ? event.headers : null;
66-
const body = new Body(event.body, headers).getParsedBody();
67-
const TEST_REQUEST_HEADER = process.env.TEST_REQUEST_HEADER || 'Test-Request';
68-
const testRequest = headers && headers[TEST_REQUEST_HEADER] ? JSON.parse(headers[TEST_REQUEST_HEADER]) : false;
69-
const request = { body, path, query, auth, headers, testRequest };
70-
return { body, path, query, auth, request, headers, testRequest };
71-
}
72-
7326
export interface ApiSignature {
7427
event: APIGatewayEvent; // original event
7528
body: any; // JSON parsed body payload if exists (otherwise null)

src/api/body.test.ts

Lines changed: 0 additions & 39 deletions
This file was deleted.

0 commit comments

Comments
 (0)