├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json └── test ├── README.md ├── data ├── accountLinkingRequest.json ├── common.js ├── interactionModel.json ├── manifest_v0.json ├── manifest_v1.json ├── request.json └── responses.json ├── test_smapi_client.js └── utils ├── mockAdapter.js └── responses.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "rules": { 8 | "indent": [ 9 | "warn", 10 | 2 11 | ], 12 | "linebreak-style": [ 13 | "warn", 14 | "unix" 15 | ], 16 | "quotes": [ 17 | "warn", 18 | "single" 19 | ], 20 | "semi": [ 21 | "warn", 22 | "always" 23 | ], 24 | "prefer-const": [ 25 | "warn", 26 | { 27 | "destructuring": "any", 28 | "ignoreReadBeforeAssign": false 29 | } 30 | ], 31 | "strict": [ 32 | "error", 33 | "global" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | # next.js build output 73 | .next 74 | 75 | # nuxt.js build output 76 | .nuxt 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ 89 | 90 | # sensitive data used for testing 91 | test/data/secrets.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Add the following environment sensitive variables to Travis CI: 2 | # - REFRESH_TOKEN 3 | # - CLIENT_ID 4 | # - CLIENT_SECRET 5 | 6 | language: node_js 7 | 8 | sudo: false 9 | 10 | matrix: 11 | include: 12 | - node_js: node 13 | script: 14 | - npm run-script lint 15 | - npm run-script test-with-coverage 16 | 17 | node_js: 18 | - 6 19 | - 8 20 | - 10 21 | - 12 22 | 23 | cache: 24 | directories: 25 | - node_modules 26 | 27 | before_install: 28 | - npm config set spin false 29 | 30 | after_success: 31 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 32 | 33 | notifications: 34 | email: false 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ### 1.1.1 (July/9/2019) 4 | 5 | * [#12](https://github.com/tejashah88/node-alexa-smapi/issues/12): Updated dependencies to resolve some moderate DDoS security vulnerabilities. Travis CI integration now working for Node.js version 6 through 12. - [@tejashah88](https://github.com/tejashah88). 6 | 7 | ### 1.1.0 (Sep/10/2018) 8 | 9 | * [#1](https://github.com/tejashah88/node-alexa-smapi/issues/1): Added unit testing for account linking, skill enablement, skill testing (validation only) and node-alexa-smapi custom operations, Travis CI integration now working for Node.js version 6 through 10. Added coveralls.io integration - [@marcelobern](https://github.com/marcelobern). 10 | * Added support for undocumented certification status operation (`alexaSmapi.skillCertification.status()`). Currently working with SMAPI v1 (location returned by SMAPI v0 is not valid/usable) - [@marcelobern](https://github.com/marcelobern). 11 | * HEAD, GET, POST, PUT, and DELETE operations now also return HTTP status - [@marcelobern](https://github.com/marcelobern). 12 | 13 | ### 1.0.0 (Sep/3/2018) 14 | 15 | * [#3](https://github.com/tejashah88/node-alexa-smapi/issues/3): Added support for [SMAPI v1](https://developer.amazon.com/docs/smapi/smapi-migration.html) - [@marcelobern](https://github.com/marcelobern). 16 | * [#1](https://github.com/tejashah88/node-alexa-smapi/issues/1): Added unit testing for skill, interaction model, and vendor operations, added Travis CI integration - [@marcelobern](https://github.com/marcelobern). 17 | * Access token can now be automatically retrieved from Login with Amazon and `smapiClient.tokens.refresh()` should be invoked right after `smapiClient` instantiation - [@marcelobern](https://github.com/marcelobern). 18 | * `smapiClient` should now be instantiated using `{version: "v0, or v1 (default)", region: "NA (default), EU, or FE"}` - [@marcelobern](https://github.com/marcelobern). 19 | * POST, HEAD, and PUT requests now also return location and etag headers - [@marcelobern](https://github.com/marcelobern). 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to node-alexa-smapi 2 | 3 | You're encouraged to submit [pull requests](https://github.com/tejashah88/node-alexa-smapi/pulls), [propose features and discuss issues](https://github.com/tejashah88/node-alexa-smapi/issues). 4 | 5 | In the examples below, substitute your Github username for `contributor` in URLs. 6 | 7 | ### Fork the Project 8 | 9 | Fork the [project on Github](https://github.com/tejashah88/node-alexa-smapi) and check out your copy. 10 | 11 | ``` 12 | git clone https://github.com/contributor/node-alexa-smapi.git 13 | cd node-alexa-smapi 14 | git remote add upstream https://github.com/tejashah88/node-alexa-smapi.git 15 | ``` 16 | 17 | ### Run Tests 18 | 19 | Ensure that you can build the project and run tests. 20 | 21 | ``` 22 | npm install 23 | npm test 24 | ``` 25 | 26 | ## Contribute Code 27 | 28 | ### Create a Topic Branch 29 | 30 | Make sure your fork is up-to-date and create a topic branch for your feature or bug fix. 31 | 32 | ``` 33 | git checkout master 34 | git pull upstream master 35 | git checkout -b my-feature-branch 36 | ``` 37 | 38 | ### Write Tests 39 | 40 | Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Tests live under [test](test). 41 | 42 | We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix. 43 | 44 | ### Write Code 45 | 46 | Implement your feature or bug fix. 47 | 48 | Make sure that `npm test` completes without errors. 49 | 50 | ### Write Documentation 51 | 52 | Document any external behavior in the [README](README.md). 53 | 54 | ### Commit Changes 55 | 56 | Make sure git knows your name and email address: 57 | 58 | ``` 59 | git config --global user.name "Your Name" 60 | git config --global user.email "contributor@example.com" 61 | ``` 62 | 63 | Writing good commit logs is important. A commit log should describe what changed and why. 64 | 65 | ``` 66 | git add ... 67 | git commit 68 | ``` 69 | 70 | ### Push 71 | 72 | ``` 73 | git push origin my-feature-branch 74 | ``` 75 | 76 | ### Make a Pull Request 77 | 78 | Go to https://github.com/tejashah88/node-alexa-smapi and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days. 79 | 80 | Add more commits or amend your previous commit with any changes. 81 | 82 | Once you have a pull request number add a commit to record your change in the CHANGELOG.md, don't worry if you forget, Travis-CI will remind you :) 83 | 84 | ``` 85 | git commit --amend 86 | git push origin my-feature-branch -f 87 | ``` 88 | 89 | ### Rebase 90 | 91 | If you've been working on a change for a while, rebase with upstream/master. 92 | 93 | ``` 94 | git fetch upstream 95 | git rebase upstream/master 96 | git push origin my-feature-branch -f 97 | ``` 98 | 99 | ### Check on Your Pull Request 100 | 101 | Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above. 102 | 103 | ### Be Patient 104 | 105 | It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there! 106 | 107 | ## Thank You 108 | 109 | Please do know that we really appreciate and value your time and work. We love you, really. 110 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2019 Tejas Shah 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-alexa-smapi 2 | 3 | [](https://www.npmjs.com/package/node-alexa-smapi) 4 | [](https://travis-ci.org/tejashah88/node-alexa-smapi) 5 | [](https://coveralls.io/github/tejashah88/node-alexa-smapi?branch=master) 6 | 7 | A node.js client library for using the Alexa Skill Management API. 8 | 9 | ## Table Of Contents 10 | 11 | * [Obtaining Essential Credentials](#obtaining-essential-credentials) 12 | * [Documentation](#documentation) 13 | * [Constructor](#constructor) 14 | * [Constants](#constants) 15 | * [Access Tokens](#access-tokens) 16 | * [Skill Operations](#skill-operations) 17 | * [Interaction Model Operations](#interaction-model-operations) 18 | * [Account Linking Operations](#account-linking-operations) 19 | * [Vendor Operations](#vendor-operations) 20 | * [Skill Enablement Operations](#skill-enablement-operations) 21 | * [Skill Certification Operations](#skill-certification-operations) 22 | * [Skill Testing Operations](#skill-testing-operations) 23 | * [Intent Request History Operations](#intent-request-history-operations) 24 | * [Miscellaneous Functions](#miscellaneous-functions) 25 | * [Custom API calls](#custom-api-calls) 26 | * [Examples](#examples) 27 | * [Using Promises](#using-promises) 28 | * [Using Async/Await](#using-asyncawait) 29 | 30 | ## Obtaining Essential Credentials 31 | 32 | In order to obtain the `refreshToken`, `clientId`, and `clientSecret`, you'll need to follow the instructions below: 33 | 1. Follow [these instructions](https://developer.amazon.com/docs/smapi/ask-cli-command-reference.html#util-command) to retrieve `clientId` and `clientSecret`. 34 | 2. Run `ask util generate-lwa-tokens` to generate a refreshToken (copy the value from `refresh_token` and use it to initialize `refreshToken` in the next step). 35 | 3. Create a file called `secrets.json` which should look something like: 36 | 37 | ```json 38 | { 39 | "refreshToken": "generated by ask util generate-lwa-tokens", 40 | "clientId": "amzn1.application-oa2-client.myClientId", 41 | "clientSecret": "myClientSecret" 42 | } 43 | ``` 44 | 45 | And here is some example code to test that the credentials are valid: 46 | ```js 47 | const mySmapiClient = require('node-alexa-smapi')(); 48 | const mySecrets = require('./secrets.json'); 49 | 50 | mySmapiClient.tokens.refresh({ 51 | refreshToken: mySecrets.refreshToken, 52 | clientId: mySecrets.clientId, 53 | clientSecret: mySecrets.clientSecret, 54 | }).then(() => { 55 | mySmapiClient.vendors.list().then(result => { 56 | console.log(`My vendor list: ${JSON.stringify(result, null, ' ')}`); 57 | }) 58 | }); 59 | ``` 60 | 61 | See the [examples](#examples) part for more examples. 62 | 63 | ## Documentation 64 | Official Documentation: https://developer.amazon.com/docs/smapi/ask-cli-intro.html#smapi-intro 65 | 66 | All methods return a promise, which either resolves to the SMAPI data received, or rejects with an error. 67 | 68 | ### Constructor 69 | ```javascript 70 | // Constructor for building the SMAPI REST client. 71 | // params should be in the form: {version: "v0, or v1 (default)", region: "NA (default), EU, or FE"}. 72 | Object alexaSmapi(optional Object params) 73 | ``` 74 | 75 | ### Constants 76 | ```javascript 77 | // All possible base URLs. By default, the NA url is used. 78 | const BASE_URLS = { 79 | NA: 'https://api.amazonalexa.com', 80 | EU: 'https://api.eu.amazonalexa.com', 81 | FE: 'https://api.fe.amazonalexa.com' 82 | }; 83 | ``` 84 | 85 | ### Access Tokens 86 | This module includes operation to retrieve a SMAPI compliant (Login with Amazon) access token. Immediately after instantiation invoke alexaSmapi.tokens.refresh(params) to seed the (Authorization header) access token for all future operations. 87 | ```javascript 88 | // Constructor for building the SMAPI REST client. 89 | // params should be in the form: {version: "v0, or v1 (default)", region: "NA (default), EU, or FE"}. 90 | const alexaSmapi = require('node-alexa-smapi'); 91 | var smapiClient = alexaSmapi(optional Object params); 92 | smapiClient.tokens.refresh({ 93 | refreshToken: "MY_REFRESH_TOKEN", 94 | clientId: "MY_CLIENT_ID", 95 | clientSecret: "MY_CLIENT_SECRET" 96 | }); 97 | ``` 98 | 99 | ### Skill Operations 100 | Official Documentation: https://developer.amazon.com/docs/smapi/skill-operations.html 101 | 102 | ```javascript 103 | // Fetches the skill manifest associated with the skill ID. 104 | Object alexaSmapi.skills.getManifest(String skillId, String stage) 105 | 106 | // Creates a new skill associated with the vendor ID. 107 | Object alexaSmapi.skills.create(String vendorId, Object skillManifest) 108 | 109 | // Updates a skill's manifest with the specified skill ID. 110 | alexaSmapi.skills.update(String skillId, String stage, Object skillManifest) 111 | 112 | // Retrieves the current statuc of the skill 113 | Object alexaSmapi.skills.status(String skillId) 114 | 115 | // List the skills for a specified vendorId, which is a mandatory parameter. 116 | // The optional maxResults and nextToken values provide paging for the results. 117 | Object alexaSmapi.skills.list(String vendorId, optional Integer maxResults, optional String nextToken) 118 | 119 | // Deletes a skill by its skill ID. 120 | alexaSmapi.skills.delete(String skillId) 121 | ``` 122 | 123 | ### Interaction Model Operations 124 | Official Documentation: https://developer.amazon.com/docs/smapi/interaction-model-operations.html 125 | 126 | ```javascript 127 | // Retrieves the interaction model for a specified skill. 128 | Object alexaSmapi.interactionModel.get(String skillId, String stage, String locale) 129 | 130 | // Retrieves the Etag for a specified skill. 131 | String alexaSmapi.interactionModel.getEtag(String skillId, String stage, String locale) 132 | 133 | // Updates the interaction model for a specified skill. 134 | Object alexaSmapi.interactionModel.update(String skillId, String stage, String locale, Object interactionModel) 135 | 136 | // Retrieves the building status of the interaction model. 137 | Object alexaSmapi.interactionModel.getStatus(String skillId, String stage, String locale) 138 | ``` 139 | 140 | ### Account Linking Operations 141 | Official Documentation: https://developer.amazon.com/docs/smapi/account-linking-operations.html 142 | 143 | ```javascript 144 | // Updates the account linking details 145 | alexaSmapi.accountLinking.update(String skillId, String stage, Object accountLinkingRequest) 146 | 147 | // Retrieves the account linking details 148 | Object alexaSmapi.accountLinking.readInfo(String skillId, String stage) 149 | 150 | // Deletes the account linking details 151 | alexaSmapi.accountLinking.delete(String skillId, String stage) 152 | ``` 153 | 154 | ### Vendor Operations 155 | Official Documentation: https://developer.amazon.com/docs/smapi/vendor-operations.html 156 | 157 | ```javascript 158 | // List all of the vendors associated with a user (access token). 159 | Array alexaSmapi.vendors.list() 160 | ``` 161 | 162 | ### Skill Enablement Operations 163 | Official Documentation: https://developer.amazon.com/docs/smapi/skill-enablement.html 164 | 165 | ```javascript 166 | // Enables a skill stage for the requestor. The requestor should be either a developer or the owner of the skill. 167 | // Please note that only one skill stage can be enabled for a given user at one time. 168 | alexaSmapi.skillEnablement.enable(String skillId, String stage) 169 | 170 | // Checks whether a skill stage is enabled or not for the requestor. 171 | alexaSmapi.skillEnablement.status(String skillId, String stage) 172 | 173 | // Disables a skill by deleting the skill enablement. 174 | alexaSmapi.skillEnablement.disable(String skillId, String stage) 175 | ``` 176 | 177 | ### Skill Certification Operations 178 | Official Documentation: https://developer.amazon.com/docs/smapi/skill-certification-operations.html 179 | 180 | ```javascript 181 | // Submit a skill for certification for potential publication. 182 | alexaSmapi.skillCertification.submit(String skillId) 183 | 184 | // Check status of a skill certification. 185 | // Working for v1 only as it was implemented mainly trough trial & error as operation is not documented under https://developer.amazon.com/docs/smapi/skill-certification-operations.html 186 | alexaSmapi.skillCertification.status(String vendorId, String skillId) 187 | 188 | // Withdraw a skill from the certification process. 189 | // Possible enumeration values for 'reason' 190 | // * TEST_SKILL 191 | // * MORE_FEATURES 192 | // * DISCOVERED_ISSUE 193 | // * NOT_RECEIVED_CERTIFICATION_FEEDBACK 194 | // * NOT_INTEND_TO_PUBLISH 195 | // * OTHER 196 | alexaSmapi.skillCertification.withdraw(String skillId, String reason, String message) 197 | ``` 198 | 199 | ### Skill Testing Operations 200 | Official Documentation: https://developer.amazon.com/docs/smapi/skill-testing-operations.html 201 | 202 | ```javascript 203 | // Used for directly testing a skill by passing the skill request object directly. 204 | Object alexaSmapi.skillTesting.validate(String skillId, String stage, [String] locales) 205 | 206 | // Used for directly testing a skill by passing the skill request object directly. 207 | Object alexaSmapi.skillTesting.validationStatus(String skillId, String stage, String validationId) 208 | 209 | // Used for directly testing a skill by passing the skill request object directly. 210 | Object alexaSmapi.skillTesting.invoke(String skillId, String endpointRegion, Object skillRequest) 211 | 212 | // Simulates a skill execution. 213 | Object alexaSmapi.skillTesting.simulate(String skillId, String content, String locale) 214 | 215 | // Retrieves the status of the simulated skill execution. 216 | Object alexaSmapi.skillTesting.simulationStatus(String skillId, String requestId) 217 | ``` 218 | 219 | ### Intent Request History Operations 220 | Official Documentation: https://developer.amazon.com/docs/smapi/intent-request-history.html 221 | 222 | ```javascript 223 | // Provides aggregated and anonymized transcriptions of user speech data and intent request details for their skills, on a per-skill basis. 224 | // A skill must have at least 10 unique users per locale in a day, in order for data to be available for that locale for that day. 225 | // Here is the format for params (only the skillId is required all others are optional): 226 | // * skillId - The skillId for which utterance data is returned. 227 | // * nextToken (default: null) - Use nextToken along with the maxResults parameter to specify how many results should be loaded in the page. 228 | // * maxResults (default: 10) - Maximum number of result items (at-most and not at-least) that will be returned in the response. 229 | // * sortDirection (dafault: desc) - Valid values: asc (for ascending) or desc (for descending). 230 | // * sortField - Valid values: dialogAct.name, locale, intent.confidence.bin, stage, publicationStatus, intent.name, interactionType, or utteranceText. 231 | // * dialogAct.name (default: null) - Valid values: Dialog.ElicitSlot, Dialog.ConfirmSlot, or Dialog.ConfirmIntent. 232 | // * locale (default: null) - Valid values: All currently supported locales. Example: en-US. This filter can have multiple values and is not case-sensitive. 233 | // * intent.confidence.bin (default: null) - Valid values: HIGH, MEDIUM, OR LOW. This filter can have multiple values and is not case-sensitive. 234 | // * stage (default: null) - Valid values: live or development. This filter can have multiple values and is not case-sensitive. 235 | // * publicationStatus (default: null) - Valid values: certification or development. This filter can have multiple values and is not case-sensitive. 236 | // * utteranceText (default: null) - Valid values: Any string. This filter can have multiple values and is not case-sensitive. 237 | // * intent.name (default: null) - Valid values: Any string without white spaces. This filter can have multiple values and is not case-sensitive. 238 | // * intent.slot.name (default: null) - Valid values: Any string without white spaces. This filter can have multiple values and is not case-sensitive. 239 | // * interactionType (default: null) - This filter can have multiple values. Valid values: 240 | // * ONE_SHOT: The user invokes the skill and states their intent in a single phrase. 241 | // * MODAL: The user first invokes the skill and then states their intent. 242 | alexaSmapi.intentRequests.list(Object params) 243 | ``` 244 | Sample params: 245 | ```javascript 246 | const params = { 247 | skillId: 'MY_SKILL_ID', 248 | maxResults: 10, 249 | sortDirection: 'desc', 250 | sortField: 'intent.confidence.bin', 251 | locale: 'en-US', 252 | locale: 'en-CA', 253 | locale: 'en-GB', 254 | locale: 'en-AU', 255 | locale: 'en-IN', 256 | 'intent.confidence.bin': 'high', 257 | 'intent.confidence.bin': 'medium', 258 | 'intent.confidence.bin': 'low', 259 | stage: 'live', 260 | stage: 'development', 261 | publicationStatus: 'certification', 262 | publicationStatus: 'development', 263 | utteranceText: 'api', 264 | utteranceText: 'a pie', 265 | utteranceText: 'ape', 266 | 'intent.name': 'testIntent', 267 | 'intent.name': 'AMAZON.HelpIntent', 268 | interactionType: 'ONE_SHOT', 269 | interactionType: 'MODAL' 270 | }; 271 | ``` 272 | 273 | ### Miscellaneous Functions 274 | ```javascript 275 | // Refeshes the authorization token with the access token provided. 276 | alexaSmapi.refreshToken(String accessToken) 277 | 278 | // Sets the new base URL for future API calls. 279 | alexaSmapi.setBaseUrl(String url) 280 | ``` 281 | 282 | ### Custom API calls 283 | Due to its recent release to the public, some API methods may not be covered by this module. In that case, a bunch of custom functions are available to use. They will return the response received from making the call. 284 | 285 | ```javascript 286 | // Perform a custom HEAD request 287 | alexaSmapi.custom.head(String url) 288 | 289 | // Perform a custom GET request 290 | alexaSmapi.custom.get(String url, Object parameters) 291 | 292 | // Perform a custom POST request 293 | alexaSmapi.custom.post(String url, Object parameters) 294 | 295 | // Perform a custom PUT request 296 | alexaSmapi.custom.put(String url, Object parameters) 297 | 298 | // Perform a custom DELETE request 299 | alexaSmapi.custom.delete(String url) 300 | ``` 301 | 302 | ## Examples 303 | 304 | Note that you'll always need to have a valid `refreshToken`, in which you can obtain that code below: 305 | ```js 306 | const mySmapiClient = require('node-alexa-smapi')(); 307 | const mySecrets = require('./secrets.json'); 308 | 309 | mySmapiClient.tokens.refresh({ 310 | refreshToken: mySecrets.refreshToken, 311 | clientId: mySecrets.clientId, 312 | clientSecret: mySecrets.clientSecret, 313 | }).then(() => { 314 | // do something with the SMAPI client... 315 | }); 316 | ``` 317 | 318 | ### Using Promises 319 | ```javascript 320 | const mySmapiClient = require('node-alexa-smapi')(); 321 | const mySecrets = require('./secrets.json'); 322 | 323 | mySmapiClient.tokens.refresh({ 324 | refreshToken: mySecrets.refreshToken, 325 | clientId: mySecrets.clientId, 326 | clientSecret: mySecrets.clientSecret, 327 | }) 328 | .then(() => smapiClient.skills.getManifest(skillId, 'development')) 329 | .then(data => console.log(data)) 330 | .catch(error => console.log(error)); 331 | ``` 332 | 333 | ### Using Async/Await 334 | ```javascript 335 | const mySmapiClient = require('node-alexa-smapi')(); 336 | const mySecrets = require('./secrets.json'); 337 | 338 | (async function() { 339 | try { 340 | await mySmapiClient.tokens.refresh({ 341 | refreshToken: mySecrets.refreshToken, 342 | clientId: mySecrets.clientId, 343 | clientSecret: mySecrets.clientSecret, 344 | }); 345 | 346 | let manifest = await smapiClient.skills.getManifest(skillId, 'development'); 347 | console.log(manifest); 348 | } catch (error) { 349 | console.log(error); 350 | } 351 | })(); 352 | ``` 353 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | var axios = require('axios'); 5 | 6 | const BASE_URLS = { 7 | NA: 'https://api.amazonalexa.com', 8 | EU: 'https://api.eu.amazonalexa.com', 9 | FE: 'https://api.fe.amazonalexa.com' 10 | }; 11 | const DEFAULT_GEO_REGION = 'NA'; 12 | 13 | const VERSION_0 = 'v0'; 14 | const VERSION_1 = 'v1'; 15 | const SUPPORTED_VERSIONS = [VERSION_0, VERSION_1]; 16 | const DEFAULT_VERSION = SUPPORTED_VERSIONS[SUPPORTED_VERSIONS.length - 1]; 17 | 18 | function alexaSMAPI(params) { 19 | params = params !== undefined ? params : {}; 20 | const version = SUPPORTED_VERSIONS.includes(params.version) ? params.version : DEFAULT_VERSION; 21 | var smapi = { BASE_URLS, version }; 22 | 23 | smapi.rest = { 24 | client: axios.create({ 25 | baseURL: BASE_URLS[params.region in BASE_URLS ? params.region : DEFAULT_GEO_REGION], 26 | headers: { 27 | 'Content-Type': 'application/json', 28 | 'Accept': 'application/json' 29 | } 30 | }), 31 | head: function(url) { 32 | return this.client.head(url) 33 | .then(response => ({status: response.status, location: response.headers.location, etag: response.headers.etag})) 34 | .catch(response => Promise.reject(response.response)); 35 | }, 36 | get: function(url, parameters) { 37 | return this.client.get(url, { params: parameters !== undefined ? parameters : {} }) 38 | .then(response => Object.assign({}, {status: response.status}, response.data)) 39 | .catch(response => Promise.reject(response.response)); 40 | }, 41 | post: function(url, parameters) { 42 | return this.client.post(url, parameters !== undefined ? parameters : {}) 43 | .then(response => Object.assign({}, {status: response.status, location: response.headers.location, etag: response.headers.etag}, response.data)) 44 | .catch(response => Promise.reject(response.response)); 45 | }, 46 | put: function(url, parameters) { 47 | return this.client.put(url, parameters !== undefined ? parameters : {}) 48 | .then(response => ({status: response.status, location: response.headers.location, etag: response.headers.etag})) 49 | .catch(response => Promise.reject(response.response)); 50 | }, 51 | delete: function(url) { 52 | return this.client.delete(url) 53 | .then(response => Object.assign({}, {status: response.status}, response.data)) 54 | .catch(response => Promise.reject(response.response)); 55 | } 56 | }; 57 | 58 | smapi.refreshToken = (token) => { 59 | if (typeof token !== 'string' || token.length === 0) 60 | throw new Error('Invalid token specified!'); 61 | else 62 | smapi.rest.client.defaults.headers.common['Authorization'] = token; 63 | }; 64 | 65 | smapi.setBaseUrl = (url) => { 66 | if (typeof url !== 'string' || url.length === 0) 67 | throw new Error('Invalid base url specified!'); 68 | else 69 | smapi.rest.client.defaults.baseURL = url; 70 | }; 71 | 72 | smapi.tokens = { 73 | refresh: (params) => { 74 | return axios({ 75 | baseURL: 'https://api.amazon.com', 76 | url: '/auth/o2/token', 77 | method: 'post', 78 | headers: { 79 | 'Content-Type': 'application/json', 80 | 'Accept': 'application/json' 81 | }, 82 | data: { 83 | grant_type: 'refresh_token', 84 | refresh_token: params.refreshToken, 85 | client_id: params.clientId, 86 | client_secret: params.clientSecret, 87 | } 88 | }) 89 | .then(response => { 90 | smapi.refreshToken(response.data.access_token); 91 | return response.data; // could do some fancy work to "auto-refresh" token based on timestamp 92 | }) 93 | .catch(response => Promise.reject(response.response)); 94 | } 95 | }; 96 | 97 | smapi.skills = { 98 | getManifest: (skillId, stage) => { 99 | const url = { 100 | v0: `/v0/skills/${skillId}`, 101 | v1: `/v1/skills/${skillId}/stages/${stage}/manifest` 102 | }; 103 | return smapi.rest.get(url[smapi.version]); 104 | }, 105 | create: (vendorId, skillManifest) => { 106 | if (smapi.version === VERSION_0) { 107 | return smapi.rest.post('/v0/skills', { vendorId, skillManifest }); 108 | } else if (smapi.version === VERSION_1) { 109 | return smapi.rest.post('/v1/skills', { vendorId, manifest: skillManifest }); 110 | } 111 | }, 112 | update: (skillId, stage, skillManifest) => { 113 | if (smapi.version === VERSION_0) { 114 | skillManifest = stage; 115 | return smapi.rest.put(`/v0/skills/${skillId}`, { skillManifest }); 116 | } else if (smapi.version === VERSION_1) { 117 | return smapi.rest.put(`/v1/skills/${skillId}/stages/${stage}/manifest`, { manifest: skillManifest }); 118 | } 119 | }, 120 | status: skillId => smapi.rest.get(`/${smapi.version}/skills/${skillId}/status`), 121 | list: (vendorId, maxResults, nextToken) => smapi.rest.get(`/${smapi.version}/skills`, { vendorId, maxResults, nextToken }), 122 | delete: skillId => smapi.rest.delete(`/${smapi.version}/skills/${skillId}`) 123 | }; 124 | 125 | smapi.interactionModel = { 126 | get: (skillId, stage, locale) => { 127 | if (smapi.version === VERSION_0) locale = stage; 128 | const url = { 129 | v0: `/v0/skills/${skillId}/interactionModel/locales/${locale}`, 130 | v1: `/v1/skills/${skillId}/stages/${stage}/interactionModel/locales/${locale}` 131 | }; 132 | return smapi.rest.get(url[smapi.version]); 133 | }, 134 | getEtag: (skillId, stage, locale) => { 135 | if (smapi.version === VERSION_0) locale = stage; 136 | const url = { 137 | v0: `/v0/skills/${skillId}/interactionModel/locales/${locale}`, 138 | v1: `/v1/skills/${skillId}/stages/${stage}/interactionModel/locales/${locale}` 139 | }; 140 | return smapi.rest.head(url[smapi.version]); 141 | }, 142 | update: (skillId, stage, locale, interactionModel) => { 143 | if (smapi.version === VERSION_0) { 144 | interactionModel = locale; 145 | locale = stage; 146 | return smapi.rest.post(`/v0/skills/${skillId}/interactionModel/locales/${locale}`, { interactionModel }); 147 | } else if (smapi.version === VERSION_1) 148 | return smapi.rest.put(`/v1/skills/${skillId}/stages/${stage}/interactionModel/locales/${locale}`, { interactionModel }); 149 | }, 150 | getStatus: (skillId, locale) => { 151 | const url = { 152 | v0: `/v0/skills/${skillId}/interactionModel/locales/${locale}/status`, 153 | v1: `/v1/skills/${skillId}/status?resource=interactionModel` 154 | }; 155 | return smapi.rest.get(url[smapi.version]); 156 | } 157 | }; 158 | 159 | smapi.accountLinking = { 160 | update: (skillId, stage, accountLinkingRequest) => { 161 | if (smapi.version === VERSION_0) accountLinkingRequest = stage; 162 | const url = { 163 | v0: `/v0/skills/${skillId}/accountLinkingClient`, 164 | v1: `/v1/skills/${skillId}/stages/${stage}/accountLinkingClient` 165 | }; 166 | return smapi.rest.put(url[smapi.version], { accountLinkingRequest }); 167 | }, 168 | readInfo: (skillId, stage) => { 169 | const url = { 170 | v0: `/v0/skills/${skillId}/accountLinkingClient`, 171 | v1: `/v1/skills/${skillId}/stages/${stage}/accountLinkingClient` 172 | }; 173 | return smapi.rest.get(url[smapi.version]); 174 | }, 175 | delete: (skillId, stage) => smapi.rest.delete(`/v1/skills/${skillId}/stages/${stage}/accountLinkingClient`), 176 | }; 177 | 178 | smapi.vendors = { 179 | list: () => smapi.rest.get(`/${smapi.version}/vendors`) 180 | }; 181 | 182 | smapi.skillEnablement = { 183 | enable: (skillId, stage) => smapi.rest.put(`/v1/skills/${skillId}/stages/${stage}/enablement`), 184 | status: (skillId, stage) => smapi.rest.get(`/v1/skills/${skillId}/stages/${stage}/enablement`), 185 | disable: (skillId, stage) => smapi.rest.delete(`/v1/skills/${skillId}/stages/${stage}/enablement`) 186 | }; 187 | 188 | smapi.skillCertification = { 189 | submit: skillId => smapi.rest.post(`/${smapi.version}/skills/${skillId}/submit`), 190 | status: (vendorId, skillId) => smapi.rest.get('/v1/skills', { vendorId, skillId }), // Trial and error as it is not properly documented at https://developer.amazon.com/docs/smapi/skill-certification-operations.html 191 | withdraw: (skillId, reason, message) => smapi.rest.post(`/${smapi.version}/skills/${skillId}/withdraw`, { reason, message }) 192 | }; 193 | 194 | smapi.skillTesting = { 195 | validate: (skillId, stage, locales) => smapi.rest.post(`/v1/skills/${skillId}/stages/${stage}/validations`, { locales }), 196 | validationStatus: (skillId, stage, validationId) => smapi.rest.get(`/v1/skills/${skillId}/stages/${stage}/validations/${validationId}`), 197 | invoke: (skillId, endpointRegion, skillRequest) => smapi.rest.post(`/${smapi.version}/skills/${skillId}/invocations`, { endpointRegion, skillRequest }), 198 | simulate: (skillId, content, locale) => smapi.rest.post(`/${smapi.version}/skills/${skillId}/simulations`, { input: { content }, device: { locale } }), 199 | simulationStatus: (skillId, requestId) => smapi.rest.get(`/${smapi.version}/skills/${skillId}/simulations/${requestId}`) 200 | }; 201 | 202 | smapi.intentRequests = { 203 | list: (params) => { 204 | const skillId = params.skillId; 205 | delete params.skillId; 206 | return smapi.rest.get(`/v1/skills/${skillId}/history/intentRequests`, params); 207 | } 208 | }; 209 | 210 | smapi.custom = { 211 | head: url => smapi.rest.head(url), 212 | get: (url, parameters) => smapi.rest.get(url, parameters), 213 | post: (url, parameters) => smapi.rest.post(url, parameters), 214 | put: (url, parameters) => smapi.rest.put(url, parameters), 215 | delete: url => smapi.rest.delete(url) 216 | }; 217 | 218 | return smapi; 219 | } 220 | 221 | module.exports = alexaSMAPI; 222 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-alexa-smapi", 3 | "version": "1.1.1", 4 | "description": "A node.js client library for using the Alexa Skill Management API.", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "test": "mocha", 9 | "test-integration": "TEST_TYPE=integration npm test", 10 | "test-certification": "TEST_TYPE=certification npm test", 11 | "test-capture": "TEST_TYPE=capture npm test", 12 | "test-with-coverage": "nyc npm test && nyc report --reporter=text-lcov | ./node_modules/coveralls/bin/coveralls.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/tejashah88/node-alexa-smapi.git" 17 | }, 18 | "keywords": [ 19 | "alexa", 20 | "alexa-skill", 21 | "skill-management", 22 | "alexa-skills-kit", 23 | "smapi" 24 | ], 25 | "author": "tejashah88", 26 | "contributors": [ 27 | "marcelobern" 28 | ], 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/tejashah88/node-alexa-smapi/issues" 32 | }, 33 | "homepage": "https://github.com/tejashah88/node-alexa-smapi#readme", 34 | "dependencies": { 35 | "axios": "^0.19.0" 36 | }, 37 | "devDependencies": { 38 | "ask-cli": "^1.7.6", 39 | "axios-mock-adapter": "^1.17.0", 40 | "chai": "^4.2.0", 41 | "chai-as-promised": "^7.1.1", 42 | "coveralls": "^3.0.4", 43 | "eslint": "^6.0.1", 44 | "mocha": "^6.1.4", 45 | "mocha-lcov-reporter": "^1.3.0", 46 | "nyc": "^14.1.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # About the node-alexa-smapi test suite 2 | This test suite can be executed in one of four modes: 3 | - **Unit** mode: will use the responses stored in test/data/responses.json to execute the test suite. 4 | - **Integration** mode: will connect live to Alexa SMAPI servers to execute the test suite (excluding certification tests). 5 | - **Certification** mode: will connect live to Alexa SMAPI servers to execute the test suite (including certification tests). 6 | - **Capture** mode: will connect live to Alexa SMAPI servers to execute the test suite (including certification tests) and store the responses in test/data/responses.json. 7 | 8 | All modes will perform the following actions: 9 | - Retrieve a valid access_token and keep it for use in all SMAPI invocations (**unit** mode will use a dummy access_token). 10 | - List all vendors and use the first vendor (account) in the list to create a test skill. 11 | - Invoke all SMAPI operations against this test skill and retry failed invocations as many as (test configuration flag) `MAX_RETRIES` times. 12 | - Will sleep before retries when receiving a `429` rate limiting status. 13 | - In all test modes except **integration** mode the following will happen: 14 | - Submit skill for certification. In all test modes except **unit** mode: **The owner of the vendor (account) should expect a certification submission email from Amazon and may receive a certification feedback on as well ;-)** 15 | - Withdraw skill from certification 16 | - Remove the test skill after all SMAPI testing is complete. 17 | - In all test modes except **unit** mode: if a test run fails, you may need to **manually clean up the test skill** created during the failed test run. **This may include withdrawing it from certification.** 18 | 19 | # About the test skill created by this test suite 20 | By default the test skill created will have: 21 | - Name: node-alexa-smapi test skill 22 | - Logo: a bug over a blue background [see it here](https://s3.amazonaws.com/node-alexa-smapi/icons/icon_512_A2Z.png). 23 | - These default values may be changed in the manifest files under test/data 24 | 25 | # node-alexa-smapi test suite requirements 26 | Please follow the steps below in order to successfully run this test suite (in all test modes except **unit** mode): 27 | - First obtain refreshToken, clientId, and clientSecret 28 | - Follow [these instructions](https://developer.amazon.com/docs/smapi/ask-cli-command-reference.html#util-command) to retrieve clientId and clientSecret. 29 | - Run `ask util generate-lwa-tokens` to generate a refreshToken (copy the value from refresh_token and use it to initialize refreshToken in the next step). 30 | - Then create and populate the file test/data/secrets.json with the following information: 31 | - refreshToken (starts with "Atzr|") 32 | - clientId 33 | - clientSecret 34 | 35 | Sample `test/data/secrets.json`: 36 | ``` 37 | { 38 | "refreshToken": "generated by ask util generate-lwa-tokens", 39 | "clientId": "amzn1.application-oa2-client.myClientId", 40 | "clientSecret": "myClientSecret" 41 | } 42 | 43 | ``` 44 | 45 | # Travis CI build requirements 46 | It is recommended to run the test suite in **unit** mode during Travis CI builds. No additional configuration is needed, as **unit** mode is the default test mode. 47 | 48 | # Test configuration options 49 | Due to the asynchronous nature of the `integration`, `certification`, and `capture` test modes, you may need to adjust one of the following configuration parameters so the test suite will complete successfully. 50 | 51 | ```javascript 52 | const TEST_TYPE = ['unit', 'integration', 'certification', 'capture'].includes(process.env.TEST_TYPE) ? process.env.TEST_TYPE : 'unit'; 53 | // TEST_TYPE = 'unit' will run unit tests locally (completes in milliseconds). This is the default value. 54 | // TEST_TYPE = 'integration' will run non-certification integration tests against SMAPI (completes in around 3 minutes). 55 | // TEST_TYPE = 'certification' same as integration plus will run the Skill Certification test cases (completes in around 20 minutes). 56 | // TEST_TYPE = 'capture' same as certification plus will capture the responses. 57 | const LOG_RESPONSES = false; // if true will output (console.log) the response received by each SMAPI operation 58 | const LOG_ERRORS = true; // if true will output (console.log) any unexpected response received from SMAPI 59 | const MAX_RETRIES = shouldMock(TEST_TYPE) ? 0 : 10; // number of times the test suite will check for completion of create/update operations before proceeding with other test cases 60 | const RETRY_TIMEOUT = shouldMock(TEST_TYPE) ? 0 : 10000; // time (in milliseconds) to wait before checking again for completion of create/update operations 61 | const WITHDRAWAL_TIMEOUT = shouldMock(TEST_TYPE) ? 0 : 1 * 60 * 1000; // time (in milliseconds) to wait before withdrawing skill from certification 62 | const MOCHA_TIMEOUT = WITHDRAWAL_TIMEOUT + 10000; // for details see https://mochajs.org/#timeouts 63 | ``` 64 | -------------------------------------------------------------------------------- /test/data/accountLinkingRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "accountLinkingRequest": { 3 | "type": "AUTH_CODE", 4 | "authorizationUrl": "https://www.example.com/auth_url", 5 | "domains": [], 6 | "clientId": "MY_CLIENT", 7 | "scopes": [], 8 | "accessTokenUrl": "https://www.example.com/accessToken_url", 9 | "clientSecret": "MY_SECRET", 10 | "accessTokenScheme": "HTTP_BASIC", 11 | "defaultTokenExpirationInSeconds": 3600 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/data/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mySecrets = process.env.TRAVIS_SECURE_ENV_VARS ? { 4 | refreshToken: process.env.REFRESH_TOKEN, 5 | clientId: process.env.CLIENT_ID, 6 | clientSecret: process.env.CLIENT_SECRET 7 | } : require('./secrets.json'); 8 | 9 | const LOCALE = 'en-US'; 10 | 11 | module.exports = { 12 | refreshToken: mySecrets.refreshToken, 13 | clientId: mySecrets.clientId, 14 | clientSecret: mySecrets.clientSecret, 15 | stage: 'development', 16 | locale: LOCALE, 17 | locales: [LOCALE], 18 | reason: 'OTHER', 19 | message: 'node-alexa-smapi testing', 20 | endpointRegion: 'Default', // string enum["Default", "NA", "EU", "FE", etc], 21 | skillRequest: { 22 | body: require('./request.json') 23 | }, 24 | simulationContent: 'api', 25 | intentRequestParams: { 26 | nextToken: null, 27 | maxResults: 10, 28 | sortDirection: 'desc', 29 | sortField: 'intent.confidence.bin', // Valid values: dialogAct.name, locale, intent.confidence.bin, stage, publicationStatus, intent.name, interactionType, or utteranceText. 30 | 'dialogAct.name': null, // Valid values: Dialog.ElicitSlot, Dialog.ConfirmSlot, or Dialog.ConfirmIntent. 31 | locale: LOCALE, // Can have multiple values and is not case-sensitive. 32 | 'intent.confidence.bin': 'high', 33 | 'intent.confidence.bin': 'medium', // eslint-disable-line no-dupe-keys 34 | 'intent.confidence.bin': 'low', // eslint-disable-line no-dupe-keys 35 | stage: 'live', 36 | stage: 'development', // eslint-disable-line no-dupe-keys 37 | publicationStatus: 'certification', 38 | publicationStatus: 'development', // eslint-disable-line no-dupe-keys 39 | utteranceText: 'api', 40 | utteranceText: 'a pie', // eslint-disable-line no-dupe-keys 41 | utteranceText: 'ape', // eslint-disable-line no-dupe-keys 42 | 'intent.name': 'testIntent', 43 | 'intent.name': 'AMAZON.HelpIntent', // eslint-disable-line no-dupe-keys 44 | 'intent.slot.name': null, // eslint-disable-line no-dupe-keys 45 | interactionType: 'ONE_SHOT', 46 | interactionType: 'MODAL' // eslint-disable-line no-dupe-keys 47 | }, 48 | v0: { 49 | skillManifest: require('./manifest_v0.json').skillManifest, 50 | interactionModel: require('./interactionModel.json').interactionModel, 51 | accountLinkingRequest: require('./accountLinkingRequest.json').accountLinkingRequest, 52 | }, 53 | v1: { 54 | skillManifest: require('./manifest_v1.json').manifest, 55 | interactionModel: require('./interactionModel.json').interactionModel, 56 | accountLinkingRequest: require('./accountLinkingRequest.json').accountLinkingRequest, 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /test/data/interactionModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "test smapi", 5 | "intents": [ 6 | { 7 | "name": "AMAZON.FallbackIntent", 8 | "samples": [] 9 | }, 10 | { 11 | "name": "AMAZON.CancelIntent", 12 | "samples": [] 13 | }, 14 | { 15 | "name": "AMAZON.HelpIntent", 16 | "samples": [] 17 | }, 18 | { 19 | "name": "AMAZON.StopIntent", 20 | "samples": [] 21 | }, 22 | { 23 | "name": "AMAZON.NavigateHomeIntent", 24 | "samples": [] 25 | }, 26 | { 27 | "name": "testIntent", 28 | "samples": [ 29 | "API" 30 | ] 31 | } 32 | ] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/data/manifest_v0.json: -------------------------------------------------------------------------------- 1 | { 2 | "skillManifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "en-US": { 6 | "summary": "Skill to test node-alexa-smapi", 7 | "examplePhrases": [ 8 | "Alexa open test smapi" 9 | ], 10 | "keywords": [], 11 | "name": "node-alexa-smapi test skill", 12 | "smallIconUri": "https://s3.amazonaws.com/node-alexa-smapi/icons/icon_108_A2Z.png", 13 | "description": "Skill to test node-alexa-smapi", 14 | "largeIconUri": "https://s3.amazonaws.com/node-alexa-smapi/icons/icon_512_A2Z.png" 15 | } 16 | }, 17 | "isAvailableWorldwide": true, 18 | "testingInstructions": "This is a dummy skill just used to test node-alexa-smapi. As such it should not be approved during certification for go live!", 19 | "category": "NOVELTY", 20 | "distributionCountries": [] 21 | }, 22 | "apis": { 23 | "custom": { 24 | "endpoint": { 25 | "sslCertificateType": "Wildcard", 26 | "uri": "https://www.example.com" 27 | }, 28 | "interfaces": [] 29 | } 30 | }, 31 | "manifestVersion": "1.0", 32 | "privacyAndCompliance": { 33 | "allowsPurchases": false, 34 | "isExportCompliant": true, 35 | "containsAds": false, 36 | "isChildDirected": false, 37 | "usesPersonalInfo": false, 38 | "locales": { 39 | "en-US": { 40 | "privacyPolicyUrl": "http://www.example.com/myprivacypolicy" 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/data/manifest_v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "en-US": { 6 | "summary": "Skill to test node-alexa-smapi", 7 | "examplePhrases": [ 8 | "Alexa open test smapi" 9 | ], 10 | "keywords": [], 11 | "name": "node-alexa-smapi test skill", 12 | "smallIconUri": "https://s3.amazonaws.com/node-alexa-smapi/icons/icon_108_A2Z.png", 13 | "description": "Skill to test node-alexa-smapi", 14 | "largeIconUri": "https://s3.amazonaws.com/node-alexa-smapi/icons/icon_512_A2Z.png" 15 | } 16 | }, 17 | "isAvailableWorldwide": true, 18 | "testingInstructions": "This is a dummy skill just used to test node-alexa-smapi. As such it should not be approved during certification for go live!", 19 | "category": "NOVELTY", 20 | "distributionCountries": [] 21 | }, 22 | "apis": { 23 | "custom": { 24 | "endpoint": { 25 | "sslCertificateType": "Wildcard", 26 | "uri": "https://www.example.com" 27 | }, 28 | "interfaces": [] 29 | } 30 | }, 31 | "manifestVersion": "1.0", 32 | "privacyAndCompliance": { 33 | "allowsPurchases": false, 34 | "isExportCompliant": true, 35 | "containsAds": false, 36 | "isChildDirected": false, 37 | "usesPersonalInfo": false 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/data/request.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "session": { 4 | "new": true, 5 | "sessionId": "amzn1.echo-api.session.[unique-value-here]", 6 | "application": { 7 | "applicationId": "TBD" 8 | }, 9 | "attributes": { 10 | "key": "string value" 11 | }, 12 | "user": { 13 | "userId": "amzn1.ask.account.[unique-value-here]", 14 | "accessToken": "Atza|AAAAAAAA...", 15 | "permissions": { 16 | "consentToken": "ZZZZZZZ..." 17 | } 18 | } 19 | }, 20 | "context": { 21 | "System": { 22 | "device": { 23 | "deviceId": "string", 24 | "supportedInterfaces": {} 25 | }, 26 | "application": { 27 | "applicationId": "TBD" 28 | }, 29 | "user": { 30 | "userId": "amzn1.ask.account.[unique-value-here]", 31 | "accessToken": "Atza|AAAAAAAA...", 32 | "permissions": { 33 | "consentToken": "ZZZZZZZ..." 34 | } 35 | }, 36 | "apiEndpoint": "https://api.amazonalexa.com", 37 | "apiAccessToken": "AxThk..." 38 | } 39 | }, 40 | "request": {} 41 | } 42 | -------------------------------------------------------------------------------- /test/data/responses.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "/v0/vendors", 4 | "method": "get", 5 | "status": 200, 6 | "headers": {}, 7 | "data": { 8 | "vendors": [ 9 | { 10 | "id": "VENDOR_ID", 11 | "name": "Tester", 12 | "roles": [ 13 | "ROLE_ADMINISTRATOR" 14 | ] 15 | } 16 | ] 17 | } 18 | }, 19 | { 20 | "url": "/v0/skills", 21 | "method": "post", 22 | "status": 202, 23 | "headers": { 24 | "location": "/v0/skills/SKILL_ID/status" 25 | }, 26 | "data": { 27 | "skillId": "SKILL_ID" 28 | } 29 | }, 30 | { 31 | "url": "/v0/skills", 32 | "method": "get", 33 | "status": 200, 34 | "headers": {}, 35 | "data": { 36 | "_links": { 37 | "self": { 38 | "href": "/v0/skills?vendorId=VENDOR_ID&maxResults=10" 39 | } 40 | }, 41 | "isTruncated": false, 42 | "skills": [ 43 | { 44 | "_links": { 45 | "self": { 46 | "href": "/v0/skills/SKILL_ID" 47 | } 48 | }, 49 | "lastUpdated": "TIMESTAMP", 50 | "nameByLocale": { 51 | "en-US": "node-alexa-smapi test skill" 52 | }, 53 | "skillId": "SKILL_ID", 54 | "stage": "development" 55 | } 56 | ] 57 | } 58 | }, 59 | { 60 | "url": "/v0/skills/SKILL_ID/status", 61 | "method": "get", 62 | "status": 200, 63 | "headers": {}, 64 | "data": { 65 | "manifest": { 66 | "lastModified": { 67 | "time": "TIMESTAMP", 68 | "status": "SUCCESSFUL" 69 | } 70 | } 71 | } 72 | }, 73 | { 74 | "url": "/v0/skills/SKILL_ID", 75 | "method": "get", 76 | "status": 200, 77 | "headers": { 78 | "etag": "1fcc91186b360a2ff59d631037711820" 79 | }, 80 | "data": { 81 | "skillManifest": { 82 | "publishingInformation": { 83 | "locales": { 84 | "en-US": { 85 | "summary": "Skill to test node-alexa-smapi", 86 | "examplePhrases": [ 87 | "Alexa open test smapi" 88 | ], 89 | "keywords": [], 90 | "name": "node-alexa-smapi test skill", 91 | "smallIconUri": "https://s3.amazonaws.com/node-alexa-smapi/icons/icon_108_A2Z.png", 92 | "description": "Skill to test node-alexa-smapi", 93 | "largeIconUri": "https://s3.amazonaws.com/node-alexa-smapi/icons/icon_512_A2Z.png" 94 | } 95 | }, 96 | "isAvailableWorldwide": true, 97 | "testingInstructions": "This is a dummy skill just used to test node-alexa-smapi. As such it should not be approved during certification for go live!", 98 | "category": "NOVELTY", 99 | "distributionCountries": [] 100 | }, 101 | "apis": { 102 | "custom": { 103 | "endpoint": { 104 | "sslCertificateType": "Wildcard", 105 | "uri": "https://www.example.com" 106 | }, 107 | "interfaces": [] 108 | } 109 | }, 110 | "manifestVersion": "1.0", 111 | "privacyAndCompliance": { 112 | "allowsPurchases": false, 113 | "locales": { 114 | "en-US": { 115 | "privacyPolicyUrl": "http://www.example.com/myprivacypolicy" 116 | } 117 | }, 118 | "isExportCompliant": true, 119 | "containsAds": false, 120 | "isChildDirected": false, 121 | "usesPersonalInfo": false 122 | } 123 | } 124 | } 125 | }, 126 | { 127 | "url": "/v0/skills/SKILL_ID", 128 | "method": "put", 129 | "status": 202, 130 | "headers": { 131 | "location": "/v0/skills/SKILL_ID/status" 132 | }, 133 | "data": {} 134 | }, 135 | { 136 | "url": "/v0/skills/SKILL_ID/status", 137 | "method": "get", 138 | "status": 200, 139 | "headers": {}, 140 | "data": { 141 | "manifest": { 142 | "lastModified": { 143 | "time": "TIMESTAMP", 144 | "status": "SUCCESSFUL" 145 | } 146 | } 147 | } 148 | }, 149 | { 150 | "url": "/v1/skills/SKILL_ID/history/intentRequests", 151 | "method": "get", 152 | "status": 200, 153 | "headers": {}, 154 | "data": { 155 | "_links": { 156 | "next": {}, 157 | "self": { 158 | "href": "/v1/skills/SKILL_ID/history/intentRequests?stage=development&locale=en-US&intent.confidence.bin=low&intent.name=AMAZON.HelpIntent&publicationStatus=development&interactionType=MODAL&utteranceText=ape&maxResults=10&sortDirection=desc&sortField=intent.confidence.bin" 159 | } 160 | }, 161 | "isTruncated": false, 162 | "items": [], 163 | "skillId": "SKILL_ID", 164 | "startIndex": 0, 165 | "totalCount": 0 166 | } 167 | }, 168 | { 169 | "url": "/v0/skills/SKILL_ID/interactionModel/locales/en-US", 170 | "method": "post", 171 | "status": 202, 172 | "headers": { 173 | "location": "/v0/skills/SKILL_ID/interactionModel/locales/en-US/status" 174 | }, 175 | "data": {} 176 | }, 177 | { 178 | "url": "/v0/skills/SKILL_ID/interactionModel/locales/en-US/status", 179 | "method": "get", 180 | "status": 200, 181 | "headers": {}, 182 | "data": { 183 | "status": "SUCCESS" 184 | } 185 | }, 186 | { 187 | "url": "/v0/skills/SKILL_ID/interactionModel/locales/en-US", 188 | "method": "head", 189 | "status": 200, 190 | "headers": { 191 | "etag": "e86999354016f13af5107f711098642d" 192 | }, 193 | "data": "" 194 | }, 195 | { 196 | "url": "/v0/skills/SKILL_ID/interactionModel/locales/en-US", 197 | "method": "get", 198 | "status": 200, 199 | "headers": { 200 | "etag": "e86999354016f13af5107f711098642d" 201 | }, 202 | "data": { 203 | "interactionModel": { 204 | "languageModel": { 205 | "invocationName": "test smapi", 206 | "intents": [ 207 | { 208 | "name": "AMAZON.FallbackIntent", 209 | "samples": [] 210 | }, 211 | { 212 | "name": "AMAZON.CancelIntent", 213 | "samples": [] 214 | }, 215 | { 216 | "name": "AMAZON.HelpIntent", 217 | "samples": [] 218 | }, 219 | { 220 | "name": "AMAZON.StopIntent", 221 | "samples": [] 222 | }, 223 | { 224 | "name": "AMAZON.NavigateHomeIntent", 225 | "samples": [] 226 | }, 227 | { 228 | "name": "testIntent", 229 | "samples": [ 230 | "API" 231 | ] 232 | } 233 | ] 234 | } 235 | } 236 | } 237 | }, 238 | { 239 | "url": "/v0/skills/SKILL_ID/accountLinkingClient", 240 | "method": "put", 241 | "status": 204, 242 | "headers": { 243 | "etag": "ac947106382b260e0617e57b17b70a6e" 244 | }, 245 | "data": "" 246 | }, 247 | { 248 | "url": "/v0/skills/SKILL_ID/accountLinkingClient", 249 | "method": "get", 250 | "status": 200, 251 | "headers": { 252 | "etag": "ac947106382b260e0617e57b17b70a6e" 253 | }, 254 | "data": { 255 | "accountLinkingResponse": { 256 | "defaultTokenExpirationInSeconds": 3600, 257 | "clientId": "MY_CLIENT", 258 | "authorizationUrl": "https://www.example.com/auth_url", 259 | "accessTokenUrl": "https://www.example.com/accessToken_url", 260 | "domains": [], 261 | "accessTokenScheme": "HTTP_BASIC", 262 | "type": "AUTH_CODE" 263 | } 264 | } 265 | }, 266 | { 267 | "url": "/v1/skills/SKILL_ID/stages/development/enablement", 268 | "method": "put", 269 | "status": 204, 270 | "headers": {}, 271 | "data": "" 272 | }, 273 | { 274 | "url": "/v1/skills/SKILL_ID/stages/development/enablement", 275 | "method": "get", 276 | "status": 204, 277 | "headers": {}, 278 | "data": "" 279 | }, 280 | { 281 | "url": "/v0/skills/SKILL_ID/invocations", 282 | "method": "post", 283 | "status": 500, 284 | "headers": {}, 285 | "data": { 286 | "message": "An unexpected error occurred." 287 | } 288 | }, 289 | { 290 | "url": "/v0/skills/SKILL_ID/simulations", 291 | "method": "post", 292 | "status": 200, 293 | "headers": {}, 294 | "data": { 295 | "id": "SIMULATION_ID", 296 | "status": "IN_PROGRESS" 297 | } 298 | }, 299 | { 300 | "url": "/v0/skills/SKILL_ID/simulations/SIMULATION_ID", 301 | "method": "get", 302 | "status": 200, 303 | "headers": {}, 304 | "data": { 305 | "id": "SIMULATION_ID", 306 | "status": "IN_PROGRESS" 307 | } 308 | }, 309 | { 310 | "url": "/v1/skills/SKILL_ID/stages/development/enablement", 311 | "method": "delete", 312 | "status": 204, 313 | "headers": {}, 314 | "data": "" 315 | }, 316 | { 317 | "url": "/v0/skills/SKILL_ID/submit", 318 | "method": "post", 319 | "status": 202, 320 | "headers": { 321 | "location": "/v0/skills/SKILL_ID/stages/certification/" 322 | }, 323 | "data": {} 324 | }, 325 | { 326 | "url": "/v0/skills/SKILL_ID/withdraw", 327 | "method": "post", 328 | "status": 204, 329 | "headers": {}, 330 | "data": "" 331 | }, 332 | { 333 | "url": "/v0/skills/SKILL_ID", 334 | "method": "delete", 335 | "status": 204, 336 | "headers": {}, 337 | "data": "" 338 | }, 339 | { 340 | "url": "/v1/vendors", 341 | "method": "get", 342 | "status": 200, 343 | "headers": {}, 344 | "data": { 345 | "vendors": [ 346 | { 347 | "id": "VENDOR_ID", 348 | "name": "Tester", 349 | "roles": [ 350 | "ROLE_ADMINISTRATOR" 351 | ] 352 | } 353 | ] 354 | } 355 | }, 356 | { 357 | "url": "/v1/skills", 358 | "method": "post", 359 | "status": 202, 360 | "headers": { 361 | "location": "/v1/skills/SKILL_ID/status?resource=manifest" 362 | }, 363 | "data": { 364 | "skillId": "SKILL_ID" 365 | } 366 | }, 367 | { 368 | "url": "/v1/skills", 369 | "method": "get", 370 | "status": 200, 371 | "headers": {}, 372 | "data": { 373 | "_links": { 374 | "self": { 375 | "href": "/v1/skills?vendorId=VENDOR_ID&maxResults=10" 376 | } 377 | }, 378 | "isTruncated": false, 379 | "skills": [] 380 | } 381 | }, 382 | { 383 | "url": "/v1/skills/SKILL_ID/status", 384 | "method": "get", 385 | "status": 200, 386 | "headers": {}, 387 | "data": { 388 | "manifest": { 389 | "eTag": "b53b3d5bf2b17cdde95cb19f7fdda7ce", 390 | "lastUpdateRequest": { 391 | "status": "SUCCEEDED" 392 | } 393 | } 394 | } 395 | }, 396 | { 397 | "url": "/v1/skills/SKILL_ID/stages/development/manifest", 398 | "method": "get", 399 | "status": 200, 400 | "headers": { 401 | "etag": "b53b3d5bf2b17cdde95cb19f7fdda7ce" 402 | }, 403 | "data": { 404 | "manifest": { 405 | "publishingInformation": { 406 | "locales": { 407 | "en-US": { 408 | "summary": "Skill to test node-alexa-smapi", 409 | "examplePhrases": [ 410 | "Alexa open test smapi" 411 | ], 412 | "keywords": [], 413 | "name": "node-alexa-smapi test skill", 414 | "smallIconUri": "https://s3.amazonaws.com/node-alexa-smapi/icons/icon_108_A2Z.png", 415 | "description": "Skill to test node-alexa-smapi", 416 | "largeIconUri": "https://s3.amazonaws.com/node-alexa-smapi/icons/icon_512_A2Z.png" 417 | } 418 | }, 419 | "isAvailableWorldwide": true, 420 | "testingInstructions": "This is a dummy skill just used to test node-alexa-smapi. As such it should not be approved during certification for go live!", 421 | "category": "NOVELTY", 422 | "distributionCountries": [] 423 | }, 424 | "apis": { 425 | "custom": { 426 | "endpoint": { 427 | "sslCertificateType": "Wildcard", 428 | "uri": "https://www.example.com" 429 | }, 430 | "interfaces": [] 431 | } 432 | }, 433 | "manifestVersion": "1.0", 434 | "privacyAndCompliance": { 435 | "allowsPurchases": false, 436 | "isExportCompliant": true, 437 | "containsAds": false, 438 | "isChildDirected": false, 439 | "usesPersonalInfo": false 440 | } 441 | } 442 | } 443 | }, 444 | { 445 | "url": "/v1/skills/SKILL_ID/stages/development/manifest", 446 | "method": "put", 447 | "status": 202, 448 | "headers": { 449 | "location": "/v1/skills/SKILL_ID/status?resource=manifest" 450 | }, 451 | "data": {} 452 | }, 453 | { 454 | "url": "/v1/skills/SKILL_ID/status", 455 | "method": "get", 456 | "status": 200, 457 | "headers": {}, 458 | "data": { 459 | "manifest": { 460 | "eTag": "b53b3d5bf2b17cdde95cb19f7fdda7ce", 461 | "lastUpdateRequest": { 462 | "status": "SUCCEEDED" 463 | } 464 | } 465 | } 466 | }, 467 | { 468 | "url": "/v1/skills/SKILL_ID/history/intentRequests", 469 | "method": "get", 470 | "status": 200, 471 | "headers": {}, 472 | "data": { 473 | "_links": { 474 | "next": {}, 475 | "self": { 476 | "href": "/v1/skills/SKILL_ID/history/intentRequests?stage=development&locale=en-US&intent.confidence.bin=low&intent.name=AMAZON.HelpIntent&publicationStatus=development&interactionType=MODAL&utteranceText=ape&maxResults=10&sortDirection=desc&sortField=intent.confidence.bin" 477 | } 478 | }, 479 | "isTruncated": false, 480 | "items": [], 481 | "skillId": "SKILL_ID", 482 | "startIndex": 0, 483 | "totalCount": 0 484 | } 485 | }, 486 | { 487 | "url": "/v1/skills/SKILL_ID/stages/development/interactionModel/locales/en-US", 488 | "method": "put", 489 | "status": 202, 490 | "headers": { 491 | "location": "/v1/skills/SKILL_ID/status?resource=interactionModel" 492 | }, 493 | "data": {} 494 | }, 495 | { 496 | "url": "/v1/skills/SKILL_ID/status?resource=interactionModel", 497 | "method": "get", 498 | "status": 200, 499 | "headers": {}, 500 | "data": { 501 | "interactionModel": { 502 | "en-US": { 503 | "lastUpdateRequest": { 504 | "status": "SUCCEEDED" 505 | } 506 | } 507 | } 508 | } 509 | }, 510 | { 511 | "url": "/v1/skills/SKILL_ID/stages/development/interactionModel/locales/en-US", 512 | "method": "head", 513 | "status": 204, 514 | "headers": { 515 | "etag": "e86999354016f13af5107f711098642d" 516 | }, 517 | "data": "" 518 | }, 519 | { 520 | "url": "/v1/skills/SKILL_ID/stages/development/interactionModel/locales/en-US", 521 | "method": "get", 522 | "status": 200, 523 | "headers": { 524 | "etag": "e86999354016f13af5107f711098642d" 525 | }, 526 | "data": { 527 | "interactionModel": { 528 | "languageModel": { 529 | "invocationName": "test smapi", 530 | "intents": [ 531 | { 532 | "name": "AMAZON.FallbackIntent", 533 | "samples": [] 534 | }, 535 | { 536 | "name": "AMAZON.CancelIntent", 537 | "samples": [] 538 | }, 539 | { 540 | "name": "AMAZON.HelpIntent", 541 | "samples": [] 542 | }, 543 | { 544 | "name": "AMAZON.StopIntent", 545 | "samples": [] 546 | }, 547 | { 548 | "name": "AMAZON.NavigateHomeIntent", 549 | "samples": [] 550 | }, 551 | { 552 | "name": "testIntent", 553 | "samples": [ 554 | "API" 555 | ] 556 | } 557 | ] 558 | } 559 | } 560 | } 561 | }, 562 | { 563 | "url": "/v1/skills/SKILL_ID/stages/development/accountLinkingClient", 564 | "method": "put", 565 | "status": 204, 566 | "headers": { 567 | "etag": "ac947106382b260e0617e57b17b70a6e" 568 | }, 569 | "data": "" 570 | }, 571 | { 572 | "url": "/v1/skills/SKILL_ID/stages/development/accountLinkingClient", 573 | "method": "get", 574 | "status": 200, 575 | "headers": { 576 | "etag": "ac947106382b260e0617e57b17b70a6e" 577 | }, 578 | "data": { 579 | "accountLinkingResponse": { 580 | "accessTokenScheme": "HTTP_BASIC", 581 | "accessTokenUrl": "https://www.example.com/accessToken_url", 582 | "authorizationUrl": "https://www.example.com/auth_url", 583 | "clientId": "MY_CLIENT", 584 | "defaultTokenExpirationInSeconds": 3600, 585 | "domains": [], 586 | "type": "AUTH_CODE" 587 | } 588 | } 589 | }, 590 | { 591 | "url": "/v1/skills/SKILL_ID/stages/development/accountLinkingClient", 592 | "method": "delete", 593 | "status": 204, 594 | "headers": {}, 595 | "data": "" 596 | }, 597 | { 598 | "url": "/v1/skills/SKILL_ID/stages/development/enablement", 599 | "method": "put", 600 | "status": 204, 601 | "headers": {}, 602 | "data": "" 603 | }, 604 | { 605 | "url": "/v1/skills/SKILL_ID/stages/development/enablement", 606 | "method": "get", 607 | "status": 204, 608 | "headers": {}, 609 | "data": "" 610 | }, 611 | { 612 | "url": "/v1/skills/SKILL_ID/stages/development/validations", 613 | "method": "post", 614 | "status": 200, 615 | "headers": {}, 616 | "data": { 617 | "id": "VALIDATION_ID", 618 | "status": "IN_PROGRESS" 619 | } 620 | }, 621 | { 622 | "url": "/v1/skills/SKILL_ID/stages/development/validations/VALIDATION_ID", 623 | "method": "get", 624 | "status": 200, 625 | "headers": {}, 626 | "data": { 627 | "id": "VALIDATION_ID", 628 | "status": "IN_PROGRESS" 629 | } 630 | }, 631 | { 632 | "url": "/v1/skills/SKILL_ID/invocations", 633 | "method": "post", 634 | "status": 500, 635 | "headers": {}, 636 | "data": { 637 | "message": "An unexpected error occurred." 638 | } 639 | }, 640 | { 641 | "url": "/v1/skills/SKILL_ID/simulations", 642 | "method": "post", 643 | "status": 200, 644 | "headers": {}, 645 | "data": { 646 | "id": "SIMULATION_ID", 647 | "status": "IN_PROGRESS" 648 | } 649 | }, 650 | { 651 | "url": "/v1/skills/SKILL_ID/simulations/SIMULATION_ID", 652 | "method": "get", 653 | "status": 200, 654 | "headers": {}, 655 | "data": { 656 | "id": "SIMULATION_ID", 657 | "status": "IN_PROGRESS" 658 | } 659 | }, 660 | { 661 | "url": "/v1/skills/SKILL_ID/stages/development/enablement", 662 | "method": "delete", 663 | "status": 204, 664 | "headers": {}, 665 | "data": "" 666 | }, 667 | { 668 | "url": "/v1/skills/SKILL_ID/submit", 669 | "method": "post", 670 | "status": 202, 671 | "headers": { 672 | "location": "/v1/skills?vendorId=VENDOR_ID&skillId=SKILL_ID" 673 | }, 674 | "data": {} 675 | }, 676 | { 677 | "url": "/v1/skills", 678 | "method": "get", 679 | "status": 200, 680 | "headers": {}, 681 | "data": { 682 | "_links": { 683 | "self": { 684 | "href": "/v1/skills?vendorId=VENDOR_ID&skillId=SKILL_ID" 685 | } 686 | }, 687 | "isTruncated": false, 688 | "skills": [ 689 | { 690 | "_links": { 691 | "self": { 692 | "href": "/v1/skills/SKILL_ID/stages/development/manifest" 693 | } 694 | }, 695 | "apis": [ 696 | "custom" 697 | ], 698 | "lastUpdated": "TIMESTAMP", 699 | "nameByLocale": { 700 | "en-US": "node-alexa-smapi test skill" 701 | }, 702 | "publicationStatus": "CERTIFICATION", 703 | "skillId": "SKILL_ID", 704 | "stage": "development" 705 | } 706 | ] 707 | } 708 | }, 709 | { 710 | "url": "/v1/skills/SKILL_ID/withdraw", 711 | "method": "post", 712 | "status": 204, 713 | "headers": {}, 714 | "data": "" 715 | }, 716 | { 717 | "url": "/v1/skills/SKILL_ID", 718 | "method": "delete", 719 | "status": 204, 720 | "headers": {}, 721 | "data": "" 722 | }, 723 | { 724 | "url": "/v1/skills/badSkill", 725 | "method": "head", 726 | "status": 405, 727 | "headers": {}, 728 | "data": "" 729 | }, 730 | { 731 | "url": "/v1/skills/badSkill", 732 | "method": "get", 733 | "status": 405, 734 | "headers": {}, 735 | "data": "\n
\nThe requested method GET is not allowed for the URL /v1/skills/badSkill.
\n\n" 736 | }, 737 | { 738 | "url": "/v1/skills/badSkill", 739 | "method": "post", 740 | "status": 405, 741 | "headers": {}, 742 | "data": "\n\nThe requested method POST is not allowed for the URL /v1/skills/badSkill.
\n\n" 743 | }, 744 | { 745 | "url": "/v1/skills/badSkill", 746 | "method": "put", 747 | "status": 405, 748 | "headers": {}, 749 | "data": "\n\nThe requested method PUT is not allowed for the URL /v1/skills/badSkill.
\n\n" 750 | }, 751 | { 752 | "url": "/v1/skills/badSkill", 753 | "method": "delete", 754 | "status": 400, 755 | "headers": {}, 756 | "data": { 757 | "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\\-]+)" 758 | } 759 | } 760 | ] -------------------------------------------------------------------------------- /test/test_smapi_client.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict'; 3 | 4 | var shouldMock = (testType) => ['unit'].includes(testType); 5 | var shouldCertify = (testType) => ['unit', 'certification', 'capture'].includes(testType); 6 | var shouldCapture = (testType) => ['capture'].includes(testType); 7 | 8 | /* 9 | * Configurable test suite parameters 10 | */ 11 | const TEST_TYPE = ['unit', 'integration', 'certification', 'capture'].includes(process.env.TEST_TYPE) ? process.env.TEST_TYPE : 'unit'; 12 | // TEST_TYPE = 'unit' will run unit tests locally (completes in milliseconds). This is the default value. 13 | // TEST_TYPE = 'integration' will run non-certification integration tests against SMAPI (completes in around 3 minutes). 14 | // TEST_TYPE = 'certification' same as integration plus will run the Skill Certification test cases (completes in around 20 minutes). 15 | // TEST_TYPE = 'capture' same as certification plus will capture the responses. 16 | const LOG_RESPONSES = false; // if true will output (console.log) the response received by each SMAPI operation 17 | const LOG_ERRORS = true; // if true will output (console.log) any unexpected response received from SMAPI 18 | const MAX_RETRIES = shouldMock(TEST_TYPE) ? 0 : 10; // number of times the test suite will check for completion of create/update operations before proceeding with other test cases 19 | const RETRY_TIMEOUT = shouldMock(TEST_TYPE) ? 0 : 10000; // time (in milliseconds) to wait before checking again for completion of create/update operations 20 | const WITHDRAWAL_TIMEOUT = shouldMock(TEST_TYPE) ? 0 : 1 * 60 * 1000; // time (in milliseconds) to wait before withdrawing skill from certification 21 | const MOCHA_TIMEOUT = WITHDRAWAL_TIMEOUT + 10000; // for details see https://mochajs.org/#timeouts 22 | 23 | var chai = require('chai'); 24 | var chaiAsPromised = require('chai-as-promised'); 25 | chai.use(chaiAsPromised); 26 | var expect = chai.expect; 27 | chai.config.includeStack = true; 28 | 29 | const SMAPI_CLIENT = require('../index'); 30 | if (shouldMock(TEST_TYPE)) require('./utils/mockAdapter'); 31 | 32 | const VERSION_0 = 'v0'; 33 | const VERSION_1 = 'v1'; 34 | const SUPPORTED_VERSIONS = [VERSION_0, VERSION_1]; 35 | const SKILL_READY = { 36 | v0: 'SUCCESSFUL', 37 | v1: 'SUCCEEDED' 38 | }; 39 | const MODEL_READY = { 40 | v0: 'SUCCESS', 41 | v1: 'SUCCEEDED' 42 | }; 43 | const BAD_URL = '/v1/skills/badSkill'; 44 | var testData = require('./data/common'); 45 | 46 | function showResponse(response) { 47 | if (LOG_RESPONSES) console.log(JSON.stringify(response, null, ' ')); // eslint-disable-line no-console 48 | } 49 | 50 | function showError(error) { 51 | if (LOG_ERRORS) console.log(JSON.stringify(error, null, ' ')); // eslint-disable-line no-console 52 | } 53 | 54 | function errorSummary(error) { 55 | const expectedErrorKeywords = /badSkill|invocations/; 56 | // should only add error responses for invoke & custom methods 57 | if (expectedErrorKeywords.test(error.config.url)) responses.add(error); 58 | return { 59 | status: error.status, 60 | statusText: error.statusText, 61 | data: error.data 62 | }; 63 | } 64 | 65 | var sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); 66 | 67 | function retry(error) { 68 | const summary = errorSummary(error); 69 | if (summary.status === 429) { 70 | console.log(`---> Operation status: ${summary.status} (${summary.data.message}) - will sleep for ${RETRY_TIMEOUT/1000}s & retry <---`); // eslint-disable-line no-console 71 | return sleep(RETRY_TIMEOUT).then((summary) => { 72 | return summary; 73 | }); 74 | } else { 75 | showError(summary); 76 | return summary; 77 | } 78 | } 79 | 80 | var responses = require('./utils/responses'); 81 | 82 | describe('Testing node-alexa-smapi', function() { 83 | after(function(){ 84 | // Persist response template 85 | if (shouldCapture(TEST_TYPE)) responses.persist('./test/data/responses.json'); 86 | }); 87 | 88 | SUPPORTED_VERSIONS.forEach(function(TEST_VERSION) { 89 | 90 | function waitOnCertification(error) { 91 | const summary = errorSummary(error); 92 | if (summary.status === 400 && (( 93 | TEST_VERSION === VERSION_0 && summary.data.violations && 94 | summary.data.violations[0].message.includes('No skill submission record found') 95 | ) || ( 96 | TEST_VERSION === VERSION_1 && summary.data.message && 97 | summary.data.message.includes('No skill submission record found') 98 | ))) { 99 | showError(summary); 100 | return {status: 204, etag: undefined, location: undefined}; 101 | } else if (summary.status < 200 || summary.status >= 300) { 102 | showError(summary); 103 | console.log(`---> Operation status: ${summary.status} (${summary.data.message}) - will sleep for ${WITHDRAWAL_TIMEOUT/1000}s & retry <---`); // eslint-disable-line no-console 104 | return sleep(WITHDRAWAL_TIMEOUT).then((summary) => { 105 | return summary; 106 | }); 107 | } else return summary; 108 | } 109 | 110 | function waitOnSkill(response) { 111 | showResponse(response); 112 | var status; 113 | if (TEST_VERSION === VERSION_0) status = response.manifest.lastModified.status; 114 | else if (TEST_VERSION === VERSION_1) status = response.manifest.lastUpdateRequest.status; 115 | if (status !== SKILL_READY[TEST_VERSION]) { 116 | console.log(`---> Skill building: ${status} - will sleep for ${RETRY_TIMEOUT/1000}s & retry <---`); // eslint-disable-line no-console 117 | return sleep(RETRY_TIMEOUT).then((response) => { 118 | return response; 119 | }); 120 | } else { 121 | console.log(`---> Skill building: ${status} <---`); // eslint-disable-line no-console 122 | return response; 123 | } 124 | } 125 | 126 | function waitOnModel(response) { 127 | showResponse(response); 128 | var status; 129 | if (TEST_VERSION === VERSION_0) status = response.status; 130 | else if (TEST_VERSION === VERSION_1) { 131 | status = response.interactionModel[testData.locale].lastUpdateRequest.status; 132 | delete response.interactionModel[testData.locale].eTag; 133 | } 134 | if (status !== MODEL_READY[TEST_VERSION]) { 135 | console.log(`---> Model building: ${status} - will sleep for ${RETRY_TIMEOUT/1000}s & retry <---`); // eslint-disable-line no-console 136 | return sleep(RETRY_TIMEOUT).then((response) => { 137 | return response; 138 | }); 139 | } else { 140 | console.log(`---> Model building: ${status} <---`); // eslint-disable-line no-console 141 | return response; 142 | } 143 | } 144 | 145 | describe('Testing with SMAPI ' + TEST_VERSION + ' for region NA', function() { 146 | after(function(){ 147 | // Persist response template 148 | if (shouldCapture(TEST_TYPE)) { 149 | if (TEST_VERSION === VERSION_0) responses.sanitize({ 150 | VENDOR_ID: testData.vendorId, 151 | SKILL_ID: testData.skillId, 152 | SIMULATION_ID: testData.simulationId 153 | }); 154 | else if (TEST_VERSION === VERSION_1) responses.sanitize({ 155 | VENDOR_ID: testData.vendorId, 156 | SKILL_ID: testData.skillId, 157 | SIMULATION_ID: testData.simulationId, 158 | VALIDATION_ID: testData.validationId 159 | }); 160 | } 161 | }); 162 | 163 | this.slow(1500); 164 | this.retries(MAX_RETRIES); 165 | this.timeout(MOCHA_TIMEOUT); 166 | const smapiClient = SMAPI_CLIENT({ 167 | version: TEST_VERSION, 168 | region: 'NA' 169 | }); 170 | if (shouldCapture(TEST_TYPE)) smapiClient.rest.client.interceptors.response.use(responses.add); 171 | 172 | context('-> Token Management', function() { 173 | describe('-> refresh token', function() { 174 | var subject; 175 | 176 | beforeEach(function() { 177 | subject = testData.accessToken = smapiClient.tokens.refresh({ 178 | refreshToken: testData.refreshToken, 179 | clientId: testData.clientId, 180 | clientSecret: testData.clientSecret, 181 | }); 182 | }); 183 | 184 | it('responds with access_token and sets token for future SMAPI calls', function() { 185 | subject = subject.then(function(response) { 186 | showResponse(response); 187 | testData.accessToken = response.access_token; 188 | return response; 189 | }, retry); 190 | return expect(subject).to.eventually.have.property('access_token'); 191 | }); 192 | }); 193 | }); 194 | 195 | context('-> Vendor Operations', function() { 196 | describe('-> Get Vendor List', function() { 197 | var subject; 198 | 199 | beforeEach(function() { 200 | subject = smapiClient.vendors.list(); 201 | }); 202 | 203 | it('responds with vendors array', function() { 204 | subject = subject.then(function(response) { 205 | showResponse(response); 206 | testData.vendorId = response.vendors[0].id; 207 | return response; 208 | }, retry); 209 | return expect(subject).to.eventually.have.property('vendors'); 210 | }); 211 | }); 212 | }); 213 | 214 | context('-> Skill Operations (except delete)', function() { 215 | describe('-> Create a skill', function() { 216 | var subject; 217 | 218 | beforeEach(function() { 219 | subject = smapiClient.skills.create(testData.vendorId, testData[TEST_VERSION].skillManifest); 220 | }); 221 | 222 | it('responds with skillId', function() { 223 | subject = subject.then(function(response) { 224 | showResponse(response); 225 | testData.skillId = response.skillId; 226 | return response; 227 | }, retry); 228 | return Promise.all([ 229 | expect(subject).to.eventually.have.property('skillId'), 230 | expect(subject).to.eventually.have.property('location') 231 | ]); 232 | }); 233 | }); 234 | 235 | describe('-> List skills (first set)', function() { 236 | var subject; 237 | 238 | beforeEach(function() { 239 | subject = smapiClient.skills.list(testData.vendorId, 10); 240 | }); 241 | 242 | it('responds with list of skills', function() { 243 | subject = subject.then(function(response) { 244 | showResponse(response); 245 | return response; 246 | }, retry); 247 | return Promise.all([ 248 | expect(subject).to.eventually.have.property('_links'), 249 | expect(subject).to.eventually.have.property('isTruncated'), 250 | expect(subject).to.eventually.have.property('skills') 251 | ]); 252 | }); 253 | }); 254 | 255 | describe('-> Get the status of a skill (and wait for changes to finish)', function() { 256 | var subject; 257 | 258 | beforeEach(function() { 259 | subject = smapiClient.skills.status(testData.skillId); 260 | }); 261 | 262 | it('responds with skill status', function() { 263 | subject = subject.then(waitOnSkill, retry); 264 | if (TEST_VERSION === VERSION_0) return expect(subject).to.eventually.have.nested.property('manifest.lastModified.status', SKILL_READY[TEST_VERSION]); 265 | else if (TEST_VERSION === VERSION_1) return expect(subject).to.eventually.have.nested.property('manifest.lastUpdateRequest.status', SKILL_READY[TEST_VERSION]); 266 | }); 267 | }); 268 | 269 | describe('-> Get Skill Information', function() { 270 | var subject; 271 | 272 | beforeEach(function() { 273 | if (TEST_VERSION === VERSION_0) subject = smapiClient.skills.getManifest(testData.skillId); 274 | else if (TEST_VERSION === VERSION_1) subject = smapiClient.skills.getManifest(testData.skillId, testData.stage); 275 | }); 276 | 277 | it('responds with skill manifest', function() { 278 | subject = subject.then(function(response) { 279 | showResponse(response); 280 | return response; 281 | }, retry); 282 | if (TEST_VERSION === VERSION_0) return expect(subject).to.eventually.have.property('skillManifest'); 283 | else if (TEST_VERSION === VERSION_1) return expect(subject).to.eventually.have.property('manifest'); 284 | }); 285 | }); 286 | 287 | describe('-> Update an existing skill', function() { 288 | var subject; 289 | 290 | beforeEach(function() { 291 | if (TEST_VERSION === VERSION_0) subject = smapiClient.skills.update(testData.skillId, testData[TEST_VERSION].skillManifest); 292 | else if (TEST_VERSION === VERSION_1) subject = smapiClient.skills.update(testData.skillId, testData.stage, testData[TEST_VERSION].skillManifest); 293 | }); 294 | 295 | it('responds with skill manifest', function() { 296 | subject = subject.then(function(response) { 297 | showResponse(response); 298 | return response; 299 | }, retry); 300 | return Promise.all([ 301 | expect(subject).to.eventually.have.property('location'), 302 | expect(subject).to.eventually.have.property('etag') 303 | ]); 304 | }); 305 | }); 306 | 307 | describe('-> Get the status of a skill (and wait for changes to finish)', function() { 308 | var subject; 309 | 310 | beforeEach(function() { 311 | subject = smapiClient.skills.status(testData.skillId); 312 | }); 313 | 314 | it('responds with skill status', function() { 315 | subject = subject.then(waitOnSkill, retry); 316 | if (TEST_VERSION === VERSION_0) return expect(subject).to.eventually.have.nested.property('manifest.lastModified.status', SKILL_READY[TEST_VERSION]); 317 | else if (TEST_VERSION === VERSION_1) return expect(subject).to.eventually.have.nested.property('manifest.lastUpdateRequest.status', SKILL_READY[TEST_VERSION]); 318 | }); 319 | }); 320 | }); 321 | 322 | context('-> Intent Request History Operations', function() { 323 | describe('-> Get intent requests', function() { 324 | var subject; 325 | 326 | beforeEach(function() { 327 | testData.intentRequestParams.skillId = testData.skillId; 328 | subject = smapiClient.intentRequests.list(testData.intentRequestParams); 329 | }); 330 | 331 | it('responds with no content', function() { 332 | subject = subject.then(function(response) { 333 | showResponse(response); 334 | return response; 335 | }, retry); 336 | return expect(subject).to.eventually.have.property('items'); 337 | }); 338 | }); 339 | }); 340 | 341 | context('-> Interaction Model Operations', function() { 342 | describe('-> Update Interaction Model', function() { 343 | var subject; 344 | 345 | beforeEach(function() { 346 | if (TEST_VERSION === VERSION_0) subject = smapiClient.interactionModel.update(testData.skillId, testData.locale, testData[TEST_VERSION].interactionModel); 347 | else if (TEST_VERSION === VERSION_1) subject = smapiClient.interactionModel.update(testData.skillId, testData.stage, testData.locale, testData[TEST_VERSION].interactionModel); 348 | }); 349 | 350 | it('responds with interaction model location', function() { 351 | subject = subject.then(function(response) { 352 | showResponse(response); 353 | return response; 354 | }, retry); 355 | return Promise.all([ 356 | expect(subject).to.eventually.have.property('location'), 357 | expect(subject).to.eventually.have.property('etag') 358 | ]); 359 | }); 360 | }); 361 | 362 | describe('-> Get the Interaction Model Building Status', function() { 363 | var subject; 364 | 365 | beforeEach(function() { 366 | subject = smapiClient.interactionModel.getStatus(testData.skillId, testData.locale); 367 | }); 368 | 369 | it('responds with interaction model status', function() { 370 | subject = subject.then(waitOnModel, retry); 371 | if (TEST_VERSION === VERSION_0) return expect(subject).to.eventually.have.property('status', MODEL_READY[TEST_VERSION]); 372 | else if (TEST_VERSION === VERSION_1) return expect(subject).to.eventually.become({ 373 | 'status': 200, 374 | 'interactionModel': { 375 | 'en-US': { 376 | 'lastUpdateRequest': { 377 | 'status': MODEL_READY[TEST_VERSION] 378 | } 379 | } 380 | } 381 | }); 382 | }); 383 | }); 384 | 385 | describe('-> Head Interaction Model', function() { 386 | var subject; 387 | 388 | beforeEach(function() { 389 | if (TEST_VERSION === VERSION_0) subject = smapiClient.interactionModel.getEtag(testData.skillId, testData.locale); 390 | else if (TEST_VERSION === VERSION_1) subject = smapiClient.interactionModel.getEtag(testData.skillId, testData.stage, testData.locale); 391 | }); 392 | 393 | it('responds with etag', function() { 394 | subject = subject.then(function(response) { 395 | showResponse(response); 396 | return response; 397 | }, retry); 398 | return expect(subject).to.eventually.have.property('etag'); 399 | }); 400 | }); 401 | 402 | describe('-> Get Interaction Model', function() { 403 | var subject; 404 | 405 | beforeEach(function() { 406 | if (TEST_VERSION === VERSION_0) subject = smapiClient.interactionModel.get(testData.skillId, testData.locale); 407 | else if (TEST_VERSION === VERSION_1) subject = smapiClient.interactionModel.get(testData.skillId, testData.stage, testData.locale); 408 | }); 409 | 410 | it('responds with interaction model for ' + testData.locale, function() { 411 | subject = subject.then(function(response) { 412 | showResponse(response); 413 | return response; 414 | }, retry); 415 | return expect(subject).to.eventually.have.property('interactionModel'); 416 | }); 417 | }); 418 | }); 419 | 420 | context('-> Account Linking Operations', function() { 421 | describe('-> Update Account Linking', function() { 422 | var subject; 423 | 424 | beforeEach(function() { 425 | if (TEST_VERSION === VERSION_0) subject = smapiClient.accountLinking.update(testData.skillId, testData[TEST_VERSION].accountLinkingRequest); 426 | else if (TEST_VERSION === VERSION_1) subject = smapiClient.accountLinking.update(testData.skillId, testData.stage, testData[TEST_VERSION].accountLinkingRequest); 427 | }); 428 | 429 | it('responds with etag', function() { 430 | subject = subject.then(function(response) { 431 | showResponse(response); 432 | return response; 433 | }, retry); 434 | return expect(subject).to.eventually.have.property('etag'); 435 | }); 436 | }); 437 | 438 | describe('-> Read Account Linking Info', function() { 439 | var subject; 440 | 441 | beforeEach(function() { 442 | if (TEST_VERSION === VERSION_0) subject = smapiClient.accountLinking.readInfo(testData.skillId); 443 | else if (TEST_VERSION === VERSION_1) subject = smapiClient.accountLinking.readInfo(testData.skillId, testData.stage); 444 | }); 445 | 446 | it('responds with etag & accountLinkingResponse', function() { 447 | subject = subject.then(function(response) { 448 | showResponse(response); 449 | return response; 450 | }, retry); 451 | return expect(subject).to.eventually.have.property('accountLinkingResponse'); 452 | }); 453 | }); 454 | 455 | if (TEST_VERSION === VERSION_1) describe('-> Delete Account Linking', function() { 456 | var subject; 457 | 458 | beforeEach(function() { 459 | subject = smapiClient.accountLinking.delete(testData.skillId, testData.stage); 460 | }); 461 | 462 | it('responds with no content', function() { 463 | subject = subject.then(function(response) { 464 | showResponse(response); 465 | return response; 466 | }, retry); 467 | return expect(subject).to.eventually.be.have.property('status', 204); 468 | }); 469 | }); 470 | }); 471 | 472 | context('-> Skill Enablement Operations (except disable)', function() { 473 | describe('-> Enable a skill', function() { 474 | var subject; 475 | 476 | beforeEach(function() { 477 | subject = smapiClient.skillEnablement.enable(testData.skillId, testData.stage); 478 | }); 479 | 480 | it('responds with no content', function() { 481 | subject = subject.then(function(response) { 482 | showResponse(response); 483 | return response; 484 | }, retry); 485 | return expect(subject).to.eventually.have.property('status', 204); 486 | }); 487 | }); 488 | 489 | describe('-> Check enablement status of a skill', function() { 490 | var subject; 491 | 492 | beforeEach(function() { 493 | subject = smapiClient.skillEnablement.status(testData.skillId, testData.stage); 494 | }); 495 | 496 | it('responds with no content', function() { 497 | subject = subject.then(function(response) { 498 | showResponse(response); 499 | return response; 500 | }, retry); 501 | return expect(subject).to.eventually.have.property('status', 204); 502 | }); 503 | }); 504 | }); 505 | 506 | context('-> Skill Testing Operations', function() { 507 | if (TEST_VERSION === VERSION_1) describe('-> Validate a skill', function() { 508 | var subject; 509 | 510 | beforeEach(function() { 511 | subject = smapiClient.skillTesting.validate(testData.skillId, testData.stage, testData.locales); 512 | }); 513 | 514 | it('responds with validationId', function() { 515 | subject = subject.then(function(response) { 516 | showResponse(response); 517 | testData.validationId = response.id; 518 | return response; 519 | }, retry); 520 | return expect(subject).to.eventually.have.property('id'); 521 | }); 522 | }); 523 | 524 | if (TEST_VERSION === VERSION_1) describe('-> Check validation status of a skill', function() { 525 | var subject; 526 | 527 | beforeEach(function() { 528 | subject = smapiClient.skillTesting.validationStatus(testData.skillId, testData.stage, testData.validationId); 529 | }); 530 | 531 | it('responds with no content', function() { 532 | subject = subject.then(function(response) { 533 | showResponse(response); 534 | return response; 535 | }, retry); 536 | return expect(subject).to.eventually.have.property('status'); 537 | }); 538 | }); 539 | 540 | describe('-> Invoke a skill', function() { 541 | var subject; 542 | 543 | beforeEach(function() { 544 | testData.skillRequest.body.session.application.applicationId = 545 | testData.skillRequest.body.context.System.application.applicationId = testData.skillId; 546 | subject = smapiClient.skillTesting.invoke(testData.skillId, testData.endpointRegion, testData.skillRequest); 547 | }); 548 | 549 | it('responds with internal server error (no reply as skill code is not deployed anywhere)', function() { 550 | subject = subject.then(function(response) { 551 | showResponse(response); 552 | return response; 553 | }, retry); 554 | return expect(subject).to.eventually.have.property('status', 500); 555 | }); 556 | }); 557 | 558 | describe('-> Simulate a skill', function() { 559 | var subject; 560 | 561 | beforeEach(function() { 562 | subject = smapiClient.skillTesting.simulate(testData.skillId, testData.simulationContent, testData.locale); 563 | }); 564 | 565 | it('responds with simulationId', function() { 566 | subject = subject.then(function(response) { 567 | showResponse(response); 568 | testData.simulationId = response.id; 569 | return response; 570 | }, retry); 571 | return expect(subject).to.eventually.have.property('id'); 572 | }); 573 | }); 574 | 575 | describe('-> Check status of a skill simulation', function() { 576 | var subject; 577 | 578 | beforeEach(function() { 579 | subject = smapiClient.skillTesting.simulationStatus(testData.skillId, testData.simulationId); 580 | }); 581 | 582 | it('responds with simulation status', function() { 583 | subject = subject.then(function(response) { 584 | showResponse(response); 585 | return response; 586 | }, retry); 587 | return expect(subject).to.eventually.have.property('status'); 588 | }); 589 | }); 590 | }); 591 | 592 | context('-> Skill Enablement Operations (disable only)', function() { 593 | describe('-> Disable a skill', function() { 594 | var subject; 595 | 596 | beforeEach(function() { 597 | subject = smapiClient.skillEnablement.disable(testData.skillId, testData.stage); 598 | }); 599 | 600 | it('responds with no content', function() { 601 | subject = subject.then(function(response) { 602 | showResponse(response); 603 | return response; 604 | }, retry); 605 | return expect(subject).to.eventually.have.property('status', 204); 606 | }); 607 | }); 608 | }); 609 | 610 | if (shouldCertify(TEST_TYPE)) context('-> Skill Certification Operations', function() { 611 | describe('-> Submit a skill for certification', function() { 612 | var subject; 613 | 614 | beforeEach(function() { 615 | subject = smapiClient.skillCertification.submit(testData.skillId); 616 | }); 617 | 618 | it('responds with location', function() { 619 | subject = subject.then(function(response) { 620 | showResponse(response); 621 | return response; 622 | }, retry); 623 | return expect(subject).to.eventually.have.property('location'); 624 | }); 625 | }); 626 | 627 | if (TEST_VERSION === VERSION_1) describe('-> Check skill certification status', function() { 628 | // Location returned by v0 is not usable 629 | var subject; 630 | 631 | beforeEach(function() { 632 | subject = smapiClient.skillCertification.status(testData.vendorId, testData.skillId); 633 | }); 634 | 635 | it('responds with publicationStatus', function() { 636 | subject = subject.then(function(response) { 637 | showResponse(response); 638 | return response.skills[0]; 639 | }, retry); 640 | return Promise.all([ 641 | expect(subject).to.eventually.have.property('publicationStatus', 'CERTIFICATION'), 642 | expect(subject).to.eventually.have.property('skillId', testData.skillId), 643 | expect(subject).to.eventually.have.property('stage', testData.stage) 644 | ]); 645 | }); 646 | }); 647 | 648 | describe('-> Withdraw a skill from certification', function() { 649 | var subject; 650 | 651 | beforeEach(function() { 652 | subject = smapiClient.skillCertification.withdraw(testData.skillId, testData.reason, testData.message); 653 | }); 654 | 655 | it('responds with no content', function() { 656 | subject = subject.then(function(response) { 657 | showResponse(response); 658 | return response; 659 | }, waitOnCertification); 660 | return expect(subject).to.become({ 661 | status: 204, 662 | etag: undefined, 663 | location: undefined 664 | }); 665 | }); 666 | }); 667 | }); 668 | 669 | context('-> Skill Operations (delete only)', function() { 670 | describe('-> Delete a skill', function() { 671 | var subject; 672 | 673 | beforeEach(function() { 674 | subject = smapiClient.skills.delete(testData.skillId); 675 | }); 676 | 677 | it('responds with no content', function() { 678 | subject = subject.then(function(response) { 679 | showResponse(response); 680 | return response; 681 | }, retry); 682 | return expect(subject).to.eventually.be.have.property('status', 204); 683 | }); 684 | }); 685 | }); 686 | }); 687 | }); 688 | context('-> Other scenarios', function() { 689 | describe('-> Create SMAPI client with no parameters', function() { 690 | var smapiClient; 691 | beforeEach(function() { 692 | smapiClient = SMAPI_CLIENT(); 693 | if (shouldCapture(TEST_TYPE)) smapiClient.rest.client.interceptors.response.use(responses.add); 694 | }); 695 | 696 | it('responds with a Object with default version', function() { 697 | expect(smapiClient).to.have.property('version', 'v1'); 698 | }); 699 | 700 | describe('-> smapiClient.setBaseUrl() with bad baseURL', function() { 701 | it('throws Error', function() { 702 | expect(() => smapiClient.setBaseUrl('')).to.throw(Error); 703 | }); 704 | }); 705 | 706 | describe('-> smapiClient.setBaseUrl() with valid baseURL', function() { 707 | it('does not throw Error', function() { 708 | expect(() => smapiClient.setBaseUrl('notEmpty')).to.not.throw(Error); 709 | }); 710 | }); 711 | 712 | describe('-> smapiClient.refreshToken() with bad token', function() { 713 | it('throws Error', function() { 714 | expect(() => smapiClient.refreshToken('')).to.throw(Error); 715 | }); 716 | }); 717 | 718 | describe('-> smapiClient.tokens.refresh() with bad refreshToken', function() { 719 | const tokensRefreshWillThrow = () => smapiClient.tokens.refresh({ 720 | refreshToken: 'bad', 721 | clientId: 'bad', 722 | clientSecret: 'bad' 723 | }); 724 | var subject; 725 | 726 | beforeEach(function() { 727 | subject = tokensRefreshWillThrow(); 728 | }); 729 | 730 | it('eventually be rejected', function() { 731 | return expect(subject).to.eventually.be.rejected; 732 | }); 733 | }); 734 | 735 | context('-> Custom Operations with bad skillId, no access_token', function() { 736 | describe('-> head()', function() { 737 | var subject; 738 | 739 | beforeEach(function() { 740 | subject = smapiClient.custom.head(BAD_URL); 741 | }); 742 | 743 | it('responds with status 405', function() { 744 | subject = subject.then(function(response) { 745 | showResponse(response); 746 | return response; 747 | }, retry); 748 | return expect(subject).to.eventually.have.property('status', 405); 749 | }); 750 | }); 751 | 752 | describe('-> get()', function() { 753 | var subject; 754 | 755 | beforeEach(function() { 756 | subject = smapiClient.custom.get(BAD_URL); 757 | }); 758 | 759 | it('responds with status 405', function() { 760 | subject = subject.then(function(response) { 761 | showResponse(response); 762 | return response; 763 | }, retry); 764 | return expect(subject).to.eventually.have.property('status', 405); 765 | }); 766 | }); 767 | 768 | describe('-> post()', function() { 769 | var subject; 770 | 771 | beforeEach(function() { 772 | subject = smapiClient.custom.post(BAD_URL); 773 | }); 774 | 775 | it('responds with status 405', function() { 776 | subject = subject.then(function(response) { 777 | showResponse(response); 778 | return response; 779 | }, retry); 780 | return expect(subject).to.eventually.have.property('status', 405); 781 | }); 782 | }); 783 | 784 | describe('-> put()', function() { 785 | var subject; 786 | 787 | beforeEach(function() { 788 | subject = smapiClient.custom.put(BAD_URL); 789 | }); 790 | 791 | it('responds with status 405', function() { 792 | subject = subject.then(function(response) { 793 | showResponse(response); 794 | return response; 795 | }, retry); 796 | return expect(subject).to.eventually.have.property('status', 405); 797 | }); 798 | }); 799 | 800 | describe('-> delete()', function() { 801 | var subject; 802 | 803 | beforeEach(function() { 804 | subject = smapiClient.custom.delete(BAD_URL); 805 | }); 806 | 807 | it('responds with status 400', function() { 808 | subject = subject.then(function(response) { 809 | showResponse(response); 810 | return response; 811 | }, retry); 812 | return expect(subject).to.eventually.have.property('status', 400); 813 | }); 814 | }); 815 | }); 816 | }); 817 | 818 | describe('-> Create SMAPI client with bad version', function() { 819 | var smapiClient; 820 | beforeEach(function() { 821 | smapiClient = SMAPI_CLIENT({ 822 | version: 'bad', 823 | region: 'bad' 824 | }); 825 | }); 826 | 827 | it('responds with a Object with default version', function() { 828 | expect(smapiClient).to.have.property('version', 'v1'); 829 | expect(smapiClient).to.have.nested.property('version', 'v1'); 830 | }); 831 | }); 832 | }); 833 | }); 834 | -------------------------------------------------------------------------------- /test/utils/mockAdapter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var axios = require('axios'); 4 | var MockAdapter = require('axios-mock-adapter'); 5 | 6 | const responses = require('../data/responses.json'); 7 | 8 | // This sets the mock adapter on the default instance 9 | var mock = new MockAdapter(axios); 10 | 11 | // Token related mocked operations were added manually to avoid exposing sensitive data 12 | mock.onPost('/auth/o2/token', { 13 | grant_type: 'refresh_token', 14 | refresh_token: 'bad', 15 | client_id: 'bad', 16 | client_secret: 'bad' 17 | }).reply(401); 18 | mock.onPost('/auth/o2/token').reply(200, { access_token: 'dummy' }); 19 | 20 | for (var response of responses) { 21 | switch (response.method) { 22 | case 'get': 23 | mock.onGet(response.url).reply(response.status, response.data, response.headers); 24 | break; 25 | case 'put': 26 | mock.onPut(response.url).reply(response.status, response.data, response.headers); 27 | break; 28 | case 'post': 29 | mock.onPost(response.url).reply(response.status, response.data, response.headers); 30 | break; 31 | case 'head': 32 | mock.onHead(response.url).reply(response.status, response.data, response.headers); 33 | break; 34 | case 'delete': 35 | mock.onDelete(response.url).reply(response.status, response.data, response.headers); 36 | break; 37 | } 38 | } 39 | 40 | module.exports = mock; 41 | -------------------------------------------------------------------------------- /test/utils/responses.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict'; 3 | 4 | var escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 5 | 6 | var responses = { 7 | responses: [] 8 | }; 9 | 10 | responses.add = (response) => { 11 | // Capture response templates 12 | responses.responses.push({ 13 | url: response.config.url.replace(response.config.baseURL, ''), 14 | method: response.config.method, 15 | status: response.status, 16 | headers: { // Only keeping SMAPI headers 17 | location: response.headers.location, 18 | etag: response.headers.etag 19 | }, 20 | data: response.data, 21 | }); 22 | return response; 23 | }; 24 | 25 | responses.sanitize = (replacements) => { 26 | var content = JSON.stringify(responses.responses, null, ' '); 27 | for (var replacement in replacements) { 28 | content = content.replace(new RegExp(escapeRegExp(replacements[replacement]), 'g'), replacement); 29 | } 30 | responses.responses = JSON.parse(content.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/g, 'TIMESTAMP')); 31 | }; 32 | 33 | responses.persist = (file) => { 34 | // Persist response template 35 | const fs = require('fs'); 36 | fs.writeFile(file, JSON.stringify(responses.responses, null, ' '), {mode: 0o600}, function(err) { 37 | if (err) { 38 | return console.error(err); // eslint-disable-line no-console 39 | } 40 | console.log(`File ${file} was updated!`); // eslint-disable-line no-console 41 | return 0; 42 | }); 43 | }; 44 | 45 | module.exports = responses; 46 | --------------------------------------------------------------------------------