Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix awsProvider.js : "Cannot use 'in' operator to search for '0' #5688

Merged
merged 1 commit into from
Jan 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 18 additions & 0 deletions lib/classes/Service.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const _ = require('lodash');
const BbPromise = require('bluebird'); const BbPromise = require('bluebird');
const semver = require('semver'); const semver = require('semver');


const validAPIGatewayStageNamePattern = /^[-_a-zA-Z0-9]+$/;

class Service { class Service {
constructor(serverless, data) { constructor(serverless, data) {
// ####################################################################### // #######################################################################
Expand Down Expand Up @@ -230,6 +232,22 @@ class Service {
}; };
warnOnDuplicateHandlers(); warnOnDuplicateHandlers();


const provider = this.serverless.getProvider('aws');
if (provider) {
const stage = provider.getStage();
this.getAllFunctions().forEach(funcName => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to use BbPromise.each function instead of forEach ? We're currently enforcing the policy that all new code should be as async as possible since sync calls are on of the roots of the sloweness of the Framework.

Copy link
Contributor Author

@exoego exoego Jan 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it can be async technically.
But I think it is premature optimization and not necessary for this case.

Both Service#getAllFunctions and Service#getAllEventsInFunction used in validation are just iterating over functions (and its events).
I assume number of those are several dozens in most cases, and thousands in very extreme situation.
I believe Node.js is fast enough for such number of elements.

And, other validations in Service#validate are also iterating over functions synchronousely.
I can make them all asynchronously, but it requires changes a lot in existing tests.
I think it is better to optimize in another PR when users complains.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@exoego
OK 👍 Thank you for considering it 😄

_.forEach(this.getAllEventsInFunction(funcName), event => {
if (_.has(event, 'http') && !validAPIGatewayStageNamePattern.test(stage)) {
throw new this.serverless.classes.Error([
`Invalid stage name ${stage}:`,
'it should contains only [-_a-zA-Z0-9] for AWS provider if http event are present',
'according to API Gateway limitation.',
].join(' '));
}
});
});
}

return this; return this;
} }


Expand Down
135 changes: 135 additions & 0 deletions lib/classes/Service.test.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -763,6 +763,141 @@ describe('Service', () => {
expect(consoleLogStub.callCount).to.equal(2); expect(consoleLogStub.callCount).to.equal(2);
}); });
}); });

describe('stage name validation', () => {
function simulateRun(serverless) {
Copy link
Contributor Author

@exoego exoego Jan 16, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: simulateRun is a simplified emulation of Serverless#run.
This is a bit tricky but required to populate function names, I think.

return serverless.init().then(() =>
serverless.variables.populateService(serverless.pluginManager.cliOptions)
.then(() => {
serverless.service.mergeArrays();
serverless.service.setFunctionNames(serverless.processedInput.options);
}));
}

it(`should not throw an error if http event is absent and
stage contains only alphanumeric, underscore and hyphen`, () => {
const SUtils = new Utils();
const serverlessYml = {
service: 'new-service',
provider: {
name: 'aws',
stage: 'xyz-101_abc-123',
},
functions: {
first: {
events: [],
},
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'),
YAML.dump(serverlessYml));

const serverless = new Serverless({ servicePath: tmpDirPath });
return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => {
expect(() => serverless.service.validate()).to.not.throw(serverless.classes.Error);
});
});

it(`should not throw an error after variable population if http event is present and
the populated stage contains only alphanumeric, underscore and hyphen`, () => {
const SUtils = new Utils();
const serverlessYml = {
service: 'new-service',
provider: {
name: 'aws',
stage: '${opt:stage, "default-stage"}',
},
functions: {
first: {
events: [
{
http: {
path: 'foo',
method: 'GET',
},
},
],
},
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'),
YAML.dump(serverlessYml));

const serverless = new Serverless({ servicePath: tmpDirPath });
return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => {
expect(() => serverless.service.validate()).to.not.throw(serverless.classes.Error);
});
});

it('should throw an error if http event is present and stage contains invalid chars', () => {
const SUtils = new Utils();
const serverlessYml = {
service: 'new-service',
provider: {
name: 'aws',
stage: 'my@stage',
},
functions: {
first: {
events: [
{
http: {
path: 'foo',
method: 'GET',
},
},
],
},
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'),
YAML.dump(serverlessYml));

const serverless = new Serverless({ servicePath: tmpDirPath });
return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => {
expect(() => serverless.service.validate()).to.throw(serverless.classes.Error, [
'Invalid stage name my@stage: it should contains only [-_a-zA-Z0-9]',
'for AWS provider if http event are present',
'according to API Gateway limitation.',
].join(' '));
});
});

it(`should throw an error after variable population
if http event is present and stage contains hyphen`, () => {
const SUtils = new Utils();
const serverlessYml = {
service: 'new-service',
provider: {
name: 'aws',
stage: '${opt:stage, "default:stage"}',
},
functions: {
first: {
events: [
{
http: {
path: 'foo',
method: 'GET',
},
},
],
},
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'),
YAML.dump(serverlessYml));

const serverless = new Serverless({ servicePath: tmpDirPath });
return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => {
expect(() => serverless.service.validate()).to.throw(serverless.classes.Error, [
'Invalid stage name default:stage: it should contains only [-_a-zA-Z0-9]',
'for AWS provider if http event are present',
'according to API Gateway limitation.',
].join(' '));
});
});
});
}); });


describe('#mergeArrays', () => { describe('#mergeArrays', () => {
Expand Down
15 changes: 0 additions & 15 deletions lib/classes/Variables.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ const fse = require('../utils/fs/fse');
const logWarning = require('./Error').logWarning; const logWarning = require('./Error').logWarning;
const PromiseTracker = require('./PromiseTracker'); const PromiseTracker = require('./PromiseTracker');


const validAPIGatewayStageNamePattern = /^[-_a-zA-Z0-9]+$/;

/** /**
* Maintainer's notes: * Maintainer's notes:
* *
Expand Down Expand Up @@ -110,19 +108,6 @@ class Variables {
this.populateValue(config.value, true) // populate this.populateValue(config.value, true) // populate
.then(populated => _.assign(config, { populated }))); .then(populated => _.assign(config, { populated })));
return this.assignProperties(provider, prepopulations); return this.assignProperties(provider, prepopulations);
}).then(() => {
const stage = provider.getStage();
this.serverless.service.getAllFunctions().forEach(funcName => {
_.forEach(this.serverless.service.getAllEventsInFunction(funcName), event => {
if (_.has(event, 'http') && !validAPIGatewayStageNamePattern.test(stage)) {
throw new this.serverless.classes.Error([
`Invalid stage name ${stage}:`,
'it should contains only [-_a-zA-Z0-9] for AWS provider if http event are present',
'according to API Gateway limitation.',
].join(' '));
}
});
});
}); });
} }
return BbPromise.resolve(); return BbPromise.resolve();
Expand Down
107 changes: 0 additions & 107 deletions lib/classes/Variables.test.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -207,113 +207,6 @@ describe('Variables', () => {
}); });
}); });
}); });
describe('stage name validation', () => {
it(`should not throw an error if http event is absent and
stage contains only alphanumeric, underscore and hyphen`, () => {
const serverlessYml = {
service: 'new-service',
provider: {
name: 'aws',
stage: 'xyz-101_abc-123',
},
functions: {
first: {
events: [],
},
},
};
serverless.service = new serverless.classes.Service(serverless, serverlessYml);

return serverless.variables.populateService().should.be.fullfilled;
});

it(`should not throw an error after variable population if http event is present and
the populated stage contains only alphanumeric, underscore and hyphen`, () => {
const serverlessYml = {
service: 'new-service',
provider: {
name: 'aws',
stage: '${opt:stage, "default-stage"}',
},
functions: {
first: {
events: [
{
http: {
path: 'foo',
method: 'GET',
},
},
],
},
},
};
serverless.service = new serverless.classes.Service(serverless, serverlessYml);

return serverless.variables.populateService().should.be.fullfilled;
});

it('should throw an error if http event is present and stage contains invalid chars', () => {
const serverlessYml = {
service: 'new-service',
provider: {
name: 'aws',
stage: 'my@stage',
},
functions: {
first: {
events: [
{
http: {
path: 'foo',
method: 'GET',
},
},
],
},
},
};
serverless.service = new serverless.classes.Service(serverless, serverlessYml);

return serverless.variables.populateService().should.be
.rejectedWith(serverless.classes.Error, [
'Invalid stage name my@stage: it should contains only [-_a-zA-Z0-9]',
'for AWS provider if http event are present',
'according to API Gateway limitation.',
].join(' '));
});

it(`should throw an error after variable population
if http event is present and stage contains hyphen`, () => {
const serverlessYml = {
service: 'new-service',
provider: {
name: 'aws',
stage: '${opt:stage, "default:stage"}',
},
functions: {
first: {
events: [
{
http: {
path: 'foo',
method: 'GET',
},
},
],
},
},
};
serverless.service = new serverless.classes.Service(serverless, serverlessYml);

return serverless.variables.populateService().should.be
.rejectedWith(serverless.classes.Error, [
'Invalid stage name default:stage: it should contains only [-_a-zA-Z0-9]',
'for AWS provider if http event are present',
'according to API Gateway limitation.',
].join(' '));
});
});
}); });


describe('#getProperties', () => { describe('#getProperties', () => {
Expand Down