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
Restricting alexaSkill functions to specific Alexa skills #4701
Conversation
- Adding support for multiple `alexaSkill` events on a single function (allows multiple Alexa Skills on a single lambda)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR :). Please have a look at my comments.
@@ -126,7 +126,7 @@ describe('extendedValidate', () => { | |||
return awsDeploy.extendedValidate().then(() => { | |||
expect(fileExistsSyncStub.calledTwice).to.equal(true); | |||
expect(readFileSyncStub.calledOnce).to.equal(true); | |||
expect(fileExistsSyncStub).to.have.been.calledWithExactly('artifact.zip'); | |||
expect(fileExistsSyncStub.calledWithExactly('artifact.zip')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You removed the check of the expectation here. This change will not evaluate the expectation result anymore.
The previous code was ok imo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
calledWithExactly
is a sinon function, not chai's. I think I'm missing here a .to.equal(true)
.
(side note - the 3 unrelated test files I changed were causing occasional failures in the test process. I added some links in the commit.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we use sinon-chai
which adds these extensions ;-) So the old one is correct and provides better error messages than just "expected true to equal false"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, it was missing in this particular file (also explains why I received TypeError: expect(...).to.have.been.calledWithExactly is not a function
when testing). I'll take care of it.
sinon.stub(fs, 'statSync').returns({ size: 1024 }); | ||
sinon.stub(awsDeploy, 'uploadZipFile').resolves(); | ||
const statSyncStub = sinon.stub(fs, 'statSync').returns({ size: 1024 }); | ||
const uploadZipFileStub = sinon.stub(awsDeploy, 'uploadZipFile').resolves(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you create the stubs inside the test, you have to make sure that they are restored if the test exists. Even if it enters a rejection path. Also use the sandbox
to create the stubs. That makes sure that they are removed properly as soon as the sanbox is cleaned up.
So having this, you must add a .finally()
clause to the test that restores and removes the stubs again. Otherwise following tests are affected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, to prevent such stub leaks, it's better to setup the test stubs in before()
or beforeEach()
and clean them in the resp. after methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. It's out of scope. So the stabilization of the unit tests should include this
getLambdaAlexaSkillPermissionLogicalId(functionName) { | ||
return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionAlexaSkill`; | ||
getLambdaAlexaSkillPermissionLogicalId(functionName, alexaSkillIndex) { | ||
return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionAlexaSkill${ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could default the skill index to 0 here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure i'm following you here.. do you want the logical numbering to start from 0 (instead of the current 1 index), or do you want this function to have a default parameter value? (the project supports node v4+, but default parameters were only enabled at node v6).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was just thinking about if it would be allowed to omit the skillIndex here in the function so that it defaults to 0 if it is not set. On the other hand that behavior does not really make sense, because the caller is currently only inside the aws plugin.
Only if a 3rd party plugin uses this function in naming
this would matter - but then the call should fail if no index is given if we do not use a default inside.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you be ok with a solution like
return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionAlexaSkill${
alexaSkillIndex || 0}`;
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that sounds reasonable... should be '0' with quotes, shouldn't it?
throw new this.serverless.classes.Error(errorMessage); | ||
} | ||
appId = event.alexaSkill.appId; | ||
enabled = event.alexaSkill.enabled !== false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
enabled = event.alexaSkill.enabled;
- having the explicit not-equal check for false just obscures this and lessens readability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the explicit not-equal check here is because the enabled
option is optional and defaults to true.
The following definition:
- alexaSkill:
appId: amzn1.ask.skill.yy-yy-yy-yy
should be considered enabled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, ok. Maybe you can add a comment above the assignment, because it is not obvious without knowing exactly this context 😄 And other people might then change it without anyone really complaining.
' Please refer to the documentation for additional information.', | ||
].join(''); | ||
this.serverless.cli.log(warningMessage); | ||
} else if (typeof event.alexaSkill === 'string') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use lodash consistently _.isString()
this.serverless.cli.log(warningMessage); | ||
} else if (typeof event.alexaSkill === 'string') { | ||
appId = event.alexaSkill; | ||
} else if (typeof event.alexaSkill === 'object') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use _.isPlainObject()
or _.isObject()
here, dependeing if you'd allow inherited full objects or just plain objects.
expect(awsCompileAlexaSkillEvents.serverless.service | ||
.provider.compiledCloudFormationTemplate.Resources | ||
.FirstLambdaPermissionAlexaSkill1.Properties.EventSourceToken | ||
).to.be.an('undefined'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
).to.be.undefined;
|
||
return awsDeploy.uploadFunctions().then(() => { | ||
expect(uploadZipFileStub.calledTwice).to.be.equal(true); | ||
expect(uploadZipFileStub.args[0][0]) | ||
.to.be.equal(awsDeploy.serverless.service.functions.first.package.artifact); | ||
expect(uploadZipFileStub.args[1][0]) | ||
.to.be.equal(awsDeploy.serverless.service.package.artifact); | ||
awsDeploy.uploadZipFile.restore(); | ||
fs.statSync.restore(); | ||
uploadZipFileStub.restore(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is wrong in case id the test fails. You need to move the restores into a .finally()
at the end of the promise chain to guarantee that they are executed, regardless which branch the promise chain takes. Otherwise the test will pollute the test environment for all tests executed afterwards.
sinon.spy(awsDeploy.serverless.cli, 'log'); | ||
|
||
return awsDeploy.uploadFunctions().then(() => { | ||
const expected = 'Uploading service .zip file to S3 (1 KB)...'; | ||
expect(awsDeploy.serverless.cli.log.calledWithExactly(expected)).to.be.equal(true); | ||
|
||
fs.statSync.restore(); | ||
awsDeploy.uploadZipFile.restore(); | ||
statSyncStub.restore(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.finally()
here too
@HyperBrain - I finally reproduced the it('should set the createLater flag and resolve if deployment bucket is provided', () => {
awsDeploy.serverless.service.provider.deploymentBucket = 'serverless';
sandbox.stub(awsDeploy.provider, 'request')
.returns(BbPromise.reject({ message: 'does not exist' }));
return new Promise(resolve => {
setTimeout(resolve, 1000);
}).then(() => {
awsDeploy.createStack().then(() => {
expect(awsDeploy.createLater).to.equal(true);
});
});
}); This is the only place |
@kobim Good catch. That's indeed the error.
It should be Can you try that? Then the "does not exist" error check should work properly. |
…lves to an error.
@horike37 @pmuens Thanks to @kobim we now got rid of the createStack unit test failures that often seemed to happen without a reason 🎉 🎉 . The fix (see the 2 comments above) is include in this PR which I will merge soon. We should check the upload tests too, because they show a quite similar behavior. |
@kobim Travis succeeded now, and I'm quite sure that the createStack tests are now ultimately fixed. Thanks for that. I'll finish the review now. |
@HyperBrain - just found out that if you specify an event which isn't alexaSkill, an error is thrown. |
@kobim Thanks for the note. I will review merge the new PR then as sson as it is there. Please add me explicitly as reviewer there. |
* The new alexaSkill skill id restriction (#4701) was throwing an error if an event which is not `alexaSkill` was presented. * `.to.not.throw()` is a function, not a getter.
Is it released? I tried to specify 2 skills for my lambda function and it didn't work with latest serverless 1.27.2 When I specify: events:
- alexaSkill:
appId: amzn1.ask.skill.xx
enabled: true
- alexaSkill:
appId: amzn1.ask.skill.yy
enabled: true The trigger gets removed completely |
@Saandji it was released with 1.27.0, and I just verified it with 1.27.2: functions:
mySkill:
handler: handler.alexa
events:
- alexaSkill: amzn1.ask.skill.xx-xx-xx-xx-xx ended up with the following at .serverless/cloudformation-template-update-stack.json: "MySkillLambdaFunction": {
"Type": "AWS::Lambda::Function",
...
"Handler": "handler.alexa",
...
},
"MySkillLambdaPermissionAlexaSkill1": {
...
"Action": "lambda:InvokeFunction",
"Principal": "alexa-appkit.amazon.com",
"EventSourceToken": "amzn1.ask.skill.xx-xx-xx-xx-xx" // <--- PR #4701
}
} |
@kobim for one skill id it works like a charm for me as well. But when I specify multiple skills, it removes Alexa triggers from my function |
I think you are missing spaces in your yml (see in the docs), try: events:
- alexaSkill:
appId: amzn1.ask.skill.xx
enabled: true
- alexaSkill:
appId: amzn1.ask.skill.yy
enabled: true YAML is very sensitive for its spaces: events:
- alexaSkill:
appId: 1 translates to {
"events": [
{
"alexaSkill": null,
"appId": 1
}
]
} while the correct format will translate to: {
"events": [
{
"alexaSkill": {
"appId": 1
}
}
]
} |
@kobim sorry I'm an idiot, I really did miss the space! Thank you very much! Sorry for bothering =) |
All good, it happens to everyone :) |
What did you implement:
Closes #4700 - using the same concept as
alexaSmartHome
event which forces you to specify skill ID.This implementation is backwards compatible and alerts the users to migrate to the new syntax.
Also, added the option to provide multiple
alexaSkill
events per a function, allowing it to be invoked by multiple skills.How did you implement it:
Added the options to provide either a string or an object to the event, while the values impact the Permission Policy which is sent to AWS.
By default, the alexaSkill is enabled.
How can we verify it:
Example configs (also available in the updated documents):
one function, one skill:
one function, two skills (of which one is disabled):
Todos:
Is this ready for review?: YES
Is it a breaking change?: NO