Skip to content

Commit 294ef04

Browse files
committed
feat(invoke-local): handle http event
1 parent 5dd3876 commit 294ef04

File tree

6 files changed

+271
-92
lines changed

6 files changed

+271
-92
lines changed

invokeLocal/googleInvokeLocal.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class GoogleInvokeLocal {
2929

3030
async invokeLocal() {
3131
const functionObj = this.serverless.service.getFunction(this.options.function);
32-
this.validateEventsProperty(functionObj, this.options.function, ['event']); // Only event is currently supported
32+
this.validateEventsProperty(functionObj, this.options.function);
3333

3434
const runtime = this.provider.getRuntime(functionObj);
3535
if (!runtime.startsWith('nodejs')) {

invokeLocal/googleInvokeLocal.test.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,7 @@ describe('GoogleInvokeLocal', () => {
165165

166166
it('should validate the function configuration', async () => {
167167
await googleInvokeLocal.invokeLocal();
168-
expect(
169-
validateEventsPropertyStub.calledOnceWith(functionObj, functionName, ['event'])
170-
).toEqual(true);
168+
expect(validateEventsPropertyStub.calledOnceWith(functionObj, functionName)).toEqual(true);
171169
});
172170

173171
it('should get the runtime', async () => {

invokeLocal/lib/httpReqRes.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
const express = require('express');
4+
const http = require('http');
5+
const net = require('net');
6+
7+
// The following lines create an express request and an express response
8+
// as they are created in an express server before being passed to the middlewares
9+
// Google use express 4.17.1 to run http cloud function
10+
// https://cloud.google.com/functions/docs/writing/http#http_frameworks
11+
const app = express();
12+
13+
const req = new http.IncomingMessage(new net.Socket());
14+
const expressRequest = Object.assign(req, { app });
15+
Object.setPrototypeOf(expressRequest, express.request);
16+
17+
const res = new http.ServerResponse(req);
18+
const _expressResponse = Object.assign(res, { app, req: expressRequest });
19+
Object.setPrototypeOf(_expressResponse, express.response);
20+
21+
expressRequest.res = _expressResponse;
22+
23+
module.exports = {
24+
expressRequest,
25+
expressResponse(endCallback) {
26+
return Object.assign(_expressResponse, { end: endCallback }); // Override of the end function which is always called to send the response of the http request
27+
},
28+
};

invokeLocal/lib/nodeJs.js

Lines changed: 66 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const chalk = require('chalk');
44
const path = require('path');
55
const _ = require('lodash');
6+
const { expressRequest, expressResponse } = require('./httpReqRes');
67

78
const tryToRequirePaths = (paths) => {
89
let loaded;
@@ -21,8 +22,6 @@ const tryToRequirePaths = (paths) => {
2122

2223
module.exports = {
2324
async invokeLocalNodeJs(functionObj, event, customContext) {
24-
let hasResponded = false;
25-
2625
// index.js and function.js are the two files supported by default by a cloud-function
2726
// TODO add the file pointed by the main key of the package.json
2827
const paths = ['index.js', 'function.js'].map((fileName) =>
@@ -41,27 +40,41 @@ module.exports = {
4140

4241
this.addEnvironmentVariablesToProcessEnv(functionObj);
4342

44-
function handleError(err) {
45-
let errorResult;
46-
if (err instanceof Error) {
47-
errorResult = {
48-
errorMessage: err.message,
49-
errorType: err.constructor.name,
50-
stackTrace: err.stack && err.stack.split('\n'),
51-
};
52-
} else {
53-
errorResult = {
54-
errorMessage: err,
55-
};
56-
}
43+
const eventType = Object.keys(functionObj.events[0])[0];
5744

58-
this.serverless.cli.consoleLog(chalk.red(JSON.stringify(errorResult, null, 4)));
59-
process.exitCode = 1;
45+
switch (eventType) {
46+
case 'event':
47+
return this.handleEvent(cloudFunction, event, customContext);
48+
case 'http':
49+
return this.handleHttp(cloudFunction, event, customContext);
50+
default:
51+
throw new Error(`${eventType} is not supported`);
52+
}
53+
},
54+
handleError(err, resolve) {
55+
let errorResult;
56+
if (err instanceof Error) {
57+
errorResult = {
58+
errorMessage: err.message,
59+
errorType: err.constructor.name,
60+
stackTrace: err.stack && err.stack.split('\n'),
61+
};
62+
} else {
63+
errorResult = {
64+
errorMessage: err,
65+
};
6066
}
6167

68+
this.serverless.cli.consoleLog(chalk.red(JSON.stringify(errorResult, null, 4)));
69+
resolve();
70+
process.exitCode = 1;
71+
},
72+
handleEvent(cloudFunction, event, customContext) {
73+
let hasResponded = false;
74+
6275
function handleResult(result) {
6376
if (result instanceof Error) {
64-
handleError.call(this, result);
77+
this.handleError.call(this, result);
6578
return;
6679
}
6780
this.serverless.cli.consoleLog(JSON.stringify(result, null, 4));
@@ -72,26 +85,54 @@ module.exports = {
7285
if (!hasResponded) {
7386
hasResponded = true;
7487
if (err) {
75-
handleError.call(this, err);
88+
this.handleError(err, resolve);
7689
} else if (result) {
7790
handleResult.call(this, result);
7891
}
92+
resolve();
7993
}
80-
resolve();
8194
};
8295

8396
let context = {};
8497

8598
if (customContext) {
8699
context = customContext;
87100
}
88-
89-
const maybeThennable = cloudFunction(event, context, callback);
90-
if (maybeThennable) {
91-
return Promise.resolve(maybeThennable).then(callback.bind(this, null), callback.bind(this));
101+
try {
102+
const maybeThennable = cloudFunction(event, context, callback);
103+
if (maybeThennable) {
104+
Promise.resolve(maybeThennable).then(callback.bind(this, null), callback.bind(this));
105+
}
106+
} catch (error) {
107+
this.handleError(error, resolve);
92108
}
109+
});
110+
},
111+
handleHttp(cloudFunction, event) {
112+
const request = Object.assign(expressRequest, event);
93113

94-
return maybeThennable;
114+
return new Promise((resolve) => {
115+
let response;
116+
const endCallback = (data) => {
117+
if (data && Buffer.isBuffer(data)) {
118+
data = data.toString();
119+
}
120+
this.serverless.cli.consoleLog(
121+
JSON.stringify({ status: response.statusCode, body: data }, null, 4)
122+
);
123+
resolve();
124+
};
125+
126+
response = expressResponse(endCallback);
127+
128+
try {
129+
const maybeThennable = cloudFunction(request, response);
130+
if (maybeThennable) {
131+
Promise.resolve(maybeThennable).catch((error) => this.handleError(error, resolve));
132+
}
133+
} catch (error) {
134+
this.handleError(error, resolve);
135+
}
95136
});
96137
},
97138

invokeLocal/lib/nodeJs.test.js

Lines changed: 136 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,6 @@ const Serverless = require('../../test/serverless');
77

88
jest.spyOn(console, 'log');
99
describe('invokeLocalNodeJs', () => {
10-
const eventName = 'eventName';
11-
const contextName = 'contextName';
12-
const event = {
13-
name: eventName,
14-
};
15-
const context = {
16-
name: contextName,
17-
};
1810
const myVarValue = 'MY_VAR_VALUE';
1911
let serverless;
2012
let googleInvokeLocal;
@@ -29,57 +21,147 @@ describe('invokeLocalNodeJs', () => {
2921
serverless.cli.consoleLog = jest.fn();
3022
googleInvokeLocal = new GoogleInvokeLocal(serverless, {});
3123
});
32-
33-
it('should invoke a sync handler', async () => {
34-
const functionConfig = {
35-
handler: 'syncHandler',
24+
describe('event', () => {
25+
const eventName = 'eventName';
26+
const contextName = 'contextName';
27+
const event = {
28+
name: eventName,
3629
};
37-
await googleInvokeLocal.invokeLocalNodeJs(functionConfig, event, context);
38-
// eslint-disable-next-line no-console
39-
expect(console.log).toHaveBeenCalledWith('SYNC_HANDLER');
40-
expect(serverless.cli.consoleLog).toHaveBeenCalledWith(`{\n "result": "${eventName}"\n}`);
41-
});
42-
43-
it('should handle errors in a sync handler', async () => {
44-
const functionConfig = {
45-
handler: 'syncHandlerWithError',
30+
const context = {
31+
name: contextName,
4632
};
47-
await googleInvokeLocal.invokeLocalNodeJs(functionConfig, event, context);
48-
// eslint-disable-next-line no-console
49-
expect(console.log).toHaveBeenCalledWith('SYNC_HANDLER');
50-
expect(serverless.cli.consoleLog).toHaveBeenCalledWith(
51-
expect.stringContaining('"errorMessage": "SYNC_ERROR"')
52-
);
53-
});
54-
55-
it('should invoke an async handler', async () => {
56-
const functionConfig = {
57-
handler: 'asyncHandler',
33+
const baseConfig = {
34+
events: [{ event: {} }],
5835
};
59-
await googleInvokeLocal.invokeLocalNodeJs(functionConfig, event, context);
60-
// eslint-disable-next-line no-console
61-
expect(console.log).toHaveBeenCalledWith('ASYNC_HANDLER');
62-
expect(serverless.cli.consoleLog).toHaveBeenCalledWith(`{\n "result": "${contextName}"\n}`);
63-
});
36+
it('should invoke a sync handler', async () => {
37+
const functionConfig = {
38+
...baseConfig,
39+
handler: 'eventSyncHandler',
40+
};
41+
await googleInvokeLocal.invokeLocalNodeJs(functionConfig, event, context);
42+
// eslint-disable-next-line no-console
43+
expect(console.log).toHaveBeenCalledWith('EVENT_SYNC_HANDLER');
44+
expect(serverless.cli.consoleLog).toHaveBeenCalledWith(`{\n "result": "${eventName}"\n}`);
45+
});
6446

65-
it('should handle errors in an async handler', async () => {
66-
const functionConfig = {
67-
handler: 'asyncHandlerWithError',
68-
};
69-
await googleInvokeLocal.invokeLocalNodeJs(functionConfig, event, context);
70-
// eslint-disable-next-line no-console
71-
expect(console.log).toHaveBeenCalledWith('ASYNC_HANDLER');
72-
expect(serverless.cli.consoleLog).toHaveBeenCalledWith(
73-
expect.stringContaining('"errorMessage": "ASYNC_ERROR"')
74-
);
75-
});
47+
it('should handle errors in a sync handler', async () => {
48+
const functionConfig = {
49+
...baseConfig,
50+
handler: 'eventSyncHandlerWithError',
51+
};
52+
await googleInvokeLocal.invokeLocalNodeJs(functionConfig, event, context);
53+
// eslint-disable-next-line no-console
54+
expect(console.log).toHaveBeenCalledWith('EVENT_SYNC_HANDLER');
55+
expect(serverless.cli.consoleLog).toHaveBeenCalledWith(
56+
expect.stringContaining('"errorMessage": "SYNC_ERROR"')
57+
);
58+
});
7659

77-
it('should give the environment variables to the handler', async () => {
78-
const functionConfig = {
79-
handler: 'envHandler',
60+
it('should invoke an async handler', async () => {
61+
const functionConfig = {
62+
...baseConfig,
63+
handler: 'eventAsyncHandler',
64+
};
65+
await googleInvokeLocal.invokeLocalNodeJs(functionConfig, event, context);
66+
// eslint-disable-next-line no-console
67+
expect(console.log).toHaveBeenCalledWith('EVENT_ASYNC_HANDLER');
68+
expect(serverless.cli.consoleLog).toHaveBeenCalledWith(
69+
`{\n "result": "${contextName}"\n}`
70+
);
71+
});
72+
73+
it('should handle errors in an async handler', async () => {
74+
const functionConfig = {
75+
...baseConfig,
76+
handler: 'eventAsyncHandlerWithError',
77+
};
78+
await googleInvokeLocal.invokeLocalNodeJs(functionConfig, event, context);
79+
// eslint-disable-next-line no-console
80+
expect(console.log).toHaveBeenCalledWith('EVENT_ASYNC_HANDLER');
81+
expect(serverless.cli.consoleLog).toHaveBeenCalledWith(
82+
expect.stringContaining('"errorMessage": "ASYNC_ERROR"')
83+
);
84+
});
85+
86+
it('should give the environment variables to the handler', async () => {
87+
const functionConfig = {
88+
...baseConfig,
89+
handler: 'eventEnvHandler',
90+
};
91+
await googleInvokeLocal.invokeLocalNodeJs(functionConfig, event, context);
92+
// eslint-disable-next-line no-console
93+
expect(console.log).toHaveBeenCalledWith(myVarValue);
94+
});
95+
});
96+
describe('http', () => {
97+
const message = 'httpBodyMessage';
98+
const req = {
99+
body: { message },
100+
};
101+
const context = {};
102+
const baseConfig = {
103+
events: [{ http: '' }],
80104
};
81-
await googleInvokeLocal.invokeLocalNodeJs(functionConfig, event, context);
82-
// eslint-disable-next-line no-console
83-
expect(console.log).toHaveBeenCalledWith(myVarValue);
105+
it('should invoke a sync handler', async () => {
106+
const functionConfig = {
107+
...baseConfig,
108+
handler: 'httpSyncHandler',
109+
};
110+
await googleInvokeLocal.invokeLocalNodeJs(functionConfig, req, context);
111+
// eslint-disable-next-line no-console
112+
expect(console.log).toHaveBeenCalledWith('HTTP_SYNC_HANDLER');
113+
expect(serverless.cli.consoleLog).toHaveBeenCalledWith(
114+
JSON.stringify({ status: 200, body: JSON.stringify({ responseMessage: message }) }, null, 4)
115+
);
116+
});
117+
118+
it('should handle errors in a sync handler', async () => {
119+
const functionConfig = {
120+
...baseConfig,
121+
handler: 'httpSyncHandlerWithError',
122+
};
123+
await googleInvokeLocal.invokeLocalNodeJs(functionConfig, req, context);
124+
// eslint-disable-next-line no-console
125+
expect(console.log).toHaveBeenCalledWith('HTTP_SYNC_HANDLER');
126+
expect(serverless.cli.consoleLog).toHaveBeenCalledWith(
127+
expect.stringContaining('"errorMessage": "SYNC_ERROR"')
128+
);
129+
});
130+
131+
it('should invoke an async handler', async () => {
132+
const functionConfig = {
133+
...baseConfig,
134+
handler: 'httpAsyncHandler',
135+
};
136+
await googleInvokeLocal.invokeLocalNodeJs(functionConfig, req, context);
137+
// eslint-disable-next-line no-console
138+
expect(console.log).toHaveBeenCalledWith('HTTP_ASYNC_HANDLER');
139+
expect(serverless.cli.consoleLog).toHaveBeenCalledWith(
140+
JSON.stringify({ status: 200, body: JSON.stringify({ responseMessage: message }) }, null, 4)
141+
);
142+
});
143+
144+
it('should handle errors in an async handler', async () => {
145+
const functionConfig = {
146+
...baseConfig,
147+
handler: 'httpAsyncHandlerWithError',
148+
};
149+
await googleInvokeLocal.invokeLocalNodeJs(functionConfig, req, context);
150+
// eslint-disable-next-line no-console
151+
expect(console.log).toHaveBeenCalledWith('HTTP_ASYNC_HANDLER');
152+
expect(serverless.cli.consoleLog).toHaveBeenCalledWith(
153+
expect.stringContaining('"errorMessage": "ASYNC_ERROR"')
154+
);
155+
});
156+
157+
it('should give the environment variables to the handler', async () => {
158+
const functionConfig = {
159+
...baseConfig,
160+
handler: 'httpEnvHandler',
161+
};
162+
await googleInvokeLocal.invokeLocalNodeJs(functionConfig, req, context);
163+
// eslint-disable-next-line no-console
164+
expect(console.log).toHaveBeenCalledWith(myVarValue);
165+
});
84166
});
85167
});

0 commit comments

Comments
 (0)