Skip to content

Commit 9e07fed

Browse files
authored
feat: Add support for invoke local (#258)
1 parent 2c4d52c commit 9e07fed

17 files changed

+751
-41
lines changed

index.js

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const GooglePackage = require('./package/googlePackage');
1111
const GoogleDeploy = require('./deploy/googleDeploy');
1212
const GoogleRemove = require('./remove/googleRemove');
1313
const GoogleInvoke = require('./invoke/googleInvoke');
14+
const GoogleInvokeLocal = require('./invokeLocal/googleInvokeLocal');
1415
const GoogleLogs = require('./logs/googleLogs');
1516
const GoogleInfo = require('./info/googleInfo');
1617

@@ -24,6 +25,7 @@ class GoogleIndex {
2425
this.serverless.pluginManager.addPlugin(GoogleDeploy);
2526
this.serverless.pluginManager.addPlugin(GoogleRemove);
2627
this.serverless.pluginManager.addPlugin(GoogleInvoke);
28+
this.serverless.pluginManager.addPlugin(GoogleInvokeLocal);
2729
this.serverless.pluginManager.addPlugin(GoogleLogs);
2830
this.serverless.pluginManager.addPlugin(GoogleInfo);
2931
}

invokeLocal/googleInvokeLocal.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use strict';
2+
3+
const validate = require('../shared/validate');
4+
const setDefaults = require('../shared/utils');
5+
const getDataAndContext = require('./lib/getDataAndContext');
6+
const nodeJs = require('./lib/nodeJs');
7+
8+
class GoogleInvokeLocal {
9+
constructor(serverless, options) {
10+
this.serverless = serverless;
11+
this.options = options;
12+
13+
this.provider = this.serverless.getProvider('google');
14+
15+
Object.assign(this, validate, setDefaults, getDataAndContext, nodeJs);
16+
17+
this.hooks = {
18+
'initialize': () => {
19+
this.options = this.serverless.processedInput.options;
20+
},
21+
'before:invoke:local:invoke': async () => {
22+
await this.validate();
23+
await this.setDefaults();
24+
await this.getDataAndContext();
25+
},
26+
'invoke:local:invoke': async () => this.invokeLocal(),
27+
};
28+
}
29+
30+
async invokeLocal() {
31+
const functionObj = this.serverless.service.getFunction(this.options.function);
32+
this.validateEventsProperty(functionObj, this.options.function, ['event']); // Only event is currently supported
33+
34+
const runtime = this.provider.getRuntime(functionObj);
35+
if (!runtime.startsWith('nodejs')) {
36+
throw new Error(`Local invocation with runtime ${runtime} is not supported`);
37+
}
38+
return this.invokeLocalNodeJs(functionObj, this.options.data, this.options.context);
39+
}
40+
}
41+
42+
module.exports = GoogleInvokeLocal;

invokeLocal/googleInvokeLocal.test.js

+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
'use strict';
2+
3+
const sinon = require('sinon');
4+
5+
const GoogleProvider = require('../provider/googleProvider');
6+
const GoogleInvokeLocal = require('./googleInvokeLocal');
7+
const Serverless = require('../test/serverless');
8+
9+
describe('GoogleInvokeLocal', () => {
10+
let serverless;
11+
const functionName = 'myFunction';
12+
const rawOptions = {
13+
f: functionName,
14+
};
15+
const processedOptions = {
16+
function: functionName,
17+
};
18+
let googleInvokeLocal;
19+
20+
beforeAll(() => {
21+
serverless = new Serverless();
22+
serverless.setProvider('google', new GoogleProvider(serverless));
23+
googleInvokeLocal = new GoogleInvokeLocal(serverless, rawOptions);
24+
serverless.processedInput.options = processedOptions;
25+
});
26+
27+
describe('#constructor()', () => {
28+
it('should set the serverless instance', () => {
29+
expect(googleInvokeLocal.serverless).toEqual(serverless);
30+
});
31+
32+
it('should set the raw options if provided', () => {
33+
expect(googleInvokeLocal.options).toEqual(rawOptions);
34+
});
35+
36+
it('should make the provider accessible', () => {
37+
expect(googleInvokeLocal.provider).toBeInstanceOf(GoogleProvider);
38+
});
39+
40+
it.each`
41+
method
42+
${'validate'}
43+
${'setDefaults'}
44+
${'getDataAndContext'}
45+
${'invokeLocalNodeJs'}
46+
${'loadFileInOption'}
47+
${'validateEventsProperty'}
48+
${'addEnvironmentVariablesToProcessEnv'}
49+
`('should declare $method method', ({ method }) => {
50+
expect(googleInvokeLocal[method]).toBeDefined();
51+
});
52+
53+
describe('hooks', () => {
54+
let validateStub;
55+
let setDefaultsStub;
56+
let getDataAndContextStub;
57+
let invokeLocalStub;
58+
59+
beforeAll(() => {
60+
validateStub = sinon.stub(googleInvokeLocal, 'validate').resolves();
61+
setDefaultsStub = sinon.stub(googleInvokeLocal, 'setDefaults').resolves();
62+
getDataAndContextStub = sinon.stub(googleInvokeLocal, 'getDataAndContext').resolves();
63+
invokeLocalStub = sinon.stub(googleInvokeLocal, 'invokeLocal').resolves();
64+
});
65+
66+
afterEach(() => {
67+
googleInvokeLocal.validate.resetHistory();
68+
googleInvokeLocal.setDefaults.resetHistory();
69+
googleInvokeLocal.getDataAndContext.resetHistory();
70+
googleInvokeLocal.invokeLocal.resetHistory();
71+
});
72+
73+
afterAll(() => {
74+
googleInvokeLocal.validate.restore();
75+
googleInvokeLocal.setDefaults.restore();
76+
googleInvokeLocal.getDataAndContext.restore();
77+
googleInvokeLocal.invokeLocal.restore();
78+
});
79+
80+
it.each`
81+
hook
82+
${'initialize'}
83+
${'before:invoke:local:invoke'}
84+
${'invoke:local:invoke'}
85+
`('should declare $hook hook', ({ hook }) => {
86+
expect(googleInvokeLocal.hooks[hook]).toBeDefined();
87+
});
88+
89+
describe('initialize hook', () => {
90+
it('should override raw options with processed options', () => {
91+
googleInvokeLocal.hooks.initialize();
92+
expect(googleInvokeLocal.options).toEqual(processedOptions);
93+
});
94+
});
95+
96+
describe('before:invoke:local:invoke hook', () => {
97+
it('should validate the configuration', async () => {
98+
await googleInvokeLocal.hooks['before:invoke:local:invoke']();
99+
expect(validateStub.calledOnce).toEqual(true);
100+
});
101+
102+
it('should set the defaults values', async () => {
103+
await googleInvokeLocal.hooks['before:invoke:local:invoke']();
104+
expect(setDefaultsStub.calledOnce).toEqual(true);
105+
});
106+
107+
it('should resolve the data and the context of the invocation', async () => {
108+
await googleInvokeLocal.hooks['before:invoke:local:invoke']();
109+
expect(getDataAndContextStub.calledOnce).toEqual(true);
110+
});
111+
});
112+
113+
describe('invoke:local:invoke hook', () => {
114+
it('should invoke the function locally', () => {
115+
googleInvokeLocal.hooks['invoke:local:invoke']();
116+
expect(invokeLocalStub.calledOnce).toEqual(true);
117+
});
118+
});
119+
});
120+
});
121+
122+
describe('#invokeLocal()', () => {
123+
const functionObj = Symbol('functionObj');
124+
const data = Symbol('data');
125+
const context = Symbol('context');
126+
const runtime = 'nodejs14';
127+
let getFunctionStub;
128+
let validateEventsPropertyStub;
129+
let getRuntimeStub;
130+
let invokeLocalNodeJsStub;
131+
132+
beforeAll(() => {
133+
googleInvokeLocal.options = {
134+
...processedOptions, // invokeLocal is called after the initialize hook which override the options
135+
data, // data and context are populated by getDataAndContext in before:invoke:local:invoke hook
136+
context,
137+
};
138+
getFunctionStub = sinon.stub(serverless.service, 'getFunction').returns(functionObj);
139+
validateEventsPropertyStub = sinon
140+
.stub(googleInvokeLocal, 'validateEventsProperty')
141+
.returns();
142+
getRuntimeStub = sinon.stub(googleInvokeLocal.provider, 'getRuntime').returns(runtime);
143+
144+
invokeLocalNodeJsStub = sinon.stub(googleInvokeLocal, 'invokeLocalNodeJs').resolves();
145+
});
146+
147+
afterEach(() => {
148+
serverless.service.getFunction.resetHistory();
149+
googleInvokeLocal.validateEventsProperty.resetHistory();
150+
googleInvokeLocal.provider.getRuntime.resetHistory();
151+
googleInvokeLocal.invokeLocalNodeJs.resetHistory();
152+
});
153+
154+
afterAll(() => {
155+
serverless.service.getFunction.restore();
156+
googleInvokeLocal.validateEventsProperty.restore();
157+
googleInvokeLocal.provider.getRuntime.restore();
158+
googleInvokeLocal.invokeLocalNodeJs.restore();
159+
});
160+
161+
it('should get the function configuration', async () => {
162+
await googleInvokeLocal.invokeLocal();
163+
expect(getFunctionStub.calledOnceWith(functionName)).toEqual(true);
164+
});
165+
166+
it('should validate the function configuration', async () => {
167+
await googleInvokeLocal.invokeLocal();
168+
expect(
169+
validateEventsPropertyStub.calledOnceWith(functionObj, functionName, ['event'])
170+
).toEqual(true);
171+
});
172+
173+
it('should get the runtime', async () => {
174+
await googleInvokeLocal.invokeLocal();
175+
expect(getRuntimeStub.calledOnceWith(functionObj)).toEqual(true);
176+
});
177+
178+
it('should invoke locally the function with node js', async () => {
179+
await googleInvokeLocal.invokeLocal();
180+
expect(invokeLocalNodeJsStub.calledOnceWith(functionObj, data, context)).toEqual(true);
181+
});
182+
183+
it('should throw if the runtime is not node js', async () => {
184+
getRuntimeStub.returns('python3');
185+
await expect(googleInvokeLocal.invokeLocal()).rejects.toThrow(
186+
'Local invocation with runtime python3 is not supported'
187+
);
188+
});
189+
});
190+
});

invokeLocal/lib/getDataAndContext.js

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use strict';
2+
3+
const path = require('path');
4+
const fs = require('fs');
5+
const stdin = require('get-stdin');
6+
7+
module.exports = {
8+
async loadFileInOption(filePath, optionKey) {
9+
const absolutePath = path.isAbsolute(filePath)
10+
? filePath
11+
: path.join(this.serverless.serviceDir, filePath);
12+
13+
if (!fs.existsSync(absolutePath)) {
14+
throw new Error(`The file you provided does not exist: ${absolutePath}`);
15+
}
16+
if (absolutePath.endsWith('.js')) {
17+
// to support js - export as an input data
18+
this.options[optionKey] = require(absolutePath);
19+
return;
20+
}
21+
this.options[optionKey] = await this.serverless.utils.readFile(absolutePath);
22+
},
23+
24+
async getDataAndContext() {
25+
// unless asked to preserve raw input, attempt to parse any provided objects
26+
if (!this.options.raw) {
27+
if (this.options.data) {
28+
try {
29+
this.options.data = JSON.parse(this.options.data);
30+
} catch (exception) {
31+
// do nothing if it's a simple string or object already
32+
}
33+
}
34+
if (this.options.context) {
35+
try {
36+
this.options.context = JSON.parse(this.options.context);
37+
} catch (exception) {
38+
// do nothing if it's a simple string or object already
39+
}
40+
}
41+
}
42+
43+
if (!this.options.data) {
44+
if (this.options.path) {
45+
await this.loadFileInOption(this.options.path, 'data');
46+
} else {
47+
try {
48+
this.options.data = await stdin();
49+
} catch (e) {
50+
// continue if no stdin was provided
51+
}
52+
}
53+
}
54+
55+
if (!this.options.context && this.options.contextPath) {
56+
await this.loadFileInOption(this.options.contextPath, 'context');
57+
}
58+
},
59+
};
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
'use strict';
2+
3+
const sinon = require('sinon');
4+
5+
const GoogleProvider = require('../../provider/googleProvider');
6+
const GoogleInvokeLocal = require('../googleInvokeLocal');
7+
const Serverless = require('../../test/serverless');
8+
9+
jest.mock('get-stdin');
10+
11+
describe('getDataAndContext', () => {
12+
let serverless;
13+
let googleInvokeLocal;
14+
let loadFileInOptionStub;
15+
16+
beforeEach(() => {
17+
serverless = new Serverless();
18+
serverless.setProvider('google', new GoogleProvider(serverless));
19+
googleInvokeLocal = new GoogleInvokeLocal(serverless, {});
20+
loadFileInOptionStub = sinon.stub(googleInvokeLocal, 'loadFileInOption').resolves();
21+
});
22+
23+
afterEach(() => {
24+
googleInvokeLocal.loadFileInOption.restore();
25+
});
26+
27+
describe.each`
28+
key | pathKey
29+
${'data'} | ${'path'}
30+
${'context'} | ${'contextPath'}
31+
`('$key', ({ key, pathKey }) => {
32+
it('should keep the raw value if the value exist and there is the raw option', async () => {
33+
const rawValue = Symbol('rawValue');
34+
googleInvokeLocal.options[key] = rawValue;
35+
googleInvokeLocal.options.raw = true;
36+
await googleInvokeLocal.getDataAndContext();
37+
expect(googleInvokeLocal.options[key]).toEqual(rawValue);
38+
});
39+
40+
it('should keep the raw value if the value exist and is not a valid JSON', async () => {
41+
const rawValue = 'rawValue';
42+
googleInvokeLocal.options[key] = rawValue;
43+
await googleInvokeLocal.getDataAndContext();
44+
expect(googleInvokeLocal.options[key]).toEqual(rawValue);
45+
});
46+
47+
it('should parse the raw value if the value exist and is a stringified JSON', async () => {
48+
googleInvokeLocal.options[key] = '{"attribute":"value"}';
49+
await googleInvokeLocal.getDataAndContext();
50+
expect(googleInvokeLocal.options[key]).toEqual({ attribute: 'value' });
51+
});
52+
53+
it('should load the file from the provided path if it exists', async () => {
54+
const path = 'path';
55+
googleInvokeLocal.options[pathKey] = path;
56+
await googleInvokeLocal.getDataAndContext();
57+
expect(loadFileInOptionStub.calledOnceWith(path, key)).toEqual(true);
58+
});
59+
60+
it('should not load the file from the provided path if the key already exists', async () => {
61+
const rawValue = Symbol('rawValue');
62+
googleInvokeLocal.options[key] = rawValue;
63+
googleInvokeLocal.options[pathKey] = 'path';
64+
65+
await googleInvokeLocal.getDataAndContext();
66+
67+
expect(loadFileInOptionStub.notCalled).toEqual(true);
68+
expect(googleInvokeLocal.options[key]).toEqual(rawValue);
69+
});
70+
});
71+
});

0 commit comments

Comments
 (0)