node-lambda
Version:
Command line tool for locally running and remotely deploying your node.js applications to Amazon Lambda.
1,504 lines (1,322 loc) • 54.6 kB
JavaScript
'use strict'
const process = require('process')
const path = require('path')
const os = require('os')
const fs = require('fs-extra')
const { execFileSync } = require('child_process')
const lambda = require(path.join(__dirname, '..', 'lib', 'main'))
const Zip = require('node-zip')
const { assert } = require('chai')
const awsMock = require('aws-sdk-mock')
awsMock.setSDK(path.resolve('node_modules/aws-sdk'))
const originalProgram = {
packageManager: 'npm',
environment: 'development',
accessKey: 'key',
secretKey: 'secret',
sessionToken: 'token',
functionName: '___node-lambda',
handler: 'index.handler',
role: 'some:arn:aws:iam::role',
memorySize: 128,
timeout: 3,
description: '',
runtime: 'nodejs20.x',
deadLetterConfigTargetArn: '',
tracingConfig: '',
Layers: '',
retentionInDays: 30,
region: 'us-east-1,us-west-2,eu-west-1',
eventFile: 'event.json',
eventSourceFile: '',
contextFile: 'context.json',
deployTimeout: 120000,
prebuiltDirectory: '',
proxy: '',
optionalDependencies: true
}
let program = {}
let codeDirectory = lambda._codeDirectory()
const sourceDirectoryForTest = path.join('.', 'test', 'testPj')
const _timeout = function (params) {
// Even if timeout is set for the whole test for Windows and Mac,
// if it is set in local it will be valid.
// For Windows and Mac, do not set it with local.
if (!['win32', 'darwin'].includes(process.platform)) {
params.this.timeout(params.sec * 1000)
}
}
// It does not completely reproduce the response of the actual API.
const lambdaMockSettings = {
addPermission: {},
getFunction: {
Code: {},
Configuration: { LastUpdateStatus: 'Successful' },
FunctionArn: 'Lambda.getFunction.mock.FunctionArn'
},
createFunction: {
FunctionArn: 'Lambda.createFunction.mock.FunctionArn',
FunctionName: 'Lambda.createFunction.mock.FunctionName'
},
listEventSourceMappings: {
EventSourceMappings: [{
EventSourceArn: 'Lambda.listEventSourceMappings.mock.EventSourceArn',
UUID: 'Lambda.listEventSourceMappings.mock.UUID'
}]
},
updateFunctionCode: {
FunctionArn: 'Lambda.updateFunctionCode.mock.FunctionArn',
FunctionName: 'Lambda.updateFunctionCode.mock.FunctionName'
},
updateFunctionConfiguration: {
FunctionArn: 'Lambda.updateFunctionConfiguration.mock.FunctionArn',
FunctionName: 'Lambda.updateFunctionConfiguration.mock.FunctionName'
},
createEventSourceMapping: {
EventSourceArn: 'Lambda.createEventSourceMapping.mock.EventSourceArn',
FunctionName: 'Lambda.createEventSourceMapping.mock.EventSourceArn'
},
updateEventSourceMapping: {
EventSourceArn: 'Lambda.updateEventSourceMapping.mock.EventSourceArn',
FunctionName: 'Lambda.updateEventSourceMapping.mock.EventSourceArn'
},
deleteEventSourceMapping: {
EventSourceArn: 'Lambda.deleteEventSourceMapping.mock.EventSourceArn',
FunctionName: 'Lambda.deleteEventSourceMapping.mock.EventSourceArn'
},
listTags: {
Tags: { tag1: 'key1' }
},
untagResource: {},
tagResource: {}
}
const _mockSetting = () => {
awsMock.mock('CloudWatchEvents', 'putRule', (params, callback) => {
callback(null, {})
})
awsMock.mock('CloudWatchEvents', 'putTargets', (params, callback) => {
callback(null, {})
})
awsMock.mock('CloudWatchLogs', 'createLogGroup', (params, callback) => {
callback(null, {})
})
awsMock.mock('CloudWatchLogs', 'putRetentionPolicy', (params, callback) => {
callback(null, {})
})
awsMock.mock('S3', 'putBucketNotificationConfiguration', (params, callback) => {
callback(null, {})
})
awsMock.mock('S3', 'putObject', (params, callback) => {
callback(null, { test: 'putObject' })
})
Object.keys(lambdaMockSettings).forEach((method) => {
awsMock.mock('Lambda', method, (params, callback) => {
callback(null, lambdaMockSettings[method])
})
})
return require('aws-sdk')
}
const _awsRestore = () => {
awsMock.restore('CloudWatchEvents')
awsMock.restore('CloudWatchLogs')
awsMock.restore('S3')
awsMock.restore('Lambda')
}
/* global before, after, beforeEach, afterEach, describe, it */
describe('lib/main', function () {
if (['win32', 'darwin'].includes(process.platform)) {
// It seems that it takes time for file operation in Windows and Mac.
// So set `timeout(120000)` for the whole test.
this.timeout(120000)
}
let aws = null // mock
let awsLambda = null // mock
before(() => {
aws = _mockSetting()
awsLambda = new aws.Lambda({ apiVersion: '2015-03-31' })
if (process.platform === 'win32') {
execFileSync('cmd.exe', ['/c', 'npm', 'ci'], { cwd: sourceDirectoryForTest })
return
}
execFileSync('npm', ['ci'], { cwd: sourceDirectoryForTest })
})
after(() => _awsRestore())
beforeEach(() => {
program = Object.assign({}, originalProgram) // clone
})
it('version should be set', () => {
assert.equal(lambda.version, '1.3.0')
})
describe('_codeDirectory', () => {
it('Working directory in the /tmp directory', () => {
assert.equal(
lambda._codeDirectory(),
path.join(fs.realpathSync(os.tmpdir()), 'node-lambda-lambda')
)
})
})
describe('_runHandler', () => {
it('context methods is a function', (done) => {
const handler = (event, context, callback) => {
assert.isFunction(context.succeed)
assert.isFunction(context.fail)
assert.isFunction(context.done)
done()
}
lambda._runHandler(handler, {}, program, {})
})
})
describe('_isFunctionDoesNotExist', () => {
it('=== true', () => {
const err = {
code: 'ResourceNotFoundException',
message: 'Function not found: arn:aws:lambda:XXX'
}
assert.isTrue(lambda._isFunctionDoesNotExist(err))
})
it('=== false', () => {
const err = {
code: 'MissingRequiredParameter',
message: 'Missing required key \'FunctionName\' in params'
}
assert.isFalse(lambda._isFunctionDoesNotExist(err))
})
})
describe('_isUseS3', () => {
it('=== true', () => {
assert.isTrue(lambda._isUseS3({ deployUseS3: true }))
assert.isTrue(lambda._isUseS3({ deployUseS3: 'true' }))
})
it('=== false', () => {
[
{},
{ deployUseS3: false },
{ deployUseS3: 'false' },
{ deployUseS3: 'foo' }
].forEach((params) => {
assert.isFalse(lambda._isUseS3(params), params)
})
})
})
describe('_useECR', () => {
it('=== true', () => {
assert.isTrue(lambda._useECR({ imageUri: 'xxx' }))
})
it('=== false', () => {
[
{},
{ imageUri: null },
{ imageUri: '' }
].forEach((params) => {
assert.isFalse(lambda._useECR(params), params)
})
})
})
describe('_params', () => {
// http://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-FunctionName
const functionNamePattern =
/(arn:aws:lambda:)?([a-z]{2}-[a-z]+-\d{1}:)?(\d{12}:)?(function:)?([a-zA-Z0-9-_]+)(:(\$LATEST|[a-zA-Z0-9-_]+))?/
it('appends environment to original functionName', () => {
const params = lambda._params(program)
assert.equal(params.FunctionName, '___node-lambda-development')
assert.match(params.FunctionName, functionNamePattern)
})
it('appends environment to original functionName (production)', () => {
program.environment = 'production'
const params = lambda._params(program)
assert.equal(params.FunctionName, '___node-lambda-production')
assert.match(params.FunctionName, functionNamePattern)
})
it('appends version to original functionName', () => {
program.lambdaVersion = '2015-02-01'
const params = lambda._params(program)
assert.equal(params.FunctionName, '___node-lambda-development-2015-02-01')
assert.match(params.FunctionName, functionNamePattern)
})
it('appends version to original functionName (value not allowed by AWS)', () => {
program.lambdaVersion = '2015.02.01'
const params = lambda._params(program)
assert.equal(params.FunctionName, '___node-lambda-development-2015_02_01')
assert.match(params.FunctionName, functionNamePattern)
})
it('appends VpcConfig to params when vpc params set', () => {
program.vpcSubnets = 'subnet-00000000,subnet-00000001,subnet-00000002'
program.vpcSecurityGroups = 'sg-00000000,sg-00000001,sg-00000002'
const params = lambda._params(program)
assert.equal(params.VpcConfig.SubnetIds[0], program.vpcSubnets.split(',')[0])
assert.equal(params.VpcConfig.SubnetIds[1], program.vpcSubnets.split(',')[1])
assert.equal(params.VpcConfig.SubnetIds[2], program.vpcSubnets.split(',')[2])
assert.equal(params.VpcConfig.SecurityGroupIds[0], program.vpcSecurityGroups.split(',')[0])
assert.equal(params.VpcConfig.SecurityGroupIds[1], program.vpcSecurityGroups.split(',')[1])
assert.equal(params.VpcConfig.SecurityGroupIds[2], program.vpcSecurityGroups.split(',')[2])
})
it('does not append VpcConfig when params are not set', () => {
const params = lambda._params(program)
assert.equal(Object.keys(params.VpcConfig.SubnetIds).length, 0)
assert.equal(Object.keys(params.VpcConfig.SecurityGroupIds).length, 0)
})
it('appends KMSKeyArn to params when KMS params set', () => {
['', 'arn:aws:kms:test'].forEach((v) => {
program.kmsKeyArn = v
const params = lambda._params(program)
assert.equal(params.KMSKeyArn, v, v)
})
})
it('does not append KMSKeyArn when params are not set', () => {
const params = lambda._params(program)
assert.isUndefined(params.KMSKeyArn)
})
it('appends DeadLetterConfig to params when DLQ params set', () => {
['', 'arn:aws:sqs:test'].forEach((v) => {
program.deadLetterConfigTargetArn = v
const params = lambda._params(program)
assert.equal(params.DeadLetterConfig.TargetArn, v, v)
})
})
it('does not append DeadLetterConfig when params are not set', () => {
delete program.deadLetterConfigTargetArn
const params = lambda._params(program)
assert.isNull(params.DeadLetterConfig.TargetArn)
})
it('appends TracingConfig to params when params set', () => {
program.tracingConfig = 'Active'
const params = lambda._params(program)
assert.equal(params.TracingConfig.Mode, 'Active')
})
it('does not append TracingConfig when params are not set', () => {
program.tracingConfig = ''
const params = lambda._params(program)
assert.isNull(params.TracingConfig.Mode)
})
it('appends Layers to params when params set', () => {
program.layers = 'Layer1,Layer2'
const params = lambda._params(program)
assert.deepEqual(params.Layers, ['Layer1', 'Layer2'])
})
it('does not append Layers when params are not set', () => {
program.layers = ''
const params = lambda._params(program)
assert.deepEqual(params.Layers, [])
})
describe('S3 deploy', () => {
it('Do not use S3 deploy', () => {
const params = lambda._params(program, 'Buffer')
assert.deepEqual(
params.Code,
{ ZipFile: 'Buffer' }
)
})
it('Use S3 deploy', () => {
const params = lambda._params(Object.assign({ deployUseS3: true }, program), 'Buffer')
assert.deepEqual(
params.Code,
{
S3Bucket: null,
S3Key: null
}
)
})
})
describe('PackageType: Zip|Image', () => {
it('PackageType: Zip', () => {
const params = lambda._params(program, 'Buffer')
assert.equal(params.PackageType, 'Zip')
assert.deepEqual(
params.Code,
{ ZipFile: 'Buffer' }
)
})
it('PackageType: Image', () => {
program.imageUri = 'xxx'
const params = lambda._params(program, 'Buffer')
assert.equal(params.PackageType, 'Image')
assert.isUndefined(params.Handler)
assert.isUndefined(params.Runtime)
assert.isUndefined(params.KMSKeyArn)
assert.deepEqual(
params.Code,
{ ImageUri: 'xxx' }
)
})
})
describe('params.Publish', () => {
describe('boolean', () => {
it('If true, it is set to true', () => {
program.publish = true
const params = lambda._params(program)
assert.isTrue(params.Publish)
})
it('If false, it is set to false', () => {
program.publish = false
const params = lambda._params(program)
assert.isFalse(params.Publish)
})
})
describe('string', () => {
it('If "true", it is set to true', () => {
program.publish = 'true'
const params = lambda._params(program)
assert.isTrue(params.Publish)
})
it('If not "true", it is set to false', () => {
program.publish = 'false'
assert.isFalse(lambda._params(program).Publish)
program.publish = 'aaa'
assert.isFalse(lambda._params(program).Publish)
})
})
})
describe('configFile', () => {
beforeEach(() => {
// Prep...
fs.writeFileSync('tmp.env', 'FOO=bar\nBAZ=bing\n')
fs.writeFileSync('empty.env', '')
})
afterEach(() => {
fs.unlinkSync('tmp.env')
fs.unlinkSync('empty.env')
})
it('adds variables when configFile param is set', () => {
program.configFile = 'tmp.env'
const params = lambda._params(program)
assert.equal(params.Environment.Variables.FOO, 'bar')
assert.equal(params.Environment.Variables.BAZ, 'bing')
})
it('when configFile param is set but it is an empty file', () => {
program.configFile = 'empty.env'
const params = lambda._params(program)
assert.equal(Object.keys(params.Environment.Variables).length, 0)
})
it('does not add when configFile param is not set', () => {
const params = lambda._params(program)
assert.isNull(params.Environment.Variables)
})
})
})
describe('_cleanDirectory', () => {
it('`codeDirectory` is empty', () => {
return lambda._cleanDirectory(codeDirectory).then(() => {
assert.isTrue(fs.existsSync(codeDirectory))
const contents = fs.readdirSync(codeDirectory)
assert.equal(contents.length, 0)
})
})
it('`codeDirectory` is empty. (For `codeDirectory` where the file was present)', () => {
return lambda._fileCopy(program, '.', codeDirectory, true).then(() => {
const contents = fs.readdirSync(codeDirectory)
assert.isTrue(contents.length > 0)
return lambda._cleanDirectory(codeDirectory).then(() => {
assert.isTrue(fs.existsSync(codeDirectory))
const contents = fs.readdirSync(codeDirectory)
assert.equal(contents.length, 0)
})
})
})
})
describe('_fileCopy', () => {
before(() => {
fs.mkdirsSync(path.join('__unittest', 'hoge'))
fs.mkdirsSync(path.join('__unittest', 'fuga'))
fs.writeFileSync(path.join('__unittest', 'hoge', 'piyo'), '')
fs.writeFileSync(path.join('__unittest', 'hoge', 'package.json'), '')
fs.writeFileSync('fuga', '')
})
after(() => {
['fuga', '__unittest'].forEach((path) => {
fs.removeSync(path)
})
})
beforeEach(() => lambda._cleanDirectory(codeDirectory))
it('_fileCopy an index.js as well as other files', () => {
return lambda._fileCopy(program, '.', codeDirectory, true).then(() => {
const contents = fs.readdirSync(codeDirectory);
['index.js', 'package.json'].forEach((needle) => {
assert.include(contents, needle, `Target: "${needle}"`)
});
['node_modules', 'build'].forEach((needle) => {
assert.notInclude(contents, needle, `Target: "${needle}"`)
})
})
})
describe('when there are excluded files', () => {
beforeEach((done) => {
// *main* => lib/main.js
// In case of specifying files under the directory with wildcards
program.excludeGlobs = [
'*.png',
'test',
'*main*',
path.join('__unittest', 'hoge', '*'),
path.join('fuga', path.sep)
].join(' ')
done()
})
it('_fileCopy an index.js as well as other files', () => {
return lambda._fileCopy(program, '.', codeDirectory, true).then(() => {
const contents = fs.readdirSync(codeDirectory);
['index.js', 'package.json'].forEach((needle) => {
assert.include(contents, needle, `Target: "${needle}"`)
})
})
})
it('_fileCopy excludes files matching excludeGlobs', () => {
return lambda._fileCopy(program, '.', codeDirectory, true).then(() => {
let contents = fs.readdirSync(codeDirectory);
['__unittest', 'fuga'].forEach((needle) => {
assert.include(contents, needle, `Target: "${needle}"`)
});
['node-lambda.png', 'test'].forEach((needle) => {
assert.notInclude(contents, needle, `Target: "${needle}"`)
})
contents = fs.readdirSync(path.join(codeDirectory, 'lib'))
assert.notInclude(contents, 'main.js', 'Target: "lib/main.js"')
contents = fs.readdirSync(path.join(codeDirectory, '__unittest'))
assert.include(contents, 'hoge', 'Target: "__unittest/hoge"')
assert.notInclude(contents, 'fuga', 'Target: "__unittest/fuga"')
contents = fs.readdirSync(path.join(codeDirectory, '__unittest', 'hoge'))
assert.equal(contents.length, 0, 'directory:__unittest/hoge is empty')
})
})
it('_fileCopy should not exclude package.json, even when excluded by excludeGlobs', () => {
program.excludeGlobs = '*.json'
return lambda._fileCopy(program, '.', codeDirectory, true).then(() => {
const contents = fs.readdirSync(codeDirectory)
assert.include(contents, 'package.json')
})
})
it('_fileCopy should not exclude package-lock.json, even when excluded by excludeGlobs', () => {
program.excludeGlobs = '*.json'
return lambda._fileCopy(program, '.', codeDirectory, true).then(() => {
const contents = fs.readdirSync(codeDirectory)
assert.include(contents, 'package-lock.json')
})
})
it('_fileCopy should not include package.json when --prebuiltDirectory is set', () => {
const buildDir = '.build_' + Date.now()
after(() => fs.removeSync(buildDir))
fs.mkdirSync(buildDir)
fs.writeFileSync(path.join(buildDir, 'testa'), '')
fs.writeFileSync(path.join(buildDir, 'package.json'), '')
program.excludeGlobs = '*.json'
program.prebuiltDirectory = buildDir
return lambda._fileCopy(program, buildDir, codeDirectory, true).then(() => {
const contents = fs.readdirSync(codeDirectory)
assert.notInclude(contents, 'package.json', 'Target: "packages.json"')
assert.include(contents, 'testa', 'Target: "testa"')
})
})
})
})
describe('_shouldUseNpmCi', () => {
beforeEach(() => {
return lambda._cleanDirectory(codeDirectory)
})
describe('when package-lock.json exists', () => {
beforeEach(() => {
fs.writeFileSync(path.join(codeDirectory, 'package-lock.json'), JSON.stringify({}))
})
it('returns true', () => {
assert.isTrue(lambda._shouldUseNpmCi(codeDirectory))
})
})
describe('when package-lock.json does not exist', () => {
beforeEach(() => {
fs.removeSync(path.join(codeDirectory, 'package-lock.json'))
})
it('returns false', () => {
assert.isFalse(lambda._shouldUseNpmCi(codeDirectory))
})
})
})
describe('_getNpmInstallCommand', () => {
describe('when package-lock.json exists', () => {
const codeDirectory = '.'
it('npm ci', () => {
const { packageManager, installOptions } = lambda._getNpmInstallCommand(program, codeDirectory)
assert.equal(packageManager, 'npm')
assert.deepEqual(installOptions, ['-s', 'ci', '--production', '--no-audit', '--prefix', codeDirectory])
})
it('npm ci with "--no-optional"', () => {
const { packageManager, installOptions } = lambda._getNpmInstallCommand(
{
...program,
optionalDependencies: false
},
codeDirectory
)
assert.equal(packageManager, 'npm')
assert.deepEqual(
installOptions,
['-s', 'ci', '--production', '--no-audit', '--no-optional', '--prefix', codeDirectory]
)
})
it('npm ci on docker', () => {
const { packageManager, installOptions } = lambda._getNpmInstallCommand(
{
...program,
dockerImage: 'test'
},
codeDirectory
)
assert.equal(packageManager, 'npm')
assert.deepEqual(installOptions, ['-s', 'ci', '--production', '--no-audit'])
})
})
describe('when package-lock.json does not exist', () => {
const codeDirectory = './test'
it('npm install', () => {
const { packageManager, installOptions } = lambda._getNpmInstallCommand(program, './test')
assert.equal(packageManager, 'npm')
assert.deepEqual(installOptions, ['-s', 'install', '--production', '--no-audit', '--prefix', './test'])
})
it('npm install with "--no-optional"', () => {
const { packageManager, installOptions } = lambda._getNpmInstallCommand(
{
...program,
optionalDependencies: false
},
codeDirectory
)
assert.equal(packageManager, 'npm')
assert.deepEqual(
installOptions,
['-s', 'install', '--production', '--no-audit', '--no-optional', '--prefix', codeDirectory]
)
})
it('npm install on docker', () => {
const { packageManager, installOptions } = lambda._getNpmInstallCommand(
{
...program,
dockerImage: 'test'
},
codeDirectory
)
assert.equal(packageManager, 'npm')
assert.deepEqual(installOptions, ['-s', 'install', '--production', '--no-audit'])
})
})
})
describe('_getYarnInstallCommand', () => {
const codeDirectory = '.'
it('yarn install', () => {
const { packageManager, installOptions } = lambda._getYarnInstallCommand(program, codeDirectory)
assert.equal(packageManager, 'yarn')
assert.deepEqual(installOptions, ['-s', 'install', '--production', '--cwd', codeDirectory])
})
it('yarn install with "--no-optional"', () => {
const { packageManager, installOptions } = lambda._getYarnInstallCommand(
{
...program,
optionalDependencies: false
},
codeDirectory
)
assert.equal(packageManager, 'yarn')
assert.deepEqual(
installOptions,
['-s', 'install', '--production', '--ignore-optional', '--cwd', codeDirectory]
)
})
it('yarn install on docker', () => {
const { packageManager, installOptions } = lambda._getYarnInstallCommand(
{
...program,
dockerImage: 'test'
},
codeDirectory
)
assert.equal(packageManager, 'yarn')
assert.deepEqual(installOptions, ['-s', 'install', '--production'])
})
})
describe('_packageInstall', function () {
_timeout({ this: this, sec: 60 }) // ci should be faster than install
// npm treats files as packages when installing, and so removes them.
// Test with `devDependencies` packages that are not installed with the `--production` option.
const nodeModulesMocha = path.join(codeDirectory, 'node_modules', 'chai')
const testCleanAndInstall = async (packageManager) => {
const beforeDotenvStat = fs.statSync(path.join(codeDirectory, 'node_modules', 'dotenv'))
const usedPackageManager = await lambda._packageInstall(
{
...program,
packageManager
},
codeDirectory
)
assert.equal(usedPackageManager, packageManager)
const contents = fs.readdirSync(path.join(codeDirectory, 'node_modules'))
assert.include(contents, 'dotenv')
// To remove and then install.
// beforeDotenvStat.ctimeMs < afterDotenvStat.ctimeMs
const afterDotenvStat = fs.statSync(path.join(codeDirectory, 'node_modules', 'dotenv'))
assert.isBelow(beforeDotenvStat.ctimeMs, afterDotenvStat.ctimeMs)
// Not installed with the `--production` option.
assert.isFalse(fs.existsSync(nodeModulesMocha))
}
const beforeEachOptionalDependencies = () => {
const packageJsonPath = path.join(codeDirectory, 'package.json')
const packageJson = require(packageJsonPath)
packageJson.optionalDependencies = { commander: '*' }
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson))
// Remove package-lock.json because it does not match the package.json to which optionalDependencies was added.
fs.removeSync(path.join(codeDirectory, 'package-lock.json'))
}
const testOptionalDependenciesIsInstalled = async (packageManager) => {
const usedPackageManager = await lambda._packageInstall(
{
...program,
packageManager
},
codeDirectory
)
assert.equal(usedPackageManager, packageManager)
const contents = fs.readdirSync(path.join(codeDirectory, 'node_modules'))
assert.include(contents, 'commander')
}
const testOptionalDependenciesIsNotInstalled = async (packageManager) => {
const params = {
...program,
packageManager,
optionalDependencies: false
}
const usedPackageManager = await lambda._packageInstall(params, codeDirectory)
assert.equal(usedPackageManager, packageManager)
const contents = fs.readdirSync(path.join(codeDirectory, 'node_modules'))
assert.notInclude(contents, 'npm')
}
beforeEach(async () => {
await lambda._cleanDirectory(codeDirectory)
await lambda._fileCopy(program, sourceDirectoryForTest, codeDirectory, false)
})
describe('Use npm', () => {
describe('when package-lock.json does exist', () => {
it('should use "npm ci"', () => testCleanAndInstall('npm'))
})
describe('when package-lock.json does not exist', () => {
beforeEach(() => {
return fs.removeSync(path.join(codeDirectory, 'package-lock.json'))
})
it('should use "npm install"', () => {
const beforeDotenvStat = fs.statSync(path.join(codeDirectory, 'node_modules', 'dotenv'))
return lambda._packageInstall(program, codeDirectory).then((usedPackageManager) => {
assert.equal(usedPackageManager, 'npm')
const contents = fs.readdirSync(path.join(codeDirectory, 'node_modules'))
assert.include(contents, 'dotenv')
// Installed packages will remain intact.
// beforeDotenvStat.ctimeMs === afterDotenvStat.ctimeMs
const afterDotenvStat = fs.statSync(path.join(codeDirectory, 'node_modules', 'dotenv'))
assert.equal(beforeDotenvStat.ctimeMs, afterDotenvStat.ctimeMs)
// Not installed with the `--production` option.
assert.isFalse(fs.existsSync(nodeModulesMocha))
})
})
})
describe('optionalDependencies', () => {
beforeEach(beforeEachOptionalDependencies)
describe('No `--no-optionalDependencies`', () => {
it('optionalDependencies is installed', () => testOptionalDependenciesIsInstalled('npm'))
})
describe('With `--no-optionalDependencies`', () => {
it('optionalDependency is NOT installed', () => testOptionalDependenciesIsNotInstalled('npm'))
})
})
})
describe('Use yarn', () => {
it('should use "yarn install"', () => testCleanAndInstall('yarn'))
describe('optionalDependencies', () => {
beforeEach(beforeEachOptionalDependencies)
describe('No `--ignore-optionalDependencies`', () => {
it('optionalDependencies is installed', () => testOptionalDependenciesIsInstalled('yarn'))
})
describe('With `--ignore-optionalDependencies`', () => {
it('optionalDependency is NOT installed', () => testOptionalDependenciesIsNotInstalled('yarn'))
})
})
})
})
describe('_packageInstall (When codeDirectory contains characters to be escaped)', function () {
_timeout({ this: this, sec: 30 }) // give it time to build the node modules
beforeEach(() => {
// Since '\' can not be included in the file or directory name in Windows
const directoryName = process.platform === 'win32'
? 'hoge fuga\' piyo'
: 'hoge "fuga\' \\piyo'
codeDirectory = path.join(os.tmpdir(), directoryName)
return lambda._cleanDirectory(codeDirectory).then(() => {
return lambda._fileCopy(program, '.', codeDirectory, true)
})
})
afterEach(() => {
fs.removeSync(codeDirectory)
codeDirectory = lambda._codeDirectory()
})
const testFunc = async (packageManager) => {
const usedPackageManager = await lambda._packageInstall(
{
...program,
packageManager
},
codeDirectory
)
assert.equal(usedPackageManager, packageManager)
const contents = fs.readdirSync(codeDirectory)
assert.include(contents, 'node_modules')
}
it('npm adds node_modules', () => testFunc('npm'))
it('yarn adds node_modules', () => testFunc('yarn'))
})
describe('_postInstallScript', () => {
if (process.platform === 'win32') {
return it('`_postInstallScript` test does not support Windows.')
}
const postInstallScriptPath = path.join(codeDirectory, 'post_install.sh')
let hook
/**
* Capture console output
*/
const captureStream = function (stream) {
const oldWrite = stream.write
let buf = ''
stream.write = function (chunk, encoding, callback) {
buf += chunk.toString() // chunk is a String or Buffer
oldWrite.apply(stream, arguments)
}
return {
unhook: () => {
stream.write = oldWrite
},
captured: () => buf
}
}
beforeEach(() => {
hook = captureStream(process.stdout)
})
afterEach(() => {
hook.unhook()
if (fs.existsSync(postInstallScriptPath)) {
fs.unlinkSync(postInstallScriptPath)
}
})
it('should not throw any errors if no script', () => {
return lambda._postInstallScript(program, codeDirectory).then((dummy) => {
assert.isUndefined(dummy)
})
})
it('should throw any errors if script fails', () => {
fs.writeFileSync(postInstallScriptPath, '___fails___')
return lambda._postInstallScript(program, codeDirectory).catch((err) => {
assert.instanceOf(err, Error)
assert.match(err.message, /^Error: Command failed:/)
})
})
it('running script gives expected output', () => {
fs.writeFileSync(
postInstallScriptPath,
fs.readFileSync(path.join('test', 'post_install.sh'))
)
fs.chmodSync(path.join(codeDirectory, 'post_install.sh'), '755')
return lambda._postInstallScript(program, codeDirectory).then((dummy) => {
assert.isUndefined(dummy)
}).catch((err) => {
assert.isNull(err)
assert.equal(
`=> Running post install script post_install.sh\n\t\tYour environment is ${program.environment}\n`,
hook.captured()
)
})
})
})
describe('_zip', function () {
_timeout({ this: this, sec: 60 }) // give it time to zip
const beforeTask = async (packageManager) => {
await lambda._cleanDirectory(codeDirectory)
await lambda._fileCopy(program, sourceDirectoryForTest, codeDirectory, true)
fs.copySync(path.join(__dirname, '..', 'bin', 'node-lambda'), path.join(codeDirectory, 'bin', 'node-lambda'))
const usedPackageManager = await lambda._packageInstall(
{
...program,
packageManager
},
codeDirectory
)
assert.equal(usedPackageManager, packageManager)
if (process.platform !== 'win32') {
fs.symlinkSync(
path.join(__dirname, '..', 'bin', 'node-lambda'),
path.join(codeDirectory, 'node-lambda-link')
)
}
}
const testFunc = async (packageManager) => {
// setup
await beforeTask(packageManager)
// tests
const data = await lambda._zip(program, codeDirectory)
const archive = new Zip(data)
assert.include(archive.files['index.js'].name, 'index.js')
assert.include(archive.files['bin/node-lambda'].name, 'bin/node-lambda')
if (process.platform !== 'win32') {
const indexJsStat = fs.lstatSync('index.js')
const binNodeLambdaStat = fs.lstatSync(path.join('bin', 'node-lambda'))
assert.equal(
archive.files['index.js'].unixPermissions,
indexJsStat.mode
)
assert.equal(
archive.files['bin/node-lambda'].unixPermissions,
binNodeLambdaStat.mode
)
// isSymbolicLink
assert.include(archive.files['node-lambda-link'].name, 'node-lambda-link')
assert.equal(
archive.files['node-lambda-link'].unixPermissions & fs.constants.S_IFMT,
fs.constants.S_IFLNK
)
}
}
describe('Use npm', () => {
it(
'Compress the file. `index.js` and `bin/node-lambda` are included and the permission is also preserved.',
() => testFunc('npm')
)
})
describe('Use yarn', () => {
it(
'Compress the file. `index.js` and `bin/node-lambda` are included and the permission is also preserved.',
() => testFunc('yarn')
)
})
})
describe('_archive', () => {
// archive.files's name is a slash delimiter regardless of platform.
it('installs and zips with an index.js file and node_modules/dotenv (It is also a test of `_buildAndArchive`)', function () {
_timeout({ this: this, sec: 30 }) // give it time to zip
return lambda._archive({ ...program, sourceDirectory: sourceDirectoryForTest }).then((data) => {
const archive = new Zip(data)
const contents = Object.keys(archive.files).map((k) => {
return archive.files[k].name.toString()
})
assert.include(contents, 'index.js')
assert.include(contents, 'node_modules/dotenv/lib/main.js')
})
})
it('packages a prebuilt module without installing (It is also a test of `_archivePrebuilt`)', function () {
_timeout({ this: this, sec: 30 }) // give it time to zip
const buildDir = '.build_' + Date.now()
after(() => fs.removeSync(buildDir))
fs.mkdirSync(buildDir)
fs.mkdirSync(path.join(buildDir, 'd'))
fs.mkdirSync(path.join(buildDir, 'node_modules'))
fs.writeFileSync(path.join(buildDir, 'node_modules', 'a'), '...')
fs.writeFileSync(path.join(buildDir, 'testa'), '...')
fs.writeFileSync(path.join(buildDir, 'd', 'testb'), '...')
program.prebuiltDirectory = buildDir
return lambda._archive(program).then((data) => {
const archive = new Zip(data)
const contents = Object.keys(archive.files).map((k) => {
return archive.files[k].name.toString()
});
[
'testa',
'd/testb',
'node_modules/a'
].forEach((needle) => {
assert.include(contents, needle, `Target: "${needle}"`)
})
})
})
it('cleans the temporary directory before running `_archivePrebuilt`', function () {
_timeout({ this: this, sec: 30 }) // give it time to zip
const buildDir = '.build_' + Date.now()
const codeDir = lambda._codeDirectory()
const tmpFile = path.join(codeDir, 'deleteme')
after(() => fs.removeSync(buildDir))
fs.mkdirSync(codeDir, { recursive: true })
fs.writeFileSync(tmpFile, '...')
fs.mkdirSync(buildDir)
fs.writeFileSync(path.join(buildDir, 'test'), '...')
program.prebuiltDirectory = buildDir
return lambda._archive(program).then((_data) => {
assert.isNotTrue(fs.existsSync(tmpFile))
})
})
})
describe('_readArchive', () => {
const testZipFile = path.join(os.tmpdir(), 'node-lambda-test.zip')
let bufferExpected = null
before(function () {
_timeout({ this: this, sec: 30 }) // give it time to zip
return lambda._zip(program, codeDirectory).then((data) => {
bufferExpected = data
fs.writeFileSync(testZipFile, data)
})
})
after(() => fs.unlinkSync(testZipFile))
it('_readArchive fails (undefined)', () => {
return lambda._readArchive(program).then((data) => {
assert.isUndefined(data)
}).catch((err) => {
assert.instanceOf(err, Error)
assert.equal(err.message, 'No such Zipfile [undefined]')
})
})
it('_readArchive fails (does not exists file)', () => {
const filePath = path.join(path.resolve('/aaaa'), 'bbbb')
const _program = Object.assign({ deployZipfile: filePath }, program)
return lambda._readArchive(_program).then((data) => {
assert.isUndefined(data)
}).catch((err) => {
assert.instanceOf(err, Error)
assert.equal(err.message, `No such Zipfile [${filePath}]`)
})
})
it('_readArchive reads the contents of the zipfile', () => {
const _program = Object.assign({ deployZipfile: testZipFile }, program)
return lambda._readArchive(_program).then((data) => {
assert.deepEqual(data, bufferExpected)
})
})
describe('If value is set in `deployZipfile`, _readArchive is executed in _archive', () => {
it('`deployZipfile` is a invalid value. Process from creation of zip file', function () {
const filePath = path.join(path.resolve('/aaaa'), 'bbbb')
const _program = {
program,
deployZipfile: filePath,
sourceDirectory: sourceDirectoryForTest
}
_timeout({ this: this, sec: 30 }) // give it time to zip
return lambda._archive(_program).then((data) => {
// same test as "installs and zips with an index.js file and node_modules/dotenv"
const archive = new Zip(data)
const contents = Object.keys(archive.files).map((k) => {
return archive.files[k].name.toString()
})
assert.include(contents, 'index.js')
assert.include(contents, 'node_modules/dotenv/lib/main.js')
})
})
it('`deployZipfile` is a valid value._archive reads the contents of the zipfile', () => {
const _program = Object.assign({ deployZipfile: testZipFile }, program)
return lambda._archive(_program).then((data) => {
assert.deepEqual(data, bufferExpected)
})
})
})
})
describe('environment variable injection at runtime', () => {
beforeEach(() => {
// Prep...
fs.writeFileSync('tmp.env', 'FOO=bar\nBAZ=bing\n')
})
afterEach(() => fs.unlinkSync('tmp.env'))
it('should inject environment variables at runtime', () => {
// Run it...
lambda._setRunTimeEnvironmentVars({
configFile: 'tmp.env'
}, process.cwd())
assert.equal(process.env.FOO, 'bar')
assert.equal(process.env.BAZ, 'bing')
})
})
describe('create sample files', () => {
const targetFiles = [
'.env',
'context.json',
'event.json',
'deploy.env',
'event_sources.json'
]
after(() => {
targetFiles.forEach((file) => fs.unlinkSync(file))
program.eventSourceFile = ''
})
it('should create sample files', () => {
lambda.setup(program)
const libPath = path.join(__dirname, '..', 'lib')
targetFiles.forEach((targetFile) => {
const boilerplateFile = path.join(libPath, `${targetFile}.example`)
assert.equal(
fs.readFileSync(targetFile).toString(),
fs.readFileSync(boilerplateFile).toString(),
targetFile
)
})
})
describe('_eventSourceList', () => {
it('program.eventSourceFile is empty value', () => {
program.eventSourceFile = ''
assert.deepEqual(
lambda._eventSourceList(program),
{
EventSourceMappings: null,
ScheduleEvents: null,
S3Events: null
}
)
})
it('program.eventSourceFile is invalid value', () => {
const dirPath = path.join(path.resolve('/hoge'), 'fuga')
program.eventSourceFile = dirPath
assert.throws(
() => { lambda._eventSourceList(program) },
Error,
`ENOENT: no such file or directory, open '${dirPath}'`
)
})
describe('program.eventSourceFile is valid value', () => {
before(() => {
fs.writeFileSync('only_EventSourceMappings.json', JSON.stringify({
EventSourceMappings: [{ test: 1 }]
}))
fs.writeFileSync('only_ScheduleEvents.json', JSON.stringify({
ScheduleEvents: [{ test: 2 }]
}))
fs.writeFileSync('only_S3Events.json', JSON.stringify({
S3Events: [{ test: 3 }]
}))
})
after(() => {
fs.unlinkSync('only_EventSourceMappings.json')
fs.unlinkSync('only_ScheduleEvents.json')
fs.unlinkSync('only_S3Events.json')
})
it('only EventSourceMappings', () => {
program.eventSourceFile = 'only_EventSourceMappings.json'
const expected = {
EventSourceMappings: [{ test: 1 }],
ScheduleEvents: [],
S3Events: []
}
assert.deepEqual(lambda._eventSourceList(program), expected)
})
it('only ScheduleEvents', () => {
program.eventSourceFile = 'only_ScheduleEvents.json'
const expected = {
EventSourceMappings: [],
ScheduleEvents: [{ test: 2 }],
S3Events: []
}
assert.deepEqual(lambda._eventSourceList(program), expected)
})
it('only S3Events', () => {
program.eventSourceFile = 'only_S3Events.json'
const expected = {
EventSourceMappings: [],
ScheduleEvents: [],
S3Events: [{ test: 3 }]
}
assert.deepEqual(lambda._eventSourceList(program), expected)
})
it('EventSourceMappings & ScheduleEvents', () => {
program.eventSourceFile = 'event_sources.json'
const expected = {
EventSourceMappings: [{
BatchSize: 100,
Enabled: true,
EventSourceArn: 'your event source arn',
StartingPosition: 'LATEST'
}],
ScheduleEvents: [{
ScheduleName: 'node-lambda-test-schedule',
ScheduleState: 'ENABLED',
ScheduleExpression: 'rate(1 hour)',
Input: {
key1: 'value',
key2: 'value'
}
}],
S3Events: [{
Bucket: 'BUCKET_NAME',
Events: [
's3:ObjectCreated:*'
],
Filter: {
Key: {
FilterRules: [{
Name: 'prefix',
Value: 'STRING_VALUE'
}]
}
}
}]
}
assert.deepEqual(lambda._eventSourceList(program), expected)
})
})
describe('old style event_sources.json', () => {
const oldStyleValue = [{
BatchSize: 100,
Enabled: true,
EventSourceArn: 'your event source arn',
StartingPosition: 'LATEST'
}]
const fileName = 'event_sources_old_style.json'
before(() => fs.writeFileSync(fileName, JSON.stringify(oldStyleValue)))
after(() => fs.unlinkSync(fileName))
it('program.eventSourceFile is valid value', () => {
program.eventSourceFile = fileName
const expected = {
EventSourceMappings: oldStyleValue,
ScheduleEvents: [],
S3Events: []
}
assert.deepEqual(lambda._eventSourceList(program), expected)
})
})
})
})
describe('_listEventSourceMappings', () => {
it('simple test with mock', () => {
return lambda._listEventSourceMappings(
awsLambda,
{ FunctionName: 'test-func' }
).then((results) => {
assert.deepEqual(
results,
lambdaMockSettings.listEventSourceMappings.EventSourceMappings
)
})
})
})
describe('_getStartingPosition', () => {
it('null in SQS', () => {
assert.isNull(lambda._getStartingPosition({
EventSourceArn: 'arn:aws:sqs:us-east-1:sqs-queuename1'
}))
})
it('When there is no setting', () => {
assert.equal(
lambda._getStartingPosition({
EventSourceArn: 'arn:aws:kinesis:test'
}),
'LATEST'
)
})
it('With StartingPosition', () => {
assert.equal(
lambda._getStartingPosition({
EventSourceArn: 'arn:aws:kinesis:test',
StartingPosition: 'test position'
}),
'test position'
)
})
})
describe('_updateEventSources', () => {
const eventSourcesJsonValue = {
EventSourceMappings: [{
EventSourceArn: lambdaMockSettings
.listEventSourceMappings
.EventSourceMappings[0]
.EventSourceArn,
StartingPosition: 'LATEST',
BatchSize: 100,
Enabled: true
}]
}
before(() => {
fs.writeFileSync(
'event_sources.json',
JSON.stringify(eventSourcesJsonValue)
)
})
after(() => fs.unlinkSync('event_sources.json'))
it('program.eventSourceFile is empty value', () => {
program.eventSourceFile = ''
const eventSourceList = lambda._eventSourceList(program)
return lambda._updateEventSources(
awsLambda,
'',
[],
eventSourceList.EventSourceMappings
).then((results) => {
assert.deepEqual(results, [])
})
})
it('simple test with mock (In case of new addition)', () => {
program.eventSourceFile = 'event_sources.json'
const eventSourceList = lambda._eventSourceList(program)
return lambda._updateEventSources(
awsLambda,
'functionName',
[],
eventSourceList.EventSourceMappings
).then((results) => {
assert.deepEqual(results, [lambdaMockSettings.createEventSourceMapping])
})
})
it('simple test with mock (In case of deletion)', () => {
return lambda._updateEventSources(
awsLambda,
'functionName',
lambdaMockSettings.listEventSourceMappings.EventSourceMappings,
{}
).then((results) => {
assert.deepEqual(results, [lambdaMockSettings.deleteEventSourceMapping])
})
})
it('simple test with mock (In case of update)', () => {
program.eventSourceFile = 'event_sources.json'
const eventSourceList = lambda._eventSourceList(program)
return lambda._updateEventSources(
awsLambda,
'functionName',
lambdaMockSettings.listEventSourceMappings.EventSourceMappings,
eventSourceList.EventSourceMappings
).then((results) => {
assert.deepEqual(results, [lambdaMockSettings.updateEventSourceMapping])
})
})
})
describe('_updateScheduleEvents', () => {
const ScheduleEvents = require(path.join('..', 'lib', 'schedule_events'))
const eventSourcesJsonValue = {
ScheduleEvents: [{
ScheduleName: 'node-lambda-test-schedule',
ScheduleState: 'ENABLED',
ScheduleExpression: 'rate(1 hour)',
ScheduleDescription: 'Run node-lambda-test-function once per hour'
}]
}
let schedule = null
before(() => {
fs.writeFileSync(
'event_sources.json',
JSON.stringify(eventSourcesJsonValue)
)
schedule = new ScheduleEvents(aws)
})
after(() => fs.unlinkSync('event_sources.json'))
it('program.eventSourceFile is empty value', () => {
program.eventSourceFile = ''
const eventSourceList = lambda._eventSourceList(program)
return lambda._updateScheduleEvents(
schedule,
'',
eventSourceList.ScheduleEvents
).then((results) => {
assert.deepEqual(results, [])
})
})
it('simple test with mock', () => {
program.eventSourceFile = 'event_sources.json'
const eventSourceList = lambda._eventSourceList(program)
const functionArn = 'arn:aws:lambda:us-west-2:XXX:function:node-lambda-test-function'
return lambda._updateScheduleEvents(
schedule,
functionArn,
eventSourceList.ScheduleEvents
).then((results) => {
const expected = [Object.assign(
eventSourcesJsonValue.ScheduleEvents[0],
{ FunctionArn: functionArn }
)]
assert.deepEqual(results, expected)
})
})
})
describe('_updateS3Events', () => {
const S3Events = require(path.join('..', 'lib', 's3_events'))
const eventSourcesJsonValue = {
S3Events: [{
Bucket: 'node-lambda