diff --git a/README.md b/README.md index 7cd7bd8..b7a0140 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,6 @@ A node.js client library for using the Alexa Skill Management API. -# Notice -As of now, the following SMAPI operations in this module are **untested**: skill testing (invocation, simulation), and intent request history. So if there are any problems, please create a new issue or a pull request to fix the issue. Additionally you can help improve this library's stability by adding tests for yet untested SMAPI operations. - ## Table Of Contents * [Documentation](#documentation) diff --git a/test/data/common.js b/test/data/common.js index 70ea352..9772a6a 100644 --- a/test/data/common.js +++ b/test/data/common.js @@ -17,6 +17,11 @@ module.exports = { locales: [LOCALE], reason: 'OTHER', message: 'node-alexa-smapi testing', + endpointRegion: 'Default', // string enum["Default", "NA", "EU", "FE", etc], + skillRequest: { + body: require('./request.json') + }, + simulationContent: 'api', intentRequestParams: { nextToken: null, maxResults: 10, diff --git a/test/data/request.json b/test/data/request.json new file mode 100644 index 0000000..ecc5aa1 --- /dev/null +++ b/test/data/request.json @@ -0,0 +1,41 @@ +{ + "version": "1.0", + "session": { + "new": true, + "sessionId": "amzn1.echo-api.session.[unique-value-here]", + "application": { + "applicationId": "TBD" + }, + "attributes": { + "key": "string value" + }, + "user": { + "userId": "amzn1.ask.account.[unique-value-here]", + "accessToken": "Atza|AAAAAAAA...", + "permissions": { + "consentToken": "ZZZZZZZ..." + } + } + }, + "context": { + "System": { + "device": { + "deviceId": "string", + "supportedInterfaces": {} + }, + "application": { + "applicationId": "TBD" + }, + "user": { + "userId": "amzn1.ask.account.[unique-value-here]", + "accessToken": "Atza|AAAAAAAA...", + "permissions": { + "consentToken": "ZZZZZZZ..." + } + }, + "apiEndpoint": "https://api.amazonalexa.com", + "apiAccessToken": "AxThk..." + } + }, + "request": {} +} diff --git a/test/data/responses.json b/test/data/responses.json index e16deca..b442d14 100644 --- a/test/data/responses.json +++ b/test/data/responses.json @@ -277,6 +277,35 @@ "headers": {}, "data": "" }, + { + "url": "/v0/skills/SKILL_ID/invocations", + "method": "post", + "status": 500, + "headers": {}, + "data": { + "message": "An unexpected error occurred." + } + }, + { + "url": "/v0/skills/SKILL_ID/simulations", + "method": "post", + "status": 200, + "headers": {}, + "data": { + "id": "SIMULATION_ID", + "status": "IN_PROGRESS" + } + }, + { + "url": "/v0/skills/SKILL_ID/simulations/SIMULATION_ID", + "method": "get", + "status": 200, + "headers": {}, + "data": { + "id": "SIMULATION_ID", + "status": "IN_PROGRESS" + } + }, { "url": "/v1/skills/SKILL_ID/stages/development/enablement", "method": "delete", @@ -579,13 +608,6 @@ "headers": {}, "data": "" }, - { - "url": "/v1/skills/SKILL_ID/stages/development/enablement", - "method": "delete", - "status": 204, - "headers": {}, - "data": "" - }, { "url": "/v1/skills/SKILL_ID/stages/development/validations", "method": "post", @@ -606,6 +628,42 @@ "status": "IN_PROGRESS" } }, + { + "url": "/v1/skills/SKILL_ID/invocations", + "method": "post", + "status": 500, + "headers": {}, + "data": { + "message": "An unexpected error occurred." + } + }, + { + "url": "/v1/skills/SKILL_ID/simulations", + "method": "post", + "status": 200, + "headers": {}, + "data": { + "id": "SIMULATION_ID", + "status": "IN_PROGRESS" + } + }, + { + "url": "/v1/skills/SKILL_ID/simulations/SIMULATION_ID", + "method": "get", + "status": 200, + "headers": {}, + "data": { + "id": "SIMULATION_ID", + "status": "IN_PROGRESS" + } + }, + { + "url": "/v1/skills/SKILL_ID/stages/development/enablement", + "method": "delete", + "status": 204, + "headers": {}, + "data": "" + }, { "url": "/v1/skills/SKILL_ID/submit", "method": "post", @@ -674,27 +732,29 @@ "method": "get", "status": 405, "headers": {}, - "data": "" + "data": "\n\n405 Method Not Allowed\n\n

Method Not Allowed

\n

The requested method GET is not allowed for the URL /v1/skills/badSkill.

\n\n" }, { "url": "/v1/skills/badSkill", "method": "post", "status": 405, "headers": {}, - "data": "" + "data": "\n\n405 Method Not Allowed\n\n

Method Not Allowed

\n

The requested method POST is not allowed for the URL /v1/skills/badSkill.

\n\n" }, { "url": "/v1/skills/badSkill", "method": "put", "status": 405, "headers": {}, - "data": "" + "data": "\n\n405 Method Not Allowed\n\n

Method Not Allowed

\n

The requested method PUT is not allowed for the URL /v1/skills/badSkill.

\n\n" }, { "url": "/v1/skills/badSkill", "method": "delete", "status": 400, "headers": {}, - "data": "" + "data": { + "message": "1 validation error detected: Value 'badSkill' at 'skillStageIdentifiers.1.member.skillId' failed to satisfy constraint: Member must satisfy regular expression pattern: (^amzn1\\.ask\\.skill\\.[0-9a-f\\-]+)|(^amzn1\\.echo-sdk-ams\\.app\\.[0-9a-f\\-]+)" + } } ] \ No newline at end of file diff --git a/test/test_smapi_client.js b/test/test_smapi_client.js index 6d7cfe2..1832ad3 100644 --- a/test/test_smapi_client.js +++ b/test/test_smapi_client.js @@ -52,12 +52,14 @@ function showError(error) { } function errorSummary(error) { - const summary = { + const expectedErrorKeywords = /badSkill|invocations/; + // should only add error responses for invoke & custom methods + if (expectedErrorKeywords.test(error.config.url)) responses.add(error); + return { status: error.status, statusText: error.statusText, data: error.data }; - return summary; } var sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); @@ -140,17 +142,19 @@ describe('Testing node-alexa-smapi', function() { } } - describe('Testing with SMAPI ' + TEST_VERSION, function() { + describe('Testing with SMAPI ' + TEST_VERSION + ' for region NA', function() { after(function(){ // Persist response template if (shouldCapture(TEST_TYPE)) { if (TEST_VERSION === VERSION_0) responses.sanitize({ VENDOR_ID: testData.vendorId, - SKILL_ID: testData.skillId + SKILL_ID: testData.skillId, + SIMULATION_ID: testData.simulationId }); else if (TEST_VERSION === VERSION_1) responses.sanitize({ VENDOR_ID: testData.vendorId, SKILL_ID: testData.skillId, + SIMULATION_ID: testData.simulationId, VALIDATION_ID: testData.validationId }); } @@ -160,7 +164,8 @@ describe('Testing node-alexa-smapi', function() { this.retries(MAX_RETRIES); this.timeout(MOCHA_TIMEOUT); const smapiClient = SMAPI_CLIENT({ - version: TEST_VERSION + version: TEST_VERSION, + region: 'NA' }); if (shouldCapture(TEST_TYPE)) smapiClient.rest.client.interceptors.response.use(responses.add); @@ -464,7 +469,7 @@ describe('Testing node-alexa-smapi', function() { }); }); - context('-> Skill Enablement Operations', function() { + context('-> Skill Enablement Operations (except disable)', function() { describe('-> Enable a skill', function() { var subject; @@ -496,12 +501,31 @@ describe('Testing node-alexa-smapi', function() { return expect(subject).to.eventually.have.property('status', 204); }); }); + }); - describe('-> Disable a skill', function() { + context('-> Skill Testing Operations', function() { + if (TEST_VERSION === VERSION_1) describe('-> Validate a skill', function() { var subject; beforeEach(function() { - subject = smapiClient.skillEnablement.disable(testData.skillId, testData.stage); + subject = smapiClient.skillTesting.validate(testData.skillId, testData.stage, testData.locales); + }); + + it('responds with validationId', function() { + subject = subject.then(function(response) { + showResponse(response); + testData.validationId = response.id; + return response; + }, retry); + return expect(subject).to.eventually.have.property('id'); + }); + }); + + if (TEST_VERSION === VERSION_1) describe('-> Check validation status of a skill', function() { + var subject; + + beforeEach(function() { + subject = smapiClient.skillTesting.validationStatus(testData.skillId, testData.stage, testData.validationId); }); it('responds with no content', function() { @@ -509,37 +533,53 @@ describe('Testing node-alexa-smapi', function() { showResponse(response); return response; }, retry); - return expect(subject).to.eventually.have.property('status', 204); + return expect(subject).to.eventually.have.property('status'); }); }); - }); - context('-> Skill Testing Operations', function() { - if (TEST_VERSION === VERSION_1) describe('-> Validate a skill', function() { + describe('-> Invoke a skill', function() { var subject; beforeEach(function() { - subject = smapiClient.skillTesting.validate(testData.skillId, testData.stage, testData.locales); + testData.skillRequest.body.session.application.applicationId = + testData.skillRequest.body.context.System.application.applicationId = testData.skillId; + subject = smapiClient.skillTesting.invoke(testData.skillId, testData.endpointRegion, testData.skillRequest); }); - it('responds with validationId', function() { + it('responds with internal server error (no reply as skill code is not deployed anywhere)', function() { subject = subject.then(function(response) { showResponse(response); - testData.validationId = response.id; + return response; + }, retry); + return expect(subject).to.eventually.have.property('status', 500); + }); + }); + + describe('-> Simulate a skill', function() { + var subject; + + beforeEach(function() { + subject = smapiClient.skillTesting.simulate(testData.skillId, testData.simulationContent, testData.locale); + }); + + it('responds with simulationId', function() { + subject = subject.then(function(response) { + showResponse(response); + testData.simulationId = response.id; return response; }, retry); return expect(subject).to.eventually.have.property('id'); }); }); - if (TEST_VERSION === VERSION_1) describe('-> Check validation status of a skill', function() { + describe('-> Check status of a skill simulation', function() { var subject; beforeEach(function() { - subject = smapiClient.skillTesting.validationStatus(testData.skillId, testData.stage, testData.validationId); + subject = smapiClient.skillTesting.simulationStatus(testData.skillId, testData.simulationId); }); - it('responds with no content', function() { + it('responds with simulation status', function() { subject = subject.then(function(response) { showResponse(response); return response; @@ -549,6 +589,24 @@ describe('Testing node-alexa-smapi', function() { }); }); + context('-> Skill Enablement Operations (disable only)', function() { + describe('-> Disable a skill', function() { + var subject; + + beforeEach(function() { + subject = smapiClient.skillEnablement.disable(testData.skillId, testData.stage); + }); + + it('responds with no content', function() { + subject = subject.then(function(response) { + showResponse(response); + return response; + }, retry); + return expect(subject).to.eventually.have.property('status', 204); + }); + }); + }); + if (shouldCertify(TEST_TYPE)) context('-> Skill Certification Operations', function() { describe('-> Submit a skill for certification', function() { var subject; @@ -675,7 +733,6 @@ describe('Testing node-alexa-smapi', function() { }); context('-> Custom Operations with bad skillId, no access_token', function() { - // TODO: troubleshoot why smapiClient.rest.client.interceptors.response.use(responses.add) is not capturing these responses describe('-> head()', function() { var subject;