-
Notifications
You must be signed in to change notification settings - Fork 127
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add support for invoke local (#258)
- Loading branch information
1 parent
2c4d52c
commit 9e07fed
Showing
17 changed files
with
751 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.