Skip to content

Commit

Permalink
feat: Add support for invoke local (#258)
Browse files Browse the repository at this point in the history
  • Loading branch information
CorentinDoue authored Jun 1, 2021
1 parent 2c4d52c commit 9e07fed
Show file tree
Hide file tree
Showing 17 changed files with 751 additions and 41 deletions.
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const GooglePackage = require('./package/googlePackage');
const GoogleDeploy = require('./deploy/googleDeploy');
const GoogleRemove = require('./remove/googleRemove');
const GoogleInvoke = require('./invoke/googleInvoke');
const GoogleInvokeLocal = require('./invokeLocal/googleInvokeLocal');
const GoogleLogs = require('./logs/googleLogs');
const GoogleInfo = require('./info/googleInfo');

Expand All @@ -24,6 +25,7 @@ class GoogleIndex {
this.serverless.pluginManager.addPlugin(GoogleDeploy);
this.serverless.pluginManager.addPlugin(GoogleRemove);
this.serverless.pluginManager.addPlugin(GoogleInvoke);
this.serverless.pluginManager.addPlugin(GoogleInvokeLocal);
this.serverless.pluginManager.addPlugin(GoogleLogs);
this.serverless.pluginManager.addPlugin(GoogleInfo);
}
Expand Down
42 changes: 42 additions & 0 deletions invokeLocal/googleInvokeLocal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';

const validate = require('../shared/validate');
const setDefaults = require('../shared/utils');
const getDataAndContext = require('./lib/getDataAndContext');
const nodeJs = require('./lib/nodeJs');

class GoogleInvokeLocal {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;

this.provider = this.serverless.getProvider('google');

Object.assign(this, validate, setDefaults, getDataAndContext, nodeJs);

this.hooks = {
'initialize': () => {
this.options = this.serverless.processedInput.options;
},
'before:invoke:local:invoke': async () => {
await this.validate();
await this.setDefaults();
await this.getDataAndContext();
},
'invoke:local:invoke': async () => this.invokeLocal(),
};
}

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

const runtime = this.provider.getRuntime(functionObj);
if (!runtime.startsWith('nodejs')) {
throw new Error(`Local invocation with runtime ${runtime} is not supported`);
}
return this.invokeLocalNodeJs(functionObj, this.options.data, this.options.context);
}
}

module.exports = GoogleInvokeLocal;
190 changes: 190 additions & 0 deletions invokeLocal/googleInvokeLocal.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
'use strict';

const sinon = require('sinon');

const GoogleProvider = require('../provider/googleProvider');
const GoogleInvokeLocal = require('./googleInvokeLocal');
const Serverless = require('../test/serverless');

describe('GoogleInvokeLocal', () => {
let serverless;
const functionName = 'myFunction';
const rawOptions = {
f: functionName,
};
const processedOptions = {
function: functionName,
};
let googleInvokeLocal;

beforeAll(() => {
serverless = new Serverless();
serverless.setProvider('google', new GoogleProvider(serverless));
googleInvokeLocal = new GoogleInvokeLocal(serverless, rawOptions);
serverless.processedInput.options = processedOptions;
});

describe('#constructor()', () => {
it('should set the serverless instance', () => {
expect(googleInvokeLocal.serverless).toEqual(serverless);
});

it('should set the raw options if provided', () => {
expect(googleInvokeLocal.options).toEqual(rawOptions);
});

it('should make the provider accessible', () => {
expect(googleInvokeLocal.provider).toBeInstanceOf(GoogleProvider);
});

it.each`
method
${'validate'}
${'setDefaults'}
${'getDataAndContext'}
${'invokeLocalNodeJs'}
${'loadFileInOption'}
${'validateEventsProperty'}
${'addEnvironmentVariablesToProcessEnv'}
`('should declare $method method', ({ method }) => {
expect(googleInvokeLocal[method]).toBeDefined();
});

describe('hooks', () => {
let validateStub;
let setDefaultsStub;
let getDataAndContextStub;
let invokeLocalStub;

beforeAll(() => {
validateStub = sinon.stub(googleInvokeLocal, 'validate').resolves();
setDefaultsStub = sinon.stub(googleInvokeLocal, 'setDefaults').resolves();
getDataAndContextStub = sinon.stub(googleInvokeLocal, 'getDataAndContext').resolves();
invokeLocalStub = sinon.stub(googleInvokeLocal, 'invokeLocal').resolves();
});

afterEach(() => {
googleInvokeLocal.validate.resetHistory();
googleInvokeLocal.setDefaults.resetHistory();
googleInvokeLocal.getDataAndContext.resetHistory();
googleInvokeLocal.invokeLocal.resetHistory();
});

afterAll(() => {
googleInvokeLocal.validate.restore();
googleInvokeLocal.setDefaults.restore();
googleInvokeLocal.getDataAndContext.restore();
googleInvokeLocal.invokeLocal.restore();
});

it.each`
hook
${'initialize'}
${'before:invoke:local:invoke'}
${'invoke:local:invoke'}
`('should declare $hook hook', ({ hook }) => {
expect(googleInvokeLocal.hooks[hook]).toBeDefined();
});

describe('initialize hook', () => {
it('should override raw options with processed options', () => {
googleInvokeLocal.hooks.initialize();
expect(googleInvokeLocal.options).toEqual(processedOptions);
});
});

describe('before:invoke:local:invoke hook', () => {
it('should validate the configuration', async () => {
await googleInvokeLocal.hooks['before:invoke:local:invoke']();
expect(validateStub.calledOnce).toEqual(true);
});

it('should set the defaults values', async () => {
await googleInvokeLocal.hooks['before:invoke:local:invoke']();
expect(setDefaultsStub.calledOnce).toEqual(true);
});

it('should resolve the data and the context of the invocation', async () => {
await googleInvokeLocal.hooks['before:invoke:local:invoke']();
expect(getDataAndContextStub.calledOnce).toEqual(true);
});
});

describe('invoke:local:invoke hook', () => {
it('should invoke the function locally', () => {
googleInvokeLocal.hooks['invoke:local:invoke']();
expect(invokeLocalStub.calledOnce).toEqual(true);
});
});
});
});

describe('#invokeLocal()', () => {
const functionObj = Symbol('functionObj');
const data = Symbol('data');
const context = Symbol('context');
const runtime = 'nodejs14';
let getFunctionStub;
let validateEventsPropertyStub;
let getRuntimeStub;
let invokeLocalNodeJsStub;

beforeAll(() => {
googleInvokeLocal.options = {
...processedOptions, // invokeLocal is called after the initialize hook which override the options
data, // data and context are populated by getDataAndContext in before:invoke:local:invoke hook
context,
};
getFunctionStub = sinon.stub(serverless.service, 'getFunction').returns(functionObj);
validateEventsPropertyStub = sinon
.stub(googleInvokeLocal, 'validateEventsProperty')
.returns();
getRuntimeStub = sinon.stub(googleInvokeLocal.provider, 'getRuntime').returns(runtime);

invokeLocalNodeJsStub = sinon.stub(googleInvokeLocal, 'invokeLocalNodeJs').resolves();
});

afterEach(() => {
serverless.service.getFunction.resetHistory();
googleInvokeLocal.validateEventsProperty.resetHistory();
googleInvokeLocal.provider.getRuntime.resetHistory();
googleInvokeLocal.invokeLocalNodeJs.resetHistory();
});

afterAll(() => {
serverless.service.getFunction.restore();
googleInvokeLocal.validateEventsProperty.restore();
googleInvokeLocal.provider.getRuntime.restore();
googleInvokeLocal.invokeLocalNodeJs.restore();
});

it('should get the function configuration', async () => {
await googleInvokeLocal.invokeLocal();
expect(getFunctionStub.calledOnceWith(functionName)).toEqual(true);
});

it('should validate the function configuration', async () => {
await googleInvokeLocal.invokeLocal();
expect(
validateEventsPropertyStub.calledOnceWith(functionObj, functionName, ['event'])
).toEqual(true);
});

it('should get the runtime', async () => {
await googleInvokeLocal.invokeLocal();
expect(getRuntimeStub.calledOnceWith(functionObj)).toEqual(true);
});

it('should invoke locally the function with node js', async () => {
await googleInvokeLocal.invokeLocal();
expect(invokeLocalNodeJsStub.calledOnceWith(functionObj, data, context)).toEqual(true);
});

it('should throw if the runtime is not node js', async () => {
getRuntimeStub.returns('python3');
await expect(googleInvokeLocal.invokeLocal()).rejects.toThrow(
'Local invocation with runtime python3 is not supported'
);
});
});
});
59 changes: 59 additions & 0 deletions invokeLocal/lib/getDataAndContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use strict';

const path = require('path');
const fs = require('fs');
const stdin = require('get-stdin');

module.exports = {
async loadFileInOption(filePath, optionKey) {
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.join(this.serverless.serviceDir, filePath);

if (!fs.existsSync(absolutePath)) {
throw new Error(`The file you provided does not exist: ${absolutePath}`);
}
if (absolutePath.endsWith('.js')) {
// to support js - export as an input data
this.options[optionKey] = require(absolutePath);
return;
}
this.options[optionKey] = await this.serverless.utils.readFile(absolutePath);
},

async getDataAndContext() {
// unless asked to preserve raw input, attempt to parse any provided objects
if (!this.options.raw) {
if (this.options.data) {
try {
this.options.data = JSON.parse(this.options.data);
} catch (exception) {
// do nothing if it's a simple string or object already
}
}
if (this.options.context) {
try {
this.options.context = JSON.parse(this.options.context);
} catch (exception) {
// do nothing if it's a simple string or object already
}
}
}

if (!this.options.data) {
if (this.options.path) {
await this.loadFileInOption(this.options.path, 'data');
} else {
try {
this.options.data = await stdin();
} catch (e) {
// continue if no stdin was provided
}
}
}

if (!this.options.context && this.options.contextPath) {
await this.loadFileInOption(this.options.contextPath, 'context');
}
},
};
71 changes: 71 additions & 0 deletions invokeLocal/lib/getDataAndContext.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
'use strict';

const sinon = require('sinon');

const GoogleProvider = require('../../provider/googleProvider');
const GoogleInvokeLocal = require('../googleInvokeLocal');
const Serverless = require('../../test/serverless');

jest.mock('get-stdin');

describe('getDataAndContext', () => {
let serverless;
let googleInvokeLocal;
let loadFileInOptionStub;

beforeEach(() => {
serverless = new Serverless();
serverless.setProvider('google', new GoogleProvider(serverless));
googleInvokeLocal = new GoogleInvokeLocal(serverless, {});
loadFileInOptionStub = sinon.stub(googleInvokeLocal, 'loadFileInOption').resolves();
});

afterEach(() => {
googleInvokeLocal.loadFileInOption.restore();
});

describe.each`
key | pathKey
${'data'} | ${'path'}
${'context'} | ${'contextPath'}
`('$key', ({ key, pathKey }) => {
it('should keep the raw value if the value exist and there is the raw option', async () => {
const rawValue = Symbol('rawValue');
googleInvokeLocal.options[key] = rawValue;
googleInvokeLocal.options.raw = true;
await googleInvokeLocal.getDataAndContext();
expect(googleInvokeLocal.options[key]).toEqual(rawValue);
});

it('should keep the raw value if the value exist and is not a valid JSON', async () => {
const rawValue = 'rawValue';
googleInvokeLocal.options[key] = rawValue;
await googleInvokeLocal.getDataAndContext();
expect(googleInvokeLocal.options[key]).toEqual(rawValue);
});

it('should parse the raw value if the value exist and is a stringified JSON', async () => {
googleInvokeLocal.options[key] = '{"attribute":"value"}';
await googleInvokeLocal.getDataAndContext();
expect(googleInvokeLocal.options[key]).toEqual({ attribute: 'value' });
});

it('should load the file from the provided path if it exists', async () => {
const path = 'path';
googleInvokeLocal.options[pathKey] = path;
await googleInvokeLocal.getDataAndContext();
expect(loadFileInOptionStub.calledOnceWith(path, key)).toEqual(true);
});

it('should not load the file from the provided path if the key already exists', async () => {
const rawValue = Symbol('rawValue');
googleInvokeLocal.options[key] = rawValue;
googleInvokeLocal.options[pathKey] = 'path';

await googleInvokeLocal.getDataAndContext();

expect(loadFileInOptionStub.notCalled).toEqual(true);
expect(googleInvokeLocal.options[key]).toEqual(rawValue);
});
});
});
Loading

0 comments on commit 9e07fed

Please sign in to comment.