├── .eslintrc.json ├── .gitattributes ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── pull_request_template.md └── workflows │ ├── auto-approve.yml │ ├── build.yml │ ├── pull-request-lint.yml │ ├── release.yml │ └── upgrade-master.yml ├── .gitignore ├── .gitpod.yml ├── .mergify.yml ├── .npmignore ├── .projen ├── deps.json ├── files.json └── tasks.json ├── .projenrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── icons.js │ ├── icons.svg │ ├── main.js │ ├── navigation.js │ ├── search.js │ └── style.css ├── classes │ ├── AlexaTest.html │ ├── AplUserEventRequestBuilder.html │ ├── AudioPlayerPauseIntentRequestBuilder.html │ ├── AudioPlayerResumeIntentRequestBuilder.html │ ├── IntentRequestBuilder.html │ ├── LaunchRequestBuilder.html │ ├── RequestBuilder.html │ ├── ResponseValidator.html │ └── SessionEndedRequestBuilder.html ├── hierarchy.html ├── index.html ├── interfaces │ ├── SequenceItem.html │ └── SkillSettings.html └── modules.html ├── examples ├── skill-sample-nodejs-apl │ ├── apl.spec.ts │ └── apl.ts ├── skill-sample-nodejs-apla │ ├── apla.spec.ts │ └── apla.ts ├── skill-sample-nodejs-audioplayer │ ├── audioplayer.spec.ts │ └── audioplayer.ts ├── skill-sample-nodejs-dynamodb │ ├── helloworld.spec.ts │ └── helloworld.ts ├── skill-sample-nodejs-integration │ └── helloworld.it.ts ├── skill-sample-nodejs-multistring │ ├── helloworld.spec.ts │ └── helloworld.ts ├── skill-sample-nodejs-profileapi │ ├── helloworld.spec.ts │ └── helloworld.ts └── skill-sample-nodejs │ ├── helloworld.spec.ts │ └── helloworld.ts ├── package.json ├── src ├── factory │ ├── AplUserEventRequestBuilder.ts │ ├── AudioIntentRequestBuilder.ts │ ├── IntentRequestBuilder.ts │ ├── LaunchRequestBuilder.ts │ ├── RequestBuilder.ts │ └── SessionEndedRequestBuilder.ts ├── index.ts ├── tester │ ├── AlexaTest.ts │ ├── AplValidator.ts │ ├── AudioPlayerValidator.ts │ ├── CardValidator.ts │ ├── DialogValidator.ts │ ├── EndSessionValidator.ts │ ├── QuestionMarkValidator.ts │ ├── SessionAttributeValidator.ts │ ├── SpeechValidator.ts │ └── VideoAppValidator.ts └── types.ts ├── test └── interfaces.spec.ts ├── tsconfig.dev.json ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | { 3 | "env": { 4 | "jest": true, 5 | "node": true 6 | }, 7 | "root": true, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 2018, 15 | "sourceType": "module", 16 | "project": "./tsconfig.dev.json" 17 | }, 18 | "extends": [ 19 | "plugin:import/typescript" 20 | ], 21 | "settings": { 22 | "import/parsers": { 23 | "@typescript-eslint/parser": [ 24 | ".ts", 25 | ".tsx" 26 | ] 27 | }, 28 | "import/resolver": { 29 | "node": {}, 30 | "typescript": { 31 | "project": "./tsconfig.dev.json", 32 | "alwaysTryTypes": true 33 | } 34 | } 35 | }, 36 | "ignorePatterns": [ 37 | "*.js", 38 | "*.d.ts", 39 | "node_modules/", 40 | "*.generated.ts", 41 | "coverage", 42 | "!.projenrc.js" 43 | ], 44 | "rules": { 45 | "indent": [ 46 | "off" 47 | ], 48 | "@typescript-eslint/indent": [ 49 | "error", 50 | 2 51 | ], 52 | "quotes": [ 53 | "error", 54 | "single", 55 | { 56 | "avoidEscape": true 57 | } 58 | ], 59 | "comma-dangle": [ 60 | "error", 61 | "always-multiline" 62 | ], 63 | "comma-spacing": [ 64 | "error", 65 | { 66 | "before": false, 67 | "after": true 68 | } 69 | ], 70 | "no-multi-spaces": [ 71 | "error", 72 | { 73 | "ignoreEOLComments": false 74 | } 75 | ], 76 | "array-bracket-spacing": [ 77 | "error", 78 | "never" 79 | ], 80 | "array-bracket-newline": [ 81 | "error", 82 | "consistent" 83 | ], 84 | "object-curly-spacing": [ 85 | "error", 86 | "always" 87 | ], 88 | "object-curly-newline": [ 89 | "error", 90 | { 91 | "multiline": true, 92 | "consistent": true 93 | } 94 | ], 95 | "object-property-newline": [ 96 | "error", 97 | { 98 | "allowAllPropertiesOnSameLine": true 99 | } 100 | ], 101 | "keyword-spacing": [ 102 | "error" 103 | ], 104 | "brace-style": [ 105 | "error", 106 | "1tbs", 107 | { 108 | "allowSingleLine": true 109 | } 110 | ], 111 | "space-before-blocks": [ 112 | "error" 113 | ], 114 | "curly": [ 115 | "error", 116 | "multi-line", 117 | "consistent" 118 | ], 119 | "@typescript-eslint/member-delimiter-style": [ 120 | "error" 121 | ], 122 | "semi": [ 123 | "error", 124 | "always" 125 | ], 126 | "max-len": [ 127 | "error", 128 | { 129 | "code": 150, 130 | "ignoreUrls": true, 131 | "ignoreStrings": true, 132 | "ignoreTemplateLiterals": true, 133 | "ignoreComments": true, 134 | "ignoreRegExpLiterals": true 135 | } 136 | ], 137 | "quote-props": [ 138 | "error", 139 | "consistent-as-needed" 140 | ], 141 | "@typescript-eslint/no-require-imports": [ 142 | "error" 143 | ], 144 | "import/no-extraneous-dependencies": [ 145 | "error", 146 | { 147 | "devDependencies": [ 148 | "**/test/**", 149 | "**/build-tools/**" 150 | ], 151 | "optionalDependencies": false, 152 | "peerDependencies": true 153 | } 154 | ], 155 | "import/no-unresolved": [ 156 | "error" 157 | ], 158 | "import/order": [ 159 | "warn", 160 | { 161 | "groups": [ 162 | "builtin", 163 | "external" 164 | ], 165 | "alphabetize": { 166 | "order": "asc", 167 | "caseInsensitive": true 168 | } 169 | } 170 | ], 171 | "import/no-duplicates": [ 172 | "error" 173 | ], 174 | "no-shadow": [ 175 | "off" 176 | ], 177 | "@typescript-eslint/no-shadow": [ 178 | "error" 179 | ], 180 | "key-spacing": [ 181 | "error" 182 | ], 183 | "no-multiple-empty-lines": [ 184 | "error" 185 | ], 186 | "@typescript-eslint/no-floating-promises": [ 187 | "error" 188 | ], 189 | "no-return-await": [ 190 | "off" 191 | ], 192 | "@typescript-eslint/return-await": [ 193 | "error" 194 | ], 195 | "no-trailing-spaces": [ 196 | "error" 197 | ], 198 | "dot-notation": [ 199 | "error" 200 | ], 201 | "no-bitwise": [ 202 | "error" 203 | ], 204 | "@typescript-eslint/member-ordering": [ 205 | "error", 206 | { 207 | "default": [ 208 | "public-static-field", 209 | "public-static-method", 210 | "protected-static-field", 211 | "protected-static-method", 212 | "private-static-field", 213 | "private-static-method", 214 | "field", 215 | "constructor", 216 | "method" 217 | ] 218 | } 219 | ] 220 | }, 221 | "overrides": [ 222 | { 223 | "files": [ 224 | ".projenrc.js" 225 | ], 226 | "rules": { 227 | "@typescript-eslint/no-require-imports": "off", 228 | "import/no-extraneous-dependencies": "off" 229 | } 230 | } 231 | ] 232 | } 233 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | /.eslintrc.json linguist-generated 4 | /.gitattributes linguist-generated 5 | /.github/pull_request_template.md linguist-generated 6 | /.github/workflows/auto-approve.yml linguist-generated 7 | /.github/workflows/build.yml linguist-generated 8 | /.github/workflows/pull-request-lint.yml linguist-generated 9 | /.github/workflows/release.yml linguist-generated 10 | /.github/workflows/upgrade-master.yml linguist-generated 11 | /.gitignore linguist-generated 12 | /.gitpod.yml linguist-generated 13 | /.mergify.yml linguist-generated 14 | /.npmignore linguist-generated 15 | /.projen/** linguist-generated 16 | /.projen/deps.json linguist-generated 17 | /.projen/files.json linguist-generated 18 | /.projen/tasks.json linguist-generated 19 | /LICENSE linguist-generated 20 | /package.json linguist-generated 21 | /tsconfig.dev.json linguist-generated 22 | /tsconfig.json linguist-generated 23 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * **Please check if the PR fulfills these requirements** 2 | - [ ] The commit message describes your change 3 | - [ ] Tests for the changes have been added if possible (for bug fixes / features) 4 | - [ ] Docs have been added / updated (for bug fixes / features) 5 | - [ ] Changes are mentioned in the changelog (for bug fixes / features) 6 | 7 | 8 | * **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 9 | 10 | 11 | 12 | * **What is the current behavior?** (You can also link to an open issue here) 13 | 14 | 15 | 16 | * **What is the new behavior (if this is a feature change)?** 17 | 18 | 19 | 20 | * **Does this PR introduce a breaking change?** (What changes might users need to make in their setup due to this PR?) 21 | 22 | 23 | 24 | * **Other information**: -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | * **Please check if the PR fulfills these requirements** 2 | - [ ] The commit message describes your change 3 | - [ ] Tests for the changes have been added if possible (for bug fixes / features) 4 | - [ ] Docs have been added / updated (for bug fixes / features) 5 | 6 | 7 | * **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 8 | 9 | 10 | 11 | * **What is the current behavior?** (You can also link to an open issue here) 12 | 13 | 14 | 15 | * **What is the new behavior (if this is a feature change)?** 16 | 17 | 18 | 19 | * **Does this PR introduce a breaking change?** (What changes might users need to make in their setup due to this PR?) 20 | 21 | 22 | 23 | * **Other information**: -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: auto-approve 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | jobs: 13 | approve: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pull-requests: write 17 | if: contains(github.event.pull_request.labels.*.name, 'auto-approve') && (github.event.pull_request.user.login == 'hoegertn' || github.event.pull_request.user.login == 'taimos-projen[bot]') 18 | steps: 19 | - uses: hmarr/auto-approve-action@v2.2.1 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: build 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | outputs: 13 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} 14 | env: 15 | CI: "true" 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: ${{ github.event.pull_request.head.ref }} 21 | repository: ${{ github.event.pull_request.head.repo.full_name }} 22 | - name: Install dependencies 23 | run: yarn install --check-files 24 | - name: build 25 | run: npx projen build 26 | - name: Find mutations 27 | id: self_mutation 28 | run: |- 29 | git add . 30 | git diff --staged --patch --exit-code > .repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT 31 | working-directory: ./ 32 | - name: Upload patch 33 | if: steps.self_mutation.outputs.self_mutation_happened 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: .repo.patch 37 | path: .repo.patch 38 | overwrite: true 39 | - name: Fail build on mutation 40 | if: steps.self_mutation.outputs.self_mutation_happened 41 | run: |- 42 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 43 | cat .repo.patch 44 | exit 1 45 | self-mutation: 46 | needs: build 47 | runs-on: ubuntu-latest 48 | permissions: 49 | contents: write 50 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 51 | steps: 52 | - name: Generate token 53 | id: generate_token 54 | uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a 55 | with: 56 | app_id: ${{ secrets.PROJEN_APP_ID }} 57 | private_key: ${{ secrets.PROJEN_APP_PRIVATE_KEY }} 58 | - name: Checkout 59 | uses: actions/checkout@v4 60 | with: 61 | token: ${{ steps.generate_token.outputs.token }} 62 | ref: ${{ github.event.pull_request.head.ref }} 63 | repository: ${{ github.event.pull_request.head.repo.full_name }} 64 | - name: Download patch 65 | uses: actions/download-artifact@v4 66 | with: 67 | name: .repo.patch 68 | path: ${{ runner.temp }} 69 | - name: Apply patch 70 | run: '[ -s ${{ runner.temp }}/.repo.patch ] && git apply ${{ runner.temp }}/.repo.patch || echo "Empty patch. Skipping."' 71 | - name: Set git identity 72 | run: |- 73 | git config user.name "github-actions" 74 | git config user.email "github-actions@github.com" 75 | - name: Push changes 76 | env: 77 | PULL_REQUEST_REF: ${{ github.event.pull_request.head.ref }} 78 | run: |- 79 | git add . 80 | git commit -s -m "chore: self mutation" 81 | git push origin HEAD:$PULL_REQUEST_REF 82 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: pull-request-lint 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | - edited 13 | jobs: 14 | validate: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | permissions: 18 | pull-requests: write 19 | steps: 20 | - uses: amannn/action-semantic-pull-request@v5.4.0 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | types: |- 25 | feat 26 | fix 27 | chore 28 | requireScope: false 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: release 4 | on: 5 | push: 6 | branches: 7 | - master 8 | workflow_dispatch: {} 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | outputs: 15 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 16 | tag_exists: ${{ steps.check_tag_exists.outputs.exists }} 17 | env: 18 | CI: "true" 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | - name: Set git identity 25 | run: |- 26 | git config user.name "github-actions" 27 | git config user.email "github-actions@github.com" 28 | - name: Install dependencies 29 | run: yarn install --check-files --frozen-lockfile 30 | - name: release 31 | run: npx projen release 32 | - name: Check if version has already been tagged 33 | id: check_tag_exists 34 | run: |- 35 | TAG=$(cat dist/releasetag.txt) 36 | ([ ! -z "$TAG" ] && git ls-remote -q --exit-code --tags origin $TAG && (echo "exists=true" >> $GITHUB_OUTPUT)) || (echo "exists=false" >> $GITHUB_OUTPUT) 37 | cat $GITHUB_OUTPUT 38 | - name: Check for new commits 39 | id: git_remote 40 | run: |- 41 | echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT 42 | cat $GITHUB_OUTPUT 43 | - name: Backup artifact permissions 44 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 45 | run: cd dist && getfacl -R . > permissions-backup.acl 46 | continue-on-error: true 47 | - name: Upload artifact 48 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: build-artifact 52 | path: dist 53 | overwrite: true 54 | release_github: 55 | name: Publish to GitHub Releases 56 | needs: 57 | - release 58 | - release_npm 59 | runs-on: ubuntu-latest 60 | permissions: 61 | contents: write 62 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 63 | steps: 64 | - uses: actions/setup-node@v4 65 | with: 66 | node-version: 18.x 67 | - name: Download build artifacts 68 | uses: actions/download-artifact@v4 69 | with: 70 | name: build-artifact 71 | path: dist 72 | - name: Restore build artifact permissions 73 | run: cd dist && setfacl --restore=permissions-backup.acl 74 | continue-on-error: true 75 | - name: Release 76 | env: 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | GITHUB_REPOSITORY: ${{ github.repository }} 79 | GITHUB_REF: ${{ github.sha }} 80 | run: errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) --target $GITHUB_REF 2> $errout && true; exitcode=$?; if [ $exitcode -ne 0 ] && ! grep -q "Release.tag_name already exists" $errout; then cat $errout; exit $exitcode; fi 81 | release_npm: 82 | name: Publish to npm 83 | needs: release 84 | runs-on: ubuntu-latest 85 | permissions: 86 | id-token: write 87 | contents: read 88 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 89 | steps: 90 | - uses: actions/setup-node@v4 91 | with: 92 | node-version: 18.x 93 | - name: Download build artifacts 94 | uses: actions/download-artifact@v4 95 | with: 96 | name: build-artifact 97 | path: dist 98 | - name: Restore build artifact permissions 99 | run: cd dist && setfacl --restore=permissions-backup.acl 100 | continue-on-error: true 101 | - name: Release 102 | env: 103 | NPM_DIST_TAG: latest 104 | NPM_REGISTRY: registry.npmjs.org 105 | NPM_CONFIG_PROVENANCE: "true" 106 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 107 | run: npx -p publib@latest publib-npm 108 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-master.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: upgrade-master 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 0 * * 1 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: master 21 | - name: Install dependencies 22 | run: yarn install --check-files --frozen-lockfile 23 | - name: Upgrade dependencies 24 | run: npx projen upgrade 25 | - name: Find mutations 26 | id: create_patch 27 | run: |- 28 | git add . 29 | git diff --staged --patch --exit-code > .repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 30 | working-directory: ./ 31 | - name: Upload patch 32 | if: steps.create_patch.outputs.patch_created 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: .repo.patch 36 | path: .repo.patch 37 | overwrite: true 38 | pr: 39 | name: Create Pull Request 40 | needs: upgrade 41 | runs-on: ubuntu-latest 42 | permissions: 43 | contents: read 44 | if: ${{ needs.upgrade.outputs.patch_created }} 45 | steps: 46 | - name: Generate token 47 | id: generate_token 48 | uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a 49 | with: 50 | app_id: ${{ secrets.PROJEN_APP_ID }} 51 | private_key: ${{ secrets.PROJEN_APP_PRIVATE_KEY }} 52 | - name: Checkout 53 | uses: actions/checkout@v4 54 | with: 55 | ref: master 56 | - name: Download patch 57 | uses: actions/download-artifact@v4 58 | with: 59 | name: .repo.patch 60 | path: ${{ runner.temp }} 61 | - name: Apply patch 62 | run: '[ -s ${{ runner.temp }}/.repo.patch ] && git apply ${{ runner.temp }}/.repo.patch || echo "Empty patch. Skipping."' 63 | - name: Set git identity 64 | run: |- 65 | git config user.name "github-actions" 66 | git config user.email "github-actions@github.com" 67 | - name: Create Pull Request 68 | id: create-pr 69 | uses: peter-evans/create-pull-request@v6 70 | with: 71 | token: ${{ steps.generate_token.outputs.token }} 72 | commit-message: |- 73 | chore(deps): upgrade dependencies 74 | 75 | Upgrades project dependencies. See details in [workflow run]. 76 | 77 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 78 | 79 | ------ 80 | 81 | *Automatically created by projen via the "upgrade-master" workflow* 82 | branch: github-actions/upgrade-master 83 | title: "chore(deps): upgrade dependencies" 84 | labels: auto-approve 85 | body: |- 86 | Upgrades project dependencies. See details in [workflow run]. 87 | 88 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 89 | 90 | ------ 91 | 92 | *Automatically created by projen via the "upgrade-master" workflow* 93 | author: github-actions 94 | committer: github-actions 95 | signoff: true 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/.github/workflows/pull-request-lint.yml 7 | !/.gitpod.yml 8 | !/.github/workflows/auto-approve.yml 9 | !/package.json 10 | !/LICENSE 11 | !/.npmignore 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | lerna-debug.log* 18 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | lib-cov 24 | coverage 25 | *.lcov 26 | .nyc_output 27 | build/Release 28 | node_modules/ 29 | jspm_packages/ 30 | *.tsbuildinfo 31 | .eslintcache 32 | *.tgz 33 | .yarn-integrity 34 | .cache 35 | !/.github/workflows/build.yml 36 | /dist/changelog.md 37 | /dist/version.txt 38 | !/.github/workflows/release.yml 39 | !/.mergify.yml 40 | !/.github/workflows/upgrade-master.yml 41 | !/.github/pull_request_template.md 42 | !/test/ 43 | !/tsconfig.json 44 | !/tsconfig.dev.json 45 | !/src/ 46 | /lib 47 | /dist/ 48 | !/.eslintrc.json 49 | !/.projenrc.js 50 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | image: taimos/gitpod 4 | tasks: 5 | - command: npx projen build 6 | init: yarn install --check-files --frozen-lockfile 7 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | queue_rules: 4 | - name: default 5 | update_method: merge 6 | conditions: 7 | - "#approved-reviews-by>=1" 8 | - -label~=(do-not-merge) 9 | - status-success=build 10 | pull_request_rules: 11 | - name: Automatic merge on approval and successful build 12 | actions: 13 | delete_head_branch: {} 14 | queue: 15 | method: squash 16 | name: default 17 | commit_message_template: |- 18 | {{ title }} (#{{ number }}) 19 | 20 | {{ body }} 21 | conditions: 22 | - "#approved-reviews-by>=1" 23 | - -label~=(do-not-merge) 24 | - status-success=build 25 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | /.projen/ 3 | permissions-backup.acl 4 | /dist/changelog.md 5 | /dist/version.txt 6 | /.mergify.yml 7 | /test/ 8 | /tsconfig.dev.json 9 | /src/ 10 | !/lib/ 11 | !/lib/**/*.js 12 | !/lib/**/*.d.ts 13 | dist 14 | /tsconfig.json 15 | /.github/ 16 | /.vscode/ 17 | /.idea/ 18 | /.projenrc.js 19 | tsconfig.tsbuildinfo 20 | /.eslintrc.json 21 | /.gitattributes 22 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@taimos/projen", 5 | "type": "build" 6 | }, 7 | { 8 | "name": "@types/chai", 9 | "type": "build" 10 | }, 11 | { 12 | "name": "@types/mocha", 13 | "type": "build" 14 | }, 15 | { 16 | "name": "@types/node", 17 | "version": "^18", 18 | "type": "build" 19 | }, 20 | { 21 | "name": "@types/sinon", 22 | "type": "build" 23 | }, 24 | { 25 | "name": "@types/uuid", 26 | "type": "build" 27 | }, 28 | { 29 | "name": "@typescript-eslint/eslint-plugin", 30 | "version": "^7", 31 | "type": "build" 32 | }, 33 | { 34 | "name": "@typescript-eslint/parser", 35 | "version": "^7", 36 | "type": "build" 37 | }, 38 | { 39 | "name": "chai", 40 | "type": "build" 41 | }, 42 | { 43 | "name": "constructs", 44 | "version": "^10.0.0", 45 | "type": "build" 46 | }, 47 | { 48 | "name": "eslint-import-resolver-typescript", 49 | "type": "build" 50 | }, 51 | { 52 | "name": "eslint-plugin-import", 53 | "type": "build" 54 | }, 55 | { 56 | "name": "eslint", 57 | "version": "^8", 58 | "type": "build" 59 | }, 60 | { 61 | "name": "mocha", 62 | "type": "build" 63 | }, 64 | { 65 | "name": "nyc", 66 | "type": "build" 67 | }, 68 | { 69 | "name": "projen", 70 | "type": "build" 71 | }, 72 | { 73 | "name": "rimraf", 74 | "type": "build" 75 | }, 76 | { 77 | "name": "sinon", 78 | "type": "build" 79 | }, 80 | { 81 | "name": "standard-version", 82 | "version": "^9", 83 | "type": "build" 84 | }, 85 | { 86 | "name": "ts-node", 87 | "type": "build" 88 | }, 89 | { 90 | "name": "typedoc", 91 | "type": "build" 92 | }, 93 | { 94 | "name": "typescript", 95 | "type": "build" 96 | }, 97 | { 98 | "name": "ask-sdk-core", 99 | "version": "^2.10.1", 100 | "type": "peer" 101 | }, 102 | { 103 | "name": "ask-sdk-model", 104 | "version": "^1.34.1", 105 | "type": "peer" 106 | }, 107 | { 108 | "name": "ask-sdk", 109 | "version": "^2.10.0", 110 | "type": "peer" 111 | }, 112 | { 113 | "name": "chai", 114 | "type": "peer" 115 | }, 116 | { 117 | "name": "mocha", 118 | "type": "peer" 119 | }, 120 | { 121 | "name": "aws-sdk", 122 | "type": "runtime" 123 | }, 124 | { 125 | "name": "aws-sdk-mock", 126 | "type": "runtime" 127 | }, 128 | { 129 | "name": "lambda-local", 130 | "type": "runtime" 131 | }, 132 | { 133 | "name": "nock", 134 | "type": "runtime" 135 | }, 136 | { 137 | "name": "uuid", 138 | "type": "runtime" 139 | } 140 | ], 141 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 142 | } 143 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/auto-approve.yml", 7 | ".github/workflows/build.yml", 8 | ".github/workflows/pull-request-lint.yml", 9 | ".github/workflows/release.yml", 10 | ".github/workflows/upgrade-master.yml", 11 | ".gitignore", 12 | ".gitpod.yml", 13 | ".mergify.yml", 14 | ".npmignore", 15 | ".projen/deps.json", 16 | ".projen/files.json", 17 | ".projen/tasks.json", 18 | "LICENSE", 19 | "tsconfig.dev.json", 20 | "tsconfig.json" 21 | ], 22 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 23 | } 24 | -------------------------------------------------------------------------------- /.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "name": "build", 5 | "description": "Full release build", 6 | "steps": [ 7 | { 8 | "spawn": "default" 9 | }, 10 | { 11 | "spawn": "pre-compile" 12 | }, 13 | { 14 | "spawn": "compile" 15 | }, 16 | { 17 | "spawn": "post-compile" 18 | }, 19 | { 20 | "spawn": "test" 21 | }, 22 | { 23 | "spawn": "package" 24 | } 25 | ] 26 | }, 27 | "bump": { 28 | "name": "bump", 29 | "description": "Bumps version based on latest git tag and generates a changelog entry", 30 | "env": { 31 | "OUTFILE": "package.json", 32 | "CHANGELOG": "dist/changelog.md", 33 | "BUMPFILE": "dist/version.txt", 34 | "RELEASETAG": "dist/releasetag.txt", 35 | "RELEASE_TAG_PREFIX": "" 36 | }, 37 | "steps": [ 38 | { 39 | "builtin": "release/bump-version" 40 | } 41 | ], 42 | "condition": "git log --oneline -1 | grep -qv \"chore(release):\"" 43 | }, 44 | "clobber": { 45 | "name": "clobber", 46 | "description": "hard resets to HEAD of origin and cleans the local repo", 47 | "env": { 48 | "BRANCH": "$(git branch --show-current)" 49 | }, 50 | "steps": [ 51 | { 52 | "exec": "git checkout -b scratch", 53 | "name": "save current HEAD in \"scratch\" branch" 54 | }, 55 | { 56 | "exec": "git checkout $BRANCH" 57 | }, 58 | { 59 | "exec": "git fetch origin", 60 | "name": "fetch latest changes from origin" 61 | }, 62 | { 63 | "exec": "git reset --hard origin/$BRANCH", 64 | "name": "hard reset to origin commit" 65 | }, 66 | { 67 | "exec": "git clean -fdx", 68 | "name": "clean all untracked files" 69 | }, 70 | { 71 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 72 | } 73 | ], 74 | "condition": "git diff --exit-code > /dev/null" 75 | }, 76 | "compile": { 77 | "name": "compile", 78 | "description": "Only compile", 79 | "steps": [ 80 | { 81 | "exec": "tsc --build" 82 | } 83 | ] 84 | }, 85 | "default": { 86 | "name": "default", 87 | "description": "Synthesize project files", 88 | "steps": [ 89 | { 90 | "exec": "node .projenrc.js" 91 | } 92 | ] 93 | }, 94 | "docgen": { 95 | "name": "docgen", 96 | "description": "Generate TypeScript API reference docs/", 97 | "steps": [ 98 | { 99 | "exec": "typedoc src --disableSources --out docs/" 100 | } 101 | ] 102 | }, 103 | "eject": { 104 | "name": "eject", 105 | "description": "Remove projen from the project", 106 | "env": { 107 | "PROJEN_EJECTING": "true" 108 | }, 109 | "steps": [ 110 | { 111 | "spawn": "default" 112 | } 113 | ] 114 | }, 115 | "eslint": { 116 | "name": "eslint", 117 | "description": "Runs eslint against the codebase", 118 | "steps": [ 119 | { 120 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern $@ src test build-tools .projenrc.js", 121 | "receiveArgs": true 122 | } 123 | ] 124 | }, 125 | "install": { 126 | "name": "install", 127 | "description": "Install project dependencies and update lockfile (non-frozen)", 128 | "steps": [ 129 | { 130 | "exec": "yarn install --check-files" 131 | } 132 | ] 133 | }, 134 | "install:ci": { 135 | "name": "install:ci", 136 | "description": "Install project dependencies using frozen lockfile", 137 | "steps": [ 138 | { 139 | "exec": "yarn install --check-files --frozen-lockfile" 140 | } 141 | ] 142 | }, 143 | "package": { 144 | "name": "package", 145 | "description": "Creates the distribution package", 146 | "steps": [ 147 | { 148 | "exec": "mkdir -p dist/js" 149 | }, 150 | { 151 | "exec": "npm pack --pack-destination dist/js" 152 | } 153 | ] 154 | }, 155 | "post-compile": { 156 | "name": "post-compile", 157 | "description": "Runs after successful compilation", 158 | "steps": [ 159 | { 160 | "spawn": "docgen" 161 | } 162 | ] 163 | }, 164 | "post-upgrade": { 165 | "name": "post-upgrade", 166 | "description": "Runs after upgrading dependencies" 167 | }, 168 | "pre-compile": { 169 | "name": "pre-compile", 170 | "description": "Prepare the project for compilation" 171 | }, 172 | "release": { 173 | "name": "release", 174 | "description": "Prepare a release from \"master\" branch", 175 | "env": { 176 | "RELEASE": "true" 177 | }, 178 | "steps": [ 179 | { 180 | "exec": "rm -fr dist" 181 | }, 182 | { 183 | "spawn": "bump" 184 | }, 185 | { 186 | "spawn": "build" 187 | }, 188 | { 189 | "spawn": "unbump" 190 | }, 191 | { 192 | "exec": "git diff --ignore-space-at-eol --exit-code" 193 | } 194 | ] 195 | }, 196 | "test": { 197 | "name": "test", 198 | "description": "Run tests", 199 | "steps": [ 200 | { 201 | "spawn": "eslint" 202 | }, 203 | { 204 | "exec": "nyc -x tst -e .ts --temp-directory 'coverage/nyc-output' -r html -r text-summary -r cobertura _mocha --require ts-node/register 'test/**/*.spec.ts' 'examples/**/*.spec.ts' --reporter nyan" 205 | } 206 | ] 207 | }, 208 | "unbump": { 209 | "name": "unbump", 210 | "description": "Restores version to 0.0.0", 211 | "env": { 212 | "OUTFILE": "package.json", 213 | "CHANGELOG": "dist/changelog.md", 214 | "BUMPFILE": "dist/version.txt", 215 | "RELEASETAG": "dist/releasetag.txt", 216 | "RELEASE_TAG_PREFIX": "" 217 | }, 218 | "steps": [ 219 | { 220 | "builtin": "release/reset-version" 221 | } 222 | ] 223 | }, 224 | "upgrade": { 225 | "name": "upgrade", 226 | "description": "upgrade dependencies", 227 | "env": { 228 | "CI": "0" 229 | }, 230 | "steps": [ 231 | { 232 | "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --dep=dev,peer,prod,optional --filter=@taimos/projen,@types/chai,@types/mocha,@types/sinon,@types/uuid,chai,eslint-import-resolver-typescript,eslint-plugin-import,mocha,nyc,projen,rimraf,sinon,ts-node,typedoc,typescript,aws-sdk,aws-sdk-mock,lambda-local,nock,uuid" 233 | }, 234 | { 235 | "exec": "yarn install --check-files" 236 | }, 237 | { 238 | "exec": "yarn upgrade @taimos/projen @types/chai @types/mocha @types/node @types/sinon @types/uuid @typescript-eslint/eslint-plugin @typescript-eslint/parser chai constructs eslint-import-resolver-typescript eslint-plugin-import eslint mocha nyc projen rimraf sinon standard-version ts-node typedoc typescript ask-sdk-core ask-sdk-model ask-sdk aws-sdk aws-sdk-mock lambda-local nock uuid" 239 | }, 240 | { 241 | "exec": "npx projen" 242 | }, 243 | { 244 | "spawn": "post-upgrade" 245 | } 246 | ] 247 | }, 248 | "watch": { 249 | "name": "watch", 250 | "description": "Watch & compile in the background", 251 | "steps": [ 252 | { 253 | "exec": "tsc --build -w" 254 | } 255 | ] 256 | } 257 | }, 258 | "env": { 259 | "PATH": "$(npx -c \"node --print process.env.PATH\")" 260 | }, 261 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 262 | } 263 | -------------------------------------------------------------------------------- /.projenrc.js: -------------------------------------------------------------------------------- 1 | const { TaimosTypescriptLibrary } = require('@taimos/projen'); 2 | 3 | const project = new TaimosTypescriptLibrary({ 4 | name: 'ask-sdk-test', 5 | license: 'MIT', 6 | deps: [ 7 | 'aws-sdk', 8 | 'aws-sdk-mock', 9 | 'lambda-local', 10 | 'uuid', 11 | 'nock', 12 | ], 13 | defaultReleaseBranch: 'master', 14 | devDeps: [ 15 | '@taimos/projen', 16 | '@types/chai', 17 | '@types/uuid', 18 | '@types/mocha', 19 | '@types/node', 20 | '@types/sinon', 21 | 'chai', 22 | 'rimraf', 23 | 'mocha', 24 | 'nyc', 25 | 'sinon', 26 | ], 27 | peerDeps: [ 28 | 'mocha', 29 | 'ask-sdk@^2.10.0', 30 | 'ask-sdk-core@^2.10.1', 31 | 'ask-sdk-model@^1.34.1', 32 | 'chai', 33 | ], 34 | keywords: [ 35 | 'aws', 36 | 'alexa', 37 | 'amazon', 38 | 'echo', 39 | 'test', 40 | 'offline', 41 | 'mocha', 42 | 'black box', 43 | 'black-box', 44 | 'coverage', 45 | ], 46 | jest: false, 47 | repository: 'https://github.com/taimos/ask-sdk-test', 48 | }); 49 | 50 | project.addFields({ 51 | resolutions: { 52 | 'ask-sdk-model': '1.34.1', 53 | }, 54 | }); 55 | 56 | project.testTask.exec("nyc -x tst -e .ts --temp-directory 'coverage/nyc-output' -r html -r text-summary -r cobertura _mocha --require ts-node/register 'test/**/*.spec.ts' 'examples/**/*.spec.ts' --reporter nyan"); 57 | 58 | project.synth(); -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## current master 3 | 4 | ## 2.6.0 5 | * Improve saysLike and repromptsLike error messages. 6 | * Introduce support for APL-A 7 | * Support withShouldEndSession having an undefined value 8 | 9 | ## 2.5.0 10 | * feat: add `renderDocument` parameter used to assert templates in `AplValidator` class. 11 | * feat: add `AplUserEventRequestBuilder` to provide support for touch events. 12 | 13 | ## 2.4.0 14 | * update libraries 15 | * null check for `AudioPlayerValidatior` 16 | * feat: add `withSlotConfirmation` to support slot confirmation 17 | * feat: add `withIntentConfirmation` to support intent confirmation 18 | 19 | ## 2.3.0 20 | * feat: add debug flag to TestSettings (add #9) 21 | * feat: support integration testing with real Lambda functions (add #8) 22 | 23 | ## 2.2.0 24 | * feat: add `elicitsForIntent` to support intent chaining 25 | * feat: add `withUserAccessToken` to provide account linking (fix #6) 26 | * update dependencies 27 | 28 | ## 2.1.0 29 | * fix: throw correct error when slot elicitation fails 30 | * update dependencies 31 | 32 | ## 2.0.5 33 | * fix `nock` dependency 34 | 35 | ## 2.0.4 36 | * fix default values for persistence keys to match ASK defaults 37 | 38 | ## 2.0.3 39 | * add `withProfile` option to test ProfileAPI 40 | * allow overriding of session attributes in `withSessionAttributes` 41 | 42 | ## 2.0.2 43 | * add supportedInterfaces to request builder 44 | * add missing feature `withSessionAttributes` (#3) 45 | * add validator for video playback 46 | 47 | ## 2.0.1 48 | * adding more supported types to attributes check 49 | * fix typo in builder method 50 | 51 | ## 2.0.0 52 | First release of rewritten version 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Taimos GmbH 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alexa Skill Test Framework 2 | 3 | [![npm version](https://badge.fury.io/js/ask-sdk-test.svg)](https://badge.fury.io/js/ask-sdk-test) 4 | 5 | This framework makes it easy to create full-coverage black box tests for an Alexa skill using [Mocha](https://mochajs.org/). 6 | 7 | Here's an example of what a test might look like with the test framework. 8 | 9 | ```typescript 10 | import {AlexaTest, IntentRequestBuilder, LaunchRequestBuilder, SkillSettings} from 'ask-sdk-test'; 11 | import {handler as skillHandler} from './helloworld'; 12 | 13 | // initialize the testing framework 14 | const skillSettings : SkillSettings = { 15 | appId: 'amzn1.ask.skill.00000000-0000-0000-0000-000000000000', 16 | userId: 'amzn1.ask.account.VOID', 17 | deviceId: 'amzn1.ask.device.VOID', 18 | locale: 'en-US', 19 | }; 20 | 21 | const alexaTest = new AlexaTest(skillHandler, skillSettings); 22 | 23 | describe('LaunchRequest', () => { 24 | alexaTest.test([ 25 | { 26 | request: new LaunchRequestBuilder(skillSettings).build(), 27 | says: 'Welcome to the Alexa Skills Kit, you can say hello!', 28 | repromptsNothing: true, 29 | shouldEndSession: true, 30 | }, 31 | ]); 32 | }); 33 | ``` 34 | 35 | If you are writing your Alexa Skills in Python, check out https://github.com/BananaNosh/py_ask_sdk_test 36 | 37 | ## How To 38 | Install the package as a dev dependency with `npm install ask-sdk-test --save-dev`. 39 | 40 | Write tests in a Typescript file and run them with Mocha. For example, if your test is at 'test/skill.spec.ts', run `mocha --require node_modules/ts-node/register/index.js test/skill.spec.ts`. 41 | 42 | For some simple examples, see the 'examples' directory. 43 | 44 | ## History 45 | 46 | This framework is based on the [alexa-skill-test-framework](https://github.com/BrianMacIntosh/alexa-skill-test-framework) by Brian MacIntosh and rewritten for Typescript and the ASK SDK v2. 47 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #AF00DB; 3 | --dark-hl-0: #C586C0; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #001080; 7 | --dark-hl-2: #9CDCFE; 8 | --light-hl-3: #A31515; 9 | --dark-hl-3: #CE9178; 10 | --light-hl-4: #008000; 11 | --dark-hl-4: #6A9955; 12 | --light-hl-5: #0000FF; 13 | --dark-hl-5: #569CD6; 14 | --light-hl-6: #0070C1; 15 | --dark-hl-6: #4FC1FF; 16 | --light-hl-7: #267F99; 17 | --dark-hl-7: #4EC9B0; 18 | --light-hl-8: #795E26; 19 | --dark-hl-8: #DCDCAA; 20 | --light-code-background: #FFFFFF; 21 | --dark-code-background: #1E1E1E; 22 | } 23 | 24 | @media (prefers-color-scheme: light) { :root { 25 | --hl-0: var(--light-hl-0); 26 | --hl-1: var(--light-hl-1); 27 | --hl-2: var(--light-hl-2); 28 | --hl-3: var(--light-hl-3); 29 | --hl-4: var(--light-hl-4); 30 | --hl-5: var(--light-hl-5); 31 | --hl-6: var(--light-hl-6); 32 | --hl-7: var(--light-hl-7); 33 | --hl-8: var(--light-hl-8); 34 | --code-background: var(--light-code-background); 35 | } } 36 | 37 | @media (prefers-color-scheme: dark) { :root { 38 | --hl-0: var(--dark-hl-0); 39 | --hl-1: var(--dark-hl-1); 40 | --hl-2: var(--dark-hl-2); 41 | --hl-3: var(--dark-hl-3); 42 | --hl-4: var(--dark-hl-4); 43 | --hl-5: var(--dark-hl-5); 44 | --hl-6: var(--dark-hl-6); 45 | --hl-7: var(--dark-hl-7); 46 | --hl-8: var(--dark-hl-8); 47 | --code-background: var(--dark-code-background); 48 | } } 49 | 50 | :root[data-theme='light'] { 51 | --hl-0: var(--light-hl-0); 52 | --hl-1: var(--light-hl-1); 53 | --hl-2: var(--light-hl-2); 54 | --hl-3: var(--light-hl-3); 55 | --hl-4: var(--light-hl-4); 56 | --hl-5: var(--light-hl-5); 57 | --hl-6: var(--light-hl-6); 58 | --hl-7: var(--light-hl-7); 59 | --hl-8: var(--light-hl-8); 60 | --code-background: var(--light-code-background); 61 | } 62 | 63 | :root[data-theme='dark'] { 64 | --hl-0: var(--dark-hl-0); 65 | --hl-1: var(--dark-hl-1); 66 | --hl-2: var(--dark-hl-2); 67 | --hl-3: var(--dark-hl-3); 68 | --hl-4: var(--dark-hl-4); 69 | --hl-5: var(--dark-hl-5); 70 | --hl-6: var(--dark-hl-6); 71 | --hl-7: var(--dark-hl-7); 72 | --hl-8: var(--dark-hl-8); 73 | --code-background: var(--dark-code-background); 74 | } 75 | 76 | .hl-0 { color: var(--hl-0); } 77 | .hl-1 { color: var(--hl-1); } 78 | .hl-2 { color: var(--hl-2); } 79 | .hl-3 { color: var(--hl-3); } 80 | .hl-4 { color: var(--hl-4); } 81 | .hl-5 { color: var(--hl-5); } 82 | .hl-6 { color: var(--hl-6); } 83 | .hl-7 { color: var(--hl-7); } 84 | .hl-8 { color: var(--hl-8); } 85 | pre, code { background: var(--code-background); } 86 | -------------------------------------------------------------------------------- /docs/assets/icons.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | addIcons(); 3 | function addIcons() { 4 | if (document.readyState === "loading") return document.addEventListener("DOMContentLoaded", addIcons); 5 | const svg = document.body.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg")); 6 | svg.innerHTML = `""`; 7 | svg.style.display = "none"; 8 | if (location.protocol === "file:") updateUseElements(); 9 | } 10 | 11 | function updateUseElements() { 12 | document.querySelectorAll("use").forEach(el => { 13 | if (el.getAttribute("href").includes("#icon-")) { 14 | el.setAttribute("href", el.getAttribute("href").replace(/.*#/, "#")); 15 | } 16 | }); 17 | } 18 | })() -------------------------------------------------------------------------------- /docs/assets/icons.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/assets/navigation.js: -------------------------------------------------------------------------------- 1 | window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAA5XOQUvDQBCG4f8y56C0oEhuFXooeCitehEPS/bTLJ1M0sysVMT/LngwVZNNct5vnnefPshwMsppxTi5e6hRRo2zknIq2KlCL3+eLkqrmDI6BPGUL5Y3n1l33/CDol2/QWyHY4TabQzs0faAg9tkIfpQb9m9o926qNiITWlNuJpY3UFjNT87fJbqTsvMVe9clKIcU/tWKXXMmyNpU4vi0XHwzupe7M8k5e2hGmpZi4cf++XwNl04RkiBjaHqzCCG9sUV32w3+A0tr67PoUNg3sMsyKv2S+eL/9TzF1w2qsbNAwAA" -------------------------------------------------------------------------------- /docs/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = "data:application/octet-stream;base64,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"; -------------------------------------------------------------------------------- /docs/classes/ResponseValidator.html: -------------------------------------------------------------------------------- 1 | ResponseValidator | ask-sdk-test

Class ResponseValidatorAbstract

Constructors

Methods

Constructors

Methods

  • Parameters

    Returns void

4 | -------------------------------------------------------------------------------- /docs/hierarchy.html: -------------------------------------------------------------------------------- 1 | ask-sdk-test
2 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | ask-sdk-test

ask-sdk-test

Alexa Skill Test Framework

npm version

2 |

This framework makes it easy to create full-coverage black box tests for an Alexa skill using Mocha.

3 |

Here's an example of what a test might look like with the test framework.

4 |
import {AlexaTest, IntentRequestBuilder, LaunchRequestBuilder, SkillSettings} from 'ask-sdk-test';
import {handler as skillHandler} from './helloworld';

// initialize the testing framework
const skillSettings : SkillSettings = {
appId: 'amzn1.ask.skill.00000000-0000-0000-0000-000000000000',
userId: 'amzn1.ask.account.VOID',
deviceId: 'amzn1.ask.device.VOID',
locale: 'en-US',
};

const alexaTest = new AlexaTest(skillHandler, skillSettings);

describe('LaunchRequest', () => {
alexaTest.test([
{
request: new LaunchRequestBuilder(skillSettings).build(),
says: 'Welcome to the Alexa Skills Kit, you can say hello!',
repromptsNothing: true,
shouldEndSession: true,
},
]);
}); 5 |
6 | 7 |

If you are writing your Alexa Skills in Python, check out https://github.com/BananaNosh/py_ask_sdk_test

8 |

Install the package as a dev dependency with npm install ask-sdk-test --save-dev.

9 |

Write tests in a Typescript file and run them with Mocha. For example, if your test is at 'test/skill.spec.ts', run mocha --require node_modules/ts-node/register/index.js test/skill.spec.ts.

10 |

For some simple examples, see the 'examples' directory.

11 |

This framework is based on the alexa-skill-test-framework by Brian MacIntosh and rewritten for Typescript and the ASK SDK v2.

12 |
13 | -------------------------------------------------------------------------------- /docs/interfaces/SkillSettings.html: -------------------------------------------------------------------------------- 1 | SkillSettings | ask-sdk-test

Interface SkillSettings

interface SkillSettings {
    appId: string;
    debug?: boolean;
    deviceId: string;
    interfaces?: InterfaceSettings;
    locale: string;
    userId: string;
}

Properties

appId 2 | debug? 3 | deviceId 4 | interfaces? 5 | locale 6 | userId 7 |

Properties

appId: string

The skill id

8 |
debug?: boolean

true to print the response to the console

9 |
deviceId: string

The device id to simulate

10 |
interfaces?: InterfaceSettings

the interfaces present for the test

11 |
locale: string

the locale to use when generating requests

12 |
userId: string

The user id to simulate

13 |
14 | -------------------------------------------------------------------------------- /docs/modules.html: -------------------------------------------------------------------------------- 1 | ask-sdk-test
13 | -------------------------------------------------------------------------------- /examples/skill-sample-nodejs-apl/apl.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { AlexaTest, AplUserEventRequestBuilder, LaunchRequestBuilder, SkillSettings } from '../../src'; 6 | import { handler as skillHandler } from './apl'; 7 | 8 | // initialize the testing framework 9 | const skillSettings : SkillSettings = { 10 | appId: 'amzn1.ask.skill.00000000-0000-0000-0000-000000000000', 11 | userId: 'amzn1.ask.account.VOID', 12 | deviceId: 'amzn1.ask.device.VOID', 13 | locale: 'en-US', 14 | }; 15 | 16 | const alexaTest = new AlexaTest(skillHandler, skillSettings); 17 | 18 | describe('APL Presentation', () => { 19 | 20 | // TODO: Pass device Alexa.Presentation.APL 21 | // TODO: Pass touch event 22 | // TODO: Check user event 23 | 24 | describe('APL disabled', () => { 25 | alexaTest.test([ 26 | { 27 | request: new LaunchRequestBuilder(skillSettings) 28 | .withInterfaces({ apl: false }) 29 | .build(), 30 | says: 'I do not support APL', 31 | repromptsNothing: true, 32 | shouldEndSession: true, 33 | }, 34 | ]); 35 | }); 36 | 37 | describe('APL with touch', () => { 38 | alexaTest.test([ 39 | { 40 | request: new LaunchRequestBuilder(skillSettings) 41 | .withInterfaces({ apl: true }) 42 | .build(), 43 | says: 'Check out my APL!', 44 | renderDocument: { 45 | token: 'LAUNCH_TOKEN', 46 | document: (doc : any) => { 47 | return doc !== undefined && doc.version === '1.3'; 48 | }, 49 | hasDataSources: { 50 | textListData: (ds : any) => { 51 | return ds !== undefined && ds.headerTitle === 'Alexa text list header title'; 52 | }, 53 | }, 54 | }, 55 | repromptsNothing: true, 56 | shouldEndSession: true, 57 | }, 58 | { 59 | request: new AplUserEventRequestBuilder(skillSettings) 60 | .withToken('LAUNCH_TOKEN') 61 | .withArguments('ListItemSelected', 1) 62 | .withSource({ 63 | type: 'TouchWrapper', 64 | handler: 'Press', 65 | id: '', 66 | }) 67 | .build(), 68 | says: 'Got launch list item 1', 69 | repromptsNothing: true, 70 | // undefined shouldEndSession has a special interpretation by Alexa. For that reason, we are explicitly setting it to undefined here to serve as documentation. 71 | // See https://developer.amazon.com/en-US/docs/alexa/custom-skills/manage-skill-session-and-session-attributes.html#screen-session 72 | shouldEndSession: undefined, 73 | }, 74 | ]); 75 | }); 76 | 77 | describe('APL touch not handled', () => { 78 | alexaTest.test([ 79 | { 80 | request: new AplUserEventRequestBuilder(skillSettings) 81 | .withArguments('goBack') 82 | .build(), 83 | says: 'Touch arg not handled', 84 | repromptsNothing: true, 85 | shouldEndSession: true, 86 | }, 87 | ]); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /examples/skill-sample-nodejs-apl/apl.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable: no-invalid-template-strings */ 2 | 3 | import { DefaultApiClient, HandlerInput, RequestHandler, SkillBuilders } from 'ask-sdk'; 4 | import { LambdaHandler } from 'ask-sdk-core/dist/skill/factory/BaseSkillFactory'; 5 | import { interfaces, Response } from 'ask-sdk-model'; 6 | import RenderDocumentDirective = interfaces.alexa.presentation.apl.RenderDocumentDirective; 7 | 8 | class LaunchRequestHandler implements RequestHandler { 9 | 10 | public canHandle(handlerInput: HandlerInput): boolean { 11 | return handlerInput.requestEnvelope.request.type === 'LaunchRequest'; 12 | } 13 | 14 | public async handle(handlerInput: HandlerInput): Promise { 15 | const device = handlerInput.requestEnvelope.context.System.device!; 16 | if (device.supportedInterfaces['Alexa.Presentation.APL'] !== undefined) { 17 | const token = 'LAUNCH_TOKEN'; 18 | const directive = this.getRenderDirective(token); 19 | return handlerInput.responseBuilder 20 | .speak('Check out my APL!') 21 | .addDirective(directive) 22 | .withShouldEndSession(true) 23 | .getResponse(); 24 | } else { 25 | return handlerInput.responseBuilder 26 | .speak('I do not support APL') 27 | .withShouldEndSession(true) 28 | .getResponse(); 29 | } 30 | } 31 | 32 | private getRenderDirective(token: string): RenderDocumentDirective { 33 | // Return an Alexa Test List Layout 34 | // See: https://developer.amazon.com/en-US/docs/alexa/alexa-presentation-language/apl-alexa-text-list-layout.html 35 | return { 36 | type: 'Alexa.Presentation.APL.RenderDocument', 37 | document: this.getTemplate(), 38 | token, 39 | datasources: { 40 | textListData: this.getListData(), 41 | }, 42 | }; 43 | } 44 | 45 | private getTemplate(): any { 46 | return { 47 | type: 'APL', 48 | version: '1.3', 49 | import: [ 50 | { 51 | name: 'alexa-layouts', 52 | version: '1.1.0', 53 | }, 54 | ], 55 | mainTemplate: { 56 | parameters: [ 57 | 'textListData', 58 | ], 59 | items: [ 60 | { 61 | type: 'AlexaTextList', 62 | theme: '${viewport.theme}', 63 | headerTitle: '${textListData.headerTitle}', 64 | headerSubtitle: '${textListData.headerSubtitle}', 65 | headerAttributionImage: '${textListData.headerAttributionImage}', 66 | headerDivider: true, 67 | headerBackButton: false, 68 | headerBackButtonAccessibilityLabel: 'back', 69 | headerBackgroundColor: 'transparent', 70 | touchForward: true, 71 | backgroundColor: 'transparent', 72 | hideOrdinal: false, 73 | backgroundImageSource: '${textListData.backgroundImageSource}', 74 | backgroundScale: 'best-fill', 75 | backgroundAlign: 'center', 76 | backgroundBlur: false, 77 | primaryAction: { 78 | type: 'SendEvent', 79 | arguments: [ 80 | 'ListItemSelected', 81 | '${ordinal}', 82 | ], 83 | }, 84 | listItems: '${textListData.listItemsToShow}', 85 | }, 86 | ], 87 | }, 88 | }; 89 | } 90 | 91 | private getListData(): any { 92 | return { 93 | headerTitle: 'Alexa text list header title', 94 | headerSubtitle: 'Header subtitle', 95 | headerAttributionImage: 'https://s3.amazonaws.com/ask-skills-assets/apl-layout-assets/attribution_dark_hub_prime.png', 96 | backgroundImageSource: 'https://d2o906d8ln7ui1.cloudfront.net/images/BT6_Background.png', 97 | listItemsToShow: [ 98 | { 99 | primaryText: 'First item in the list.', 100 | }, 101 | { 102 | primaryText: 'Second item in the list.', 103 | }, 104 | { 105 | primaryText: 'Third item in the list.', 106 | }, 107 | { 108 | primaryText: 'Fourth item in the list', 109 | }, 110 | { 111 | primaryText: 'Fifth item in the list', 112 | }, 113 | { 114 | primaryText: 'This list might have many more items', 115 | }, 116 | ], 117 | }; 118 | } 119 | 120 | } 121 | 122 | class TouchHandler implements RequestHandler { 123 | 124 | public canHandle(handlerInput: HandlerInput): boolean { 125 | return handlerInput.requestEnvelope.request.type === 'Alexa.Presentation.APL.UserEvent'; 126 | } 127 | 128 | public async handle(handlerInput: HandlerInput): Promise { 129 | if (handlerInput.requestEnvelope.request.type === 'Alexa.Presentation.APL.UserEvent' && 130 | handlerInput.requestEnvelope.request.arguments !== undefined) { 131 | if (handlerInput.requestEnvelope.request.arguments[0] === 'ListItemSelected') { 132 | const arg = handlerInput.requestEnvelope.request.arguments[1]; 133 | return handlerInput.responseBuilder.speak(`Got launch list item ${arg}`) 134 | .getResponse(); 135 | } 136 | } 137 | 138 | return handlerInput.responseBuilder.speak('Touch arg not handled').withShouldEndSession(true).getResponse(); 139 | } 140 | } 141 | 142 | export const handler: LambdaHandler = SkillBuilders.custom() 143 | .addRequestHandlers( 144 | new LaunchRequestHandler(), 145 | new TouchHandler(), 146 | ) 147 | .withApiClient(new DefaultApiClient()) 148 | .lambda(); 149 | -------------------------------------------------------------------------------- /examples/skill-sample-nodejs-apla/apla.spec.ts: -------------------------------------------------------------------------------- 1 | import { AlexaTest, LaunchRequestBuilder, SkillSettings } from '../../src'; 2 | import { handler as skillHandler } from './apla'; 3 | 4 | // initialize the testing framework 5 | const skillSettings: SkillSettings = { 6 | appId: 'amzn1.ask.skill.00000000-0000-0000-0000-000000000000', 7 | userId: 'amzn1.ask.account.VOID', 8 | deviceId: 'amzn1.ask.device.VOID', 9 | locale: 'en-US', 10 | }; 11 | 12 | const alexaTest = new AlexaTest(skillHandler, skillSettings); 13 | 14 | describe('APLA', () => { 15 | 16 | describe('APLA directives', () => { 17 | alexaTest.test([ 18 | { 19 | request: new LaunchRequestBuilder(skillSettings) 20 | .build(), 21 | says: 'Check out my APL for Audio!', 22 | renderDocument: { 23 | token: 'LAUNCH_TOKEN', 24 | document: (doc: any) => { 25 | return doc !== undefined && doc.version === '0.9'; 26 | }, 27 | hasDataSources: { 28 | payload: (ds: any) => { 29 | return ds !== undefined && ds.user.name === 'John'; 30 | }, 31 | }, 32 | }, 33 | repromptsNothing: true, 34 | shouldEndSession: true, 35 | }, 36 | ]); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /examples/skill-sample-nodejs-apla/apla.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable: no-invalid-template-strings */ 2 | 3 | import { DefaultApiClient, HandlerInput, RequestHandler, SkillBuilders } from 'ask-sdk'; 4 | import { LambdaHandler } from 'ask-sdk-core/dist/skill/factory/BaseSkillFactory'; 5 | import { interfaces, Response } from 'ask-sdk-model'; 6 | import RenderDocumentDirective = interfaces.alexa.presentation.apla.RenderDocumentDirective; 7 | 8 | class LaunchRequestHandler implements RequestHandler { 9 | 10 | public canHandle(handlerInput: HandlerInput): boolean { 11 | return handlerInput.requestEnvelope.request.type === 'LaunchRequest'; 12 | } 13 | 14 | public async handle(handlerInput: HandlerInput): Promise { 15 | const token = 'LAUNCH_TOKEN'; 16 | const directive = this.getRenderDirective(token); 17 | return handlerInput.responseBuilder 18 | .speak('Check out my APL for Audio!') 19 | .addDirective(directive) 20 | .withShouldEndSession(true) 21 | .getResponse(); 22 | } 23 | 24 | private getRenderDirective(token: string): RenderDocumentDirective { 25 | // Return an APLA docuemnt 26 | // See: https://developer.amazon.com/en-US/docs/alexa/alexa-presentation-language/apla-interface.html#renderdocument-directive 27 | return { 28 | type: 'Alexa.Presentation.APLA.RenderDocument', 29 | document: this.getTemplate(), 30 | token, 31 | datasources: { 32 | payload: this.getPayload(), 33 | }, 34 | }; 35 | } 36 | 37 | private getTemplate(): any { 38 | return { 39 | version: "0.9", 40 | type: "APLA", 41 | mainTemplate: { 42 | parameters: [ 43 | "payload" 44 | ], 45 | item: { 46 | type: "Selector", 47 | items: [ 48 | { 49 | type: "Speech", 50 | when: "${payload.user.name == ''}", 51 | content: "Hello!" 52 | }, 53 | { 54 | type: "Speech", 55 | content: "Hi ${payload.user.name}!" 56 | } 57 | ] 58 | } 59 | } 60 | }; 61 | } 62 | 63 | private getPayload(): any { 64 | return { 65 | user: { 66 | name: "John" 67 | } 68 | }; 69 | } 70 | 71 | } 72 | 73 | export const handler: LambdaHandler = SkillBuilders.custom() 74 | .addRequestHandlers( 75 | new LaunchRequestHandler(), 76 | ) 77 | .withApiClient(new DefaultApiClient()) 78 | .lambda(); 79 | -------------------------------------------------------------------------------- /examples/skill-sample-nodejs-audioplayer/audioplayer.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { AlexaTest, IntentRequestBuilder, LaunchRequestBuilder, SkillSettings } from '../../src'; 6 | import { AudioPlayerPauseIntentRequestBuilder, AudioPlayerResumeIntentRequestBuilder } from '../../src/factory/AudioIntentRequestBuilder'; 7 | import { handler as skillHandler } from './audioplayer'; 8 | 9 | // initialize the testing framework 10 | const skillSettings : SkillSettings = { 11 | appId: 'amzn1.ask.skill.00000000-0000-0000-0000-000000000000', 12 | userId: 'amzn1.ask.account.VOID', 13 | deviceId: 'amzn1.ask.device.VOID', 14 | locale: 'en-US', 15 | }; 16 | 17 | const alexaTest = new AlexaTest(skillHandler, skillSettings); 18 | 19 | describe('Audio Player Skill', () => { 20 | 'use strict'; 21 | 22 | describe('LaunchRequest', () => { 23 | alexaTest.test([ 24 | { 25 | request: new LaunchRequestBuilder(skillSettings).build(), 26 | says: 'Hello World!', repromptsNothing: true, shouldEndSession: true, 27 | }, 28 | ]); 29 | }); 30 | 31 | describe('PlayStreamIntent', () => { 32 | alexaTest.test([ 33 | { 34 | request: new IntentRequestBuilder(skillSettings, 'PlayStreamIntent').build(), 35 | playsStream: { 36 | behavior: 'REPLACE_ALL', 37 | url: 'https://superAudio.stream', 38 | token: 'superToken', 39 | }, 40 | }, 41 | ]); 42 | }); 43 | 44 | describe('ClearQueueIntent', () => { 45 | alexaTest.test([ 46 | { 47 | request: new IntentRequestBuilder(skillSettings, 'ClearQueueIntent').build(), 48 | clearsQueue: 'CLEAR_ALL', 49 | }, 50 | ]); 51 | }); 52 | 53 | describe('AMAZON.ResumeIntent', () => { 54 | alexaTest.test([ 55 | { 56 | request: new AudioPlayerResumeIntentRequestBuilder(skillSettings).build(), 57 | playsStream: { 58 | behavior: 'REPLACE_ALL', 59 | url: 'https://superAudio.stream', 60 | token: 'superToken', 61 | offset: 0, 62 | }, 63 | }, 64 | ]); 65 | }); 66 | 67 | describe('AMAZON.ResumeIntent at position', () => { 68 | alexaTest.test([ 69 | { 70 | request: new AudioPlayerResumeIntentRequestBuilder(skillSettings).withToken('superToken').withOffset(123).build(), 71 | playsStream: { 72 | behavior: 'REPLACE_ALL', 73 | url: 'https://superAudio.stream', 74 | token: 'superToken', 75 | offset: 123, 76 | }, 77 | }, 78 | ]); 79 | }); 80 | 81 | describe('AMAZON.PauseIntent', () => { 82 | alexaTest.test([ 83 | { 84 | request: new AudioPlayerPauseIntentRequestBuilder(skillSettings).build(), 85 | stopsStream: true, 86 | }, 87 | ]); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /examples/skill-sample-nodejs-audioplayer/audioplayer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { HandlerInput, RequestHandler, SkillBuilders } from 'ask-sdk'; 6 | import { LambdaHandler } from 'ask-sdk-core/dist/skill/factory/BaseSkillFactory'; 7 | import { Response } from 'ask-sdk-model'; 8 | 9 | class LaunchRequestHandler implements RequestHandler { 10 | 11 | public canHandle(handlerInput: HandlerInput): boolean { 12 | return handlerInput.requestEnvelope.request.type === 'LaunchRequest'; 13 | } 14 | 15 | public handle(handlerInput: HandlerInput): Response { 16 | return handlerInput.responseBuilder.speak('Hello World!').withShouldEndSession(true).getResponse(); 17 | } 18 | 19 | } 20 | 21 | class PlayStreamIntentHandler implements RequestHandler { 22 | 23 | public canHandle(handlerInput: HandlerInput): boolean { 24 | return handlerInput.requestEnvelope.request.type === 'IntentRequest' 25 | && handlerInput.requestEnvelope.request.intent.name === 'PlayStreamIntent'; 26 | } 27 | 28 | public handle(handlerInput: HandlerInput): Response { 29 | return handlerInput.responseBuilder 30 | .addAudioPlayerPlayDirective('REPLACE_ALL', 'https://superAudio.stream', 'superToken', 0, undefined) 31 | .getResponse(); 32 | } 33 | } 34 | 35 | class ClearQueueIntentHandler implements RequestHandler { 36 | 37 | public canHandle(handlerInput: HandlerInput): boolean { 38 | return handlerInput.requestEnvelope.request.type === 'IntentRequest' 39 | && handlerInput.requestEnvelope.request.intent.name === 'ClearQueueIntent'; 40 | } 41 | 42 | public handle(handlerInput: HandlerInput): Response { 43 | return handlerInput.responseBuilder 44 | .addAudioPlayerClearQueueDirective('CLEAR_ALL') 45 | .getResponse(); 46 | } 47 | } 48 | 49 | class StopAudioHandler implements RequestHandler { 50 | 51 | public canHandle(handlerInput: HandlerInput): boolean { 52 | return handlerInput.requestEnvelope.request.type === 'IntentRequest' 53 | && (handlerInput.requestEnvelope.request.intent.name === 'AMAZON.PauseIntent' || 54 | handlerInput.requestEnvelope.request.intent.name === 'AMAZON.StopIntent' || 55 | handlerInput.requestEnvelope.request.intent.name === 'AMAZON.CancelIntent'); 56 | } 57 | 58 | public handle(handlerInput: HandlerInput): Response { 59 | return handlerInput.responseBuilder 60 | .addAudioPlayerStopDirective() 61 | .getResponse(); 62 | } 63 | } 64 | 65 | class ResumeIntentHandler implements RequestHandler { 66 | 67 | public canHandle(handlerInput: HandlerInput): boolean { 68 | return handlerInput.requestEnvelope.request.type === 'IntentRequest' 69 | && handlerInput.requestEnvelope.request.intent.name === 'AMAZON.ResumeIntent'; 70 | } 71 | 72 | public handle(handlerInput: HandlerInput): Response { 73 | const offset = handlerInput.requestEnvelope.context.AudioPlayer ? handlerInput.requestEnvelope.context.AudioPlayer.offsetInMilliseconds || 0 : 0; 74 | return handlerInput.responseBuilder 75 | .addAudioPlayerPlayDirective('REPLACE_ALL', 'https://superAudio.stream', 'superToken', offset, 'superToken') 76 | .getResponse(); 77 | } 78 | } 79 | 80 | export const handler: LambdaHandler = SkillBuilders.custom() 81 | .addRequestHandlers( 82 | new LaunchRequestHandler(), 83 | new PlayStreamIntentHandler(), 84 | new ClearQueueIntentHandler(), 85 | new StopAudioHandler(), 86 | new ResumeIntentHandler(), 87 | ) 88 | .lambda(); 89 | -------------------------------------------------------------------------------- /examples/skill-sample-nodejs-dynamodb/helloworld.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { AlexaTest, IntentRequestBuilder, LaunchRequestBuilder, SkillSettings } from '../../src'; 6 | import { handler as skillHandler } from './helloworld'; 7 | 8 | // initialize the testing framework 9 | const skillSettings : SkillSettings = { 10 | appId: 'amzn1.ask.skill.00000000-0000-0000-0000-000000000000', 11 | userId: 'amzn1.ask.account.VOID', 12 | deviceId: 'amzn1.ask.device.VOID', 13 | locale: 'en-US', 14 | }; 15 | const alexaTest = new AlexaTest(skillHandler, skillSettings).withDynamoDBPersistence('TestTable', 'userId', 'mapAttr'); 16 | 17 | describe('Hello World Skill DynamoDB', () => { 18 | // tests the behavior of the skill's LaunchRequest 19 | describe('LaunchRequest', () => { 20 | alexaTest.test([ 21 | { 22 | request: new LaunchRequestBuilder(skillSettings).build(), 23 | says: 'Hello World!', 24 | repromptsNothing: true, 25 | shouldEndSession: true, 26 | }, 27 | ]); 28 | }); 29 | 30 | // tests the behavior of the skill's HelloWorldIntent 31 | describe('HelloWorldIntent', () => { 32 | alexaTest.test([ 33 | { 34 | request: new IntentRequestBuilder(skillSettings, 'HelloWorldIntent').build(), 35 | says: 'Hello World!', repromptsNothing: true, shouldEndSession: true, 36 | storesAttributes: { 37 | foo: 'bar', 38 | count: 1, 39 | }, 40 | }, 41 | ]); 42 | }); 43 | 44 | // tests the behavior of the skill's HelloWorldIntent using validation function 45 | describe('HelloWorldIntent', () => { 46 | alexaTest.test([ 47 | { 48 | request: new IntentRequestBuilder(skillSettings, 'HelloWorldIntent').build(), 49 | says: 'Hello World!', repromptsNothing: true, shouldEndSession: true, 50 | storesAttributes: { 51 | foo: (value) => { 52 | return value === 'bar'; 53 | }, 54 | }, 55 | }, 56 | ]); 57 | }); 58 | 59 | // tests the behavior of the skill's HelloWorldIntent using validation function 60 | describe('SayGoodbye', () => { 61 | alexaTest.test([ 62 | { 63 | request: new IntentRequestBuilder(skillSettings, 'SayGoodbye').build(), 64 | says: 'Bye bar!', shouldEndSession: true, 65 | withStoredAttributes: { 66 | foo: 'bar', 67 | }, 68 | }, 69 | ]); 70 | }); 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /examples/skill-sample-nodejs-dynamodb/helloworld.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { DefaultApiClient, DynamoDbPersistenceAdapter, HandlerInput, RequestHandler, SkillBuilders } from 'ask-sdk'; 6 | import { LambdaHandler } from 'ask-sdk-core/dist/skill/factory/BaseSkillFactory'; 7 | import { Response } from 'ask-sdk-model'; 8 | 9 | class SayHelloHandler implements RequestHandler { 10 | 11 | public canHandle(handlerInput : HandlerInput) : boolean { 12 | return handlerInput.requestEnvelope.request.type === 'LaunchRequest' || 13 | (handlerInput.requestEnvelope.request.type === 'IntentRequest' && handlerInput.requestEnvelope.request.intent.name === 'HelloWorldIntent') || 14 | (handlerInput.requestEnvelope.request.type === 'IntentRequest' && handlerInput.requestEnvelope.request.intent.name === 'SayHello'); 15 | } 16 | 17 | public async handle(handlerInput : HandlerInput) : Promise { 18 | const attributes = await handlerInput.attributesManager.getPersistentAttributes(); 19 | attributes.foo = 'bar'; 20 | attributes.count = 1; 21 | handlerInput.attributesManager.setPersistentAttributes(attributes); 22 | await handlerInput.attributesManager.savePersistentAttributes(); 23 | 24 | return handlerInput.responseBuilder.speak('Hello World!').withShouldEndSession(true).getResponse(); 25 | } 26 | } 27 | 28 | class SayGoodbyeHandler implements RequestHandler { 29 | 30 | public canHandle(handlerInput : HandlerInput) : boolean { 31 | return handlerInput.requestEnvelope.request.type === 'IntentRequest' 32 | && handlerInput.requestEnvelope.request.intent.name === 'SayGoodbye'; 33 | } 34 | 35 | public async handle(handlerInput : HandlerInput) : Promise { 36 | const attributes = await handlerInput.attributesManager.getPersistentAttributes(); 37 | return handlerInput.responseBuilder.speak(`Bye ${attributes.foo}!`).withShouldEndSession(true).getResponse(); 38 | } 39 | } 40 | 41 | export const handler : LambdaHandler = SkillBuilders.custom() 42 | .withPersistenceAdapter(new DynamoDbPersistenceAdapter({ 43 | tableName: 'TestTable', 44 | partitionKeyName: 'userId', 45 | attributesName: 'mapAttr', 46 | })) 47 | .withApiClient(new DefaultApiClient()) 48 | .addRequestHandlers( 49 | new SayHelloHandler(), 50 | new SayGoodbyeHandler(), 51 | ) 52 | .lambda(); 53 | -------------------------------------------------------------------------------- /examples/skill-sample-nodejs-integration/helloworld.it.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { AlexaTest, LaunchRequestBuilder, SkillSettings } from '../../src'; 6 | 7 | // initialize the testing framework 8 | const skillSettings : SkillSettings = { 9 | appId: 'amzn1.ask.skill.a5cbce33-2287-40ad-a408-d8ccccb4c794', 10 | userId: 'amzn1.ask.account.VOID', 11 | deviceId: 'amzn1.ask.device.VOID', 12 | locale: 'en-US', 13 | debug: true, 14 | }; 15 | 16 | // Using Lambda function name 17 | // const alexaTest = new AlexaTest('lambda:pse-skill-SkillFunction-4EYL1F63JXV', skillSettings); 18 | 19 | // Using Logical function name from CloudFormation 20 | const alexaTest = new AlexaTest('cfn:pse-skill:SkillFunction', skillSettings); 21 | 22 | describe('Hello World Skill', () => { 23 | // tests the behavior of the skill's LaunchRequest 24 | describe('LaunchRequest', () => { 25 | alexaTest.test([ 26 | { 27 | request: new LaunchRequestBuilder(skillSettings).build(), 28 | says: "Willkommen bei Taimos P.S.E.! Wie kann ich helfen?", 29 | shouldEndSession: false, 30 | }, 31 | ]); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /examples/skill-sample-nodejs-multistring/helloworld.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { AlexaTest, IntentRequestBuilder, LaunchRequestBuilder, SkillSettings } from '../../src'; 6 | import { handler as skillHandler } from './helloworld'; 7 | 8 | // initialize the testing framework 9 | const skillSettings : SkillSettings = { 10 | appId: 'amzn1.ask.skill.00000000-0000-0000-0000-000000000000', 11 | userId: 'amzn1.ask.account.VOID', 12 | deviceId: 'amzn1.ask.device.VOID', 13 | locale: 'en-US', 14 | }; 15 | 16 | const alexaTest = new AlexaTest(skillHandler, skillSettings); 17 | 18 | describe('Hello World Skill with MultiStrings', () => { 19 | // tests the behavior of the skill's LaunchRequest 20 | describe('LaunchRequest', () => { 21 | alexaTest.test([ 22 | { 23 | request: new LaunchRequestBuilder(skillSettings).build(), 24 | says: ['Hello, how are you?', 'Hi, what\'s up?', 'Good day, how are you doing?'], 25 | reprompts: ['How are you?', 'How do you feel?'], 26 | shouldEndSession: false, 27 | }, 28 | ]); 29 | }); 30 | 31 | // tests the behavior of the skill's HelloWorldIntent 32 | describe('HelloWorldIntent', () => { 33 | alexaTest.test([ 34 | { 35 | request: new IntentRequestBuilder(skillSettings, 'HelloWorldIntent').build(), 36 | says: ['Hello, how are you?', 'Hi, what\'s up?', 'Good day, how are you doing?'], 37 | reprompts: ['How are you?', 'How do you feel?'], 38 | shouldEndSession: false, 39 | }, 40 | ]); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /examples/skill-sample-nodejs-multistring/helloworld.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { HandlerInput, RequestHandler, SkillBuilders } from 'ask-sdk'; 6 | import { LambdaHandler } from 'ask-sdk-core/dist/skill/factory/BaseSkillFactory'; 7 | import { Response } from 'ask-sdk-model'; 8 | 9 | class LaunchRequestHandler implements RequestHandler { 10 | 11 | public canHandle(handlerInput : HandlerInput) : boolean { 12 | return handlerInput.requestEnvelope.request.type === 'LaunchRequest' 13 | || (handlerInput.requestEnvelope.request.type === 'IntentRequest' 14 | && handlerInput.requestEnvelope.request.intent.name === 'HelloWorldIntent'); 15 | } 16 | 17 | public handle(handlerInput : HandlerInput) : Response { 18 | const speech = ['Hello, how are you?', 'Hi, what\'s up?', 'Good day, how are you doing?']; 19 | const reprompt = ['How are you?', 'How do you feel?']; 20 | return handlerInput.responseBuilder 21 | .speak(speech[Math.floor(Math.random() * speech.length)]) 22 | .reprompt(reprompt[Math.floor(Math.random() * reprompt.length)]) 23 | .getResponse(); 24 | } 25 | 26 | } 27 | 28 | export const handler : LambdaHandler = SkillBuilders.custom() 29 | .addRequestHandlers( 30 | new LaunchRequestHandler(), 31 | ) 32 | .lambda(); 33 | -------------------------------------------------------------------------------- /examples/skill-sample-nodejs-profileapi/helloworld.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { AlexaTest, LaunchRequestBuilder, SkillSettings } from '../../src'; 6 | import { handler as skillHandler } from './helloworld'; 7 | 8 | // initialize the testing framework 9 | const skillSettings : SkillSettings = { 10 | appId: 'amzn1.ask.skill.00000000-0000-0000-0000-000000000000', 11 | userId: 'amzn1.ask.account.VOID', 12 | deviceId: 'amzn1.ask.device.VOID', 13 | locale: 'en-US', 14 | }; 15 | 16 | const alexaTest = new AlexaTest(skillHandler, skillSettings); 17 | 18 | describe('Hello World Skill Profile API', () => { 19 | 20 | describe('LaunchRequest', () => { 21 | alexaTest.test([ 22 | { 23 | request: new LaunchRequestBuilder(skillSettings).build(), 24 | withProfile: { 25 | givenName: 'John', 26 | name: 'Smith', 27 | email: 'john@smith.com', 28 | mobileNumber: '+1234567890', 29 | }, 30 | says: 'Hello, John Smith. Your e-mail is john@smith.com and your phone number is +1234567890', 31 | repromptsNothing: true, 32 | shouldEndSession: true, 33 | }, 34 | ]); 35 | }); 36 | 37 | describe('LaunchRequest without profile info', () => { 38 | alexaTest.test([ 39 | { 40 | request: new LaunchRequestBuilder(skillSettings).build(), 41 | says: 'Hello, world! I am not allowed to view your profile.', 42 | repromptsNothing: true, 43 | shouldEndSession: true, 44 | }, 45 | ]); 46 | }); 47 | 48 | }); 49 | -------------------------------------------------------------------------------- /examples/skill-sample-nodejs-profileapi/helloworld.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { DefaultApiClient, HandlerInput, RequestHandler, SkillBuilders } from 'ask-sdk'; 6 | import { LambdaHandler } from 'ask-sdk-core/dist/skill/factory/BaseSkillFactory'; 7 | import { Response } from 'ask-sdk-model'; 8 | 9 | class LaunchRequestHandler implements RequestHandler { 10 | 11 | public canHandle(handlerInput: HandlerInput): boolean { 12 | return handlerInput.requestEnvelope.request.type === 'LaunchRequest'; 13 | } 14 | 15 | public async handle(handlerInput: HandlerInput): Promise { 16 | const upsServiceClient = handlerInput.serviceClientFactory!.getUpsServiceClient(); 17 | 18 | try { 19 | const name = await upsServiceClient.getProfileName(); 20 | const givenName = await upsServiceClient.getProfileGivenName(); 21 | const email = await upsServiceClient.getProfileEmail(); 22 | const mobile = await upsServiceClient.getProfileMobileNumber(); 23 | 24 | const speechText = `Hello, ${givenName} ${name}. Your e-mail is ${email} and your phone number is ${mobile}`; 25 | return handlerInput.responseBuilder.speak(speechText).withShouldEndSession(true).getResponse(); 26 | } catch (e) { 27 | return handlerInput.responseBuilder.speak('Hello, world! I am not allowed to view your profile.').withShouldEndSession(true).getResponse(); 28 | } 29 | } 30 | 31 | } 32 | 33 | export const handler: LambdaHandler = SkillBuilders.custom() 34 | .addRequestHandlers( 35 | new LaunchRequestHandler(), 36 | ) 37 | .withApiClient(new DefaultApiClient()) 38 | .lambda(); 39 | -------------------------------------------------------------------------------- /examples/skill-sample-nodejs/helloworld.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { AlexaTest, IntentRequestBuilder, LaunchRequestBuilder, SkillSettings } from '../../src'; 6 | import { handler as skillHandler } from './helloworld'; 7 | 8 | // initialize the testing framework 9 | const skillSettings : SkillSettings = { 10 | appId: 'amzn1.ask.skill.00000000-0000-0000-0000-000000000000', 11 | userId: 'amzn1.ask.account.VOID', 12 | deviceId: 'amzn1.ask.device.VOID', 13 | locale: 'en-US', 14 | }; 15 | 16 | const alexaTest = new AlexaTest(skillHandler, skillSettings); 17 | 18 | describe('Hello World Skill', () => { 19 | // tests the behavior of the skill's LaunchRequest 20 | describe('LaunchRequest', () => { 21 | alexaTest.test([ 22 | { 23 | request: new LaunchRequestBuilder(skillSettings).build(), 24 | says: 'Welcome to the Alexa Skills Kit, you can say hello!', 25 | repromptsNothing: true, 26 | shouldEndSession: true, 27 | }, 28 | ]); 29 | }); 30 | 31 | // tests the behavior of the skill's HelloWorldIntent 32 | describe('HelloWorldIntent', () => { 33 | alexaTest.test([ 34 | { 35 | request: new IntentRequestBuilder(skillSettings, 'HelloWorldIntent').build(), 36 | says: 'Hello World!', repromptsNothing: true, shouldEndSession: true, 37 | withSessionAttributes: { 38 | bar: true, 39 | }, 40 | hasAttributes: { 41 | foo: 'bar', 42 | count: 1, 43 | bar: true, 44 | }, 45 | }, 46 | ]); 47 | }); 48 | 49 | // tests the behavior of the skill's HelloWorldIntent with like operator 50 | describe('HelloWorldIntent like', () => { 51 | alexaTest.test([ 52 | { 53 | request: new IntentRequestBuilder(skillSettings, 'HelloWorldIntent').build(), 54 | saysLike: 'World', repromptsNothing: true, shouldEndSession: true, 55 | }, 56 | ]); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /examples/skill-sample-nodejs/helloworld.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { HandlerInput, RequestHandler, SkillBuilders } from 'ask-sdk'; 6 | import { LambdaHandler } from 'ask-sdk-core/dist/skill/factory/BaseSkillFactory'; 7 | import { Response } from 'ask-sdk-model'; 8 | 9 | class LaunchRequestHandler implements RequestHandler { 10 | 11 | public canHandle(handlerInput : HandlerInput) : boolean { 12 | return handlerInput.requestEnvelope.request.type === 'LaunchRequest'; 13 | } 14 | 15 | public handle(handlerInput : HandlerInput) : Response { 16 | const speechText = 'Welcome to the Alexa Skills Kit, you can say hello!'; 17 | return handlerInput.responseBuilder 18 | .speak(speechText) 19 | .withSimpleCard('Hello World', speechText) 20 | .withShouldEndSession(true) 21 | .getResponse(); 22 | } 23 | 24 | } 25 | 26 | class HelloWorldIntentHandler implements RequestHandler { 27 | 28 | public canHandle(handlerInput : HandlerInput) : boolean { 29 | return handlerInput.requestEnvelope.request.type === 'IntentRequest' 30 | && handlerInput.requestEnvelope.request.intent.name === 'HelloWorldIntent'; 31 | } 32 | 33 | public handle(handlerInput : HandlerInput) : Response { 34 | const speechText = 'Hello World!'; 35 | 36 | const attributes = handlerInput.attributesManager.getSessionAttributes(); 37 | attributes.foo = 'bar'; 38 | attributes.count = 1; 39 | handlerInput.attributesManager.setSessionAttributes(attributes); 40 | 41 | return handlerInput.responseBuilder 42 | .speak(speechText) 43 | .withSimpleCard('Hello World', speechText) 44 | .withShouldEndSession(true) 45 | .getResponse(); 46 | } 47 | } 48 | 49 | export const handler : LambdaHandler = SkillBuilders.custom() 50 | .addRequestHandlers( 51 | new LaunchRequestHandler(), 52 | new HelloWorldIntentHandler(), 53 | ) 54 | .lambda(); 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ask-sdk-test", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/taimos/ask-sdk-test" 6 | }, 7 | "scripts": { 8 | "build": "npx projen build", 9 | "bump": "npx projen bump", 10 | "clobber": "npx projen clobber", 11 | "compile": "npx projen compile", 12 | "default": "npx projen default", 13 | "docgen": "npx projen docgen", 14 | "eject": "npx projen eject", 15 | "eslint": "npx projen eslint", 16 | "package": "npx projen package", 17 | "post-compile": "npx projen post-compile", 18 | "post-upgrade": "npx projen post-upgrade", 19 | "pre-compile": "npx projen pre-compile", 20 | "release": "npx projen release", 21 | "test": "npx projen test", 22 | "unbump": "npx projen unbump", 23 | "upgrade": "npx projen upgrade", 24 | "watch": "npx projen watch", 25 | "projen": "npx projen" 26 | }, 27 | "author": { 28 | "name": "Taimos GmbH", 29 | "email": "info@taimos.de", 30 | "url": "https://taimos.de", 31 | "organization": true 32 | }, 33 | "devDependencies": { 34 | "@taimos/projen": "^0.0.214", 35 | "@types/chai": "^4.3.19", 36 | "@types/mocha": "^10.0.7", 37 | "@types/node": "^18", 38 | "@types/sinon": "^10.0.20", 39 | "@types/uuid": "^9.0.8", 40 | "@typescript-eslint/eslint-plugin": "^7", 41 | "@typescript-eslint/parser": "^7", 42 | "ask-sdk": "2.10.0", 43 | "ask-sdk-core": "2.10.1", 44 | "ask-sdk-model": "1.34.1", 45 | "chai": "^4.5.0", 46 | "constructs": "^10.0.0", 47 | "eslint": "^8", 48 | "eslint-import-resolver-typescript": "^3.6.3", 49 | "eslint-plugin-import": "^2.29.1", 50 | "mocha": "10.7.3", 51 | "nyc": "^15.1.0", 52 | "projen": "^0.85.2", 53 | "rimraf": "^5.0.10", 54 | "sinon": "^15.0.2", 55 | "standard-version": "^9", 56 | "ts-node": "^10.9.2", 57 | "typedoc": "^0.26.6", 58 | "typescript": "^4.9.5" 59 | }, 60 | "peerDependencies": { 61 | "ask-sdk": "^2.10.0", 62 | "ask-sdk-core": "^2.10.1", 63 | "ask-sdk-model": "^1.34.1", 64 | "chai": "^4.5.0", 65 | "mocha": "^10.7.3" 66 | }, 67 | "dependencies": { 68 | "aws-sdk": "^2.1687.0", 69 | "aws-sdk-mock": "^5.9.0", 70 | "lambda-local": "^2.2.0", 71 | "nock": "^13.5.5", 72 | "uuid": "^9.0.1" 73 | }, 74 | "resolutions": { 75 | "ask-sdk-model": "1.34.1" 76 | }, 77 | "keywords": [ 78 | "alexa", 79 | "amazon", 80 | "aws", 81 | "black box", 82 | "black-box", 83 | "coverage", 84 | "echo", 85 | "mocha", 86 | "offline", 87 | "test" 88 | ], 89 | "main": "lib/index.js", 90 | "license": "MIT", 91 | "publishConfig": { 92 | "access": "public" 93 | }, 94 | "version": "0.0.0", 95 | "types": "lib/index.d.ts", 96 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 97 | } 98 | -------------------------------------------------------------------------------- /src/factory/AplUserEventRequestBuilder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { interfaces } from 'ask-sdk-model'; 6 | import { v4 } from 'uuid'; 7 | import { RequestBuilder } from './RequestBuilder'; 8 | import { SkillSettings } from '../types'; 9 | 10 | import UserEvent = interfaces.alexa.presentation.apl.UserEvent; 11 | 12 | export class AplUserEventRequestBuilder extends RequestBuilder { 13 | private token?: string; 14 | private arguments?: any[]; 15 | private source: any | undefined; 16 | private components: any | undefined; 17 | 18 | constructor(settings: SkillSettings) { 19 | super(settings); 20 | } 21 | 22 | public withToken(token: string): AplUserEventRequestBuilder { 23 | this.token = token; 24 | return this; 25 | } 26 | 27 | public withArguments(...args: any[]): AplUserEventRequestBuilder { 28 | this.arguments = args; 29 | return this; 30 | } 31 | 32 | public withSource(source: any): AplUserEventRequestBuilder { 33 | this.source = source; 34 | return this; 35 | } 36 | 37 | public withComponents(components: string): AplUserEventRequestBuilder { 38 | this.components = components; 39 | return this; 40 | } 41 | 42 | protected buildRequest(): UserEvent { 43 | return { 44 | type: 'Alexa.Presentation.APL.UserEvent', 45 | requestId: `EdwRequestId.${v4()}`, 46 | timestamp: new Date().toISOString(), 47 | locale: this.settings.locale, 48 | token: this.token, 49 | arguments: this.arguments, 50 | components: this.components, 51 | source: this.source, 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/factory/AudioIntentRequestBuilder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { interfaces, RequestEnvelope } from 'ask-sdk-model'; 6 | import { IntentRequestBuilder } from './IntentRequestBuilder'; 7 | import { SkillSettings } from '../types'; 8 | import PlayerActivity = interfaces.audioplayer.PlayerActivity; 9 | 10 | export class AudioPlayerResumeIntentRequestBuilder extends IntentRequestBuilder { 11 | private token?: string; 12 | private offset?: number; 13 | private activity?: interfaces.audioplayer.PlayerActivity; 14 | 15 | constructor(settings: SkillSettings) { 16 | super(settings, 'AMAZON.ResumeIntent'); 17 | } 18 | 19 | public withToken(token: string): AudioPlayerResumeIntentRequestBuilder { 20 | this.token = token; 21 | return this; 22 | } 23 | 24 | public withOffset(offset: number): AudioPlayerResumeIntentRequestBuilder { 25 | this.offset = offset; 26 | return this; 27 | } 28 | 29 | public withCurrentActivity(activity: PlayerActivity): AudioPlayerResumeIntentRequestBuilder { 30 | this.activity = activity; 31 | return this; 32 | } 33 | 34 | protected modifyRequest(request: RequestEnvelope): void { 35 | super.modifyRequest(request); 36 | if (!request.context.AudioPlayer) { 37 | request.context.AudioPlayer = {}; 38 | } 39 | if (this.token) { 40 | request.context.AudioPlayer.token = this.token; 41 | } 42 | if (this.offset) { 43 | request.context.AudioPlayer.offsetInMilliseconds = this.offset; 44 | } 45 | if (this.activity) { 46 | request.context.AudioPlayer.playerActivity = this.activity; 47 | } 48 | } 49 | 50 | } 51 | 52 | export class AudioPlayerPauseIntentRequestBuilder extends IntentRequestBuilder { 53 | 54 | constructor(settings: SkillSettings) { 55 | super(settings, 'AMAZON.PauseIntent'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/factory/IntentRequestBuilder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { IntentConfirmationStatus, Request, Slot, SlotConfirmationStatus } from 'ask-sdk-model'; 6 | import { v4 } from 'uuid'; 7 | import { RequestBuilder } from './RequestBuilder'; 8 | import { SkillSettings } from '../types'; 9 | 10 | export class IntentRequestBuilder extends RequestBuilder { 11 | 12 | private intentName: string; 13 | private slots?: { [key: string]: Slot }; 14 | private confirmationStatus: IntentConfirmationStatus; 15 | 16 | constructor(settings: SkillSettings, intentName: string) { 17 | super(settings); 18 | this.intentName = intentName; 19 | this.confirmationStatus = 'NONE'; 20 | } 21 | 22 | public withEmptySlot(name: string): IntentRequestBuilder { 23 | return this.withSlotConfirmation(name, 'NONE'); 24 | } 25 | 26 | public withSlot(name: string, value: string): IntentRequestBuilder { 27 | return this.withSlotConfirmation(name, 'NONE', value); 28 | } 29 | 30 | public withSlotConfirmation(name: string, confirmationStatus: SlotConfirmationStatus, value?: string): IntentRequestBuilder { 31 | if (!this.slots) { 32 | this.slots = {}; 33 | } 34 | if (!this.slots[name]) { 35 | this.slots[name] = { name, value, confirmationStatus }; 36 | } else { 37 | this.slots[name].confirmationStatus = confirmationStatus; 38 | this.slots[name].value = value; 39 | } 40 | 41 | return this; 42 | } 43 | 44 | public withSlotResolution(name: string, value: string, slotType: string, id: string): IntentRequestBuilder { 45 | this.withSlot(name, value); 46 | if (!this.slots) { 47 | throw new Error('Invalid Slots'); 48 | } 49 | 50 | const authority = `amzn1.er-authority.echo-sdk.${this.settings.appId}.${slotType}`; 51 | let valueAdded = false; 52 | if (this.slots[name].resolutions) { 53 | this.slots[name].resolutions?.resolutionsPerAuthority?.forEach((rpa) => { 54 | if (!valueAdded && (rpa.authority === authority)) { 55 | rpa.values.push({ value: { name: value, id } }); 56 | valueAdded = true; 57 | } 58 | }); 59 | } else { 60 | this.slots[name].resolutions = { 61 | resolutionsPerAuthority: [], 62 | }; 63 | } 64 | if (!valueAdded) { 65 | this.slots[name].resolutions?.resolutionsPerAuthority?.push({ 66 | authority, 67 | status: { code: 'ER_SUCCESS_MATCH' }, 68 | values: [{ value: { name: value, id } }], 69 | }); 70 | } 71 | return this; 72 | } 73 | 74 | public withSlotResolutionNoMatch(name: string, value: string, slotType: string): IntentRequestBuilder { 75 | this.withSlot(name, value); 76 | if (!this.slots) { 77 | throw new Error('Invalid Slots'); 78 | } 79 | 80 | if (!this.slots[name].resolutions) { 81 | this.slots[name].resolutions = { 82 | resolutionsPerAuthority: [], 83 | }; 84 | } 85 | this.slots[name].resolutions?.resolutionsPerAuthority?.push({ 86 | authority: `amzn1.er-authority.echo-sdk.${this.settings.appId}.${slotType}`, 87 | status: { code: 'ER_SUCCESS_NO_MATCH' }, 88 | values: [], 89 | }); 90 | return this; 91 | } 92 | 93 | public withIntentConfirmation(confirmationStatus: IntentConfirmationStatus): IntentRequestBuilder { 94 | this.confirmationStatus = confirmationStatus; 95 | return this; 96 | } 97 | 98 | protected buildRequest(): Request { 99 | return { 100 | type: 'IntentRequest', 101 | requestId: `EdwRequestId.${v4()}`, 102 | timestamp: new Date().toISOString(), 103 | locale: this.settings.locale, 104 | intent: { 105 | name: this.intentName, 106 | slots: this.slots, 107 | confirmationStatus: this.confirmationStatus, 108 | }, 109 | dialogState: 'STARTED', 110 | }; 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/factory/LaunchRequestBuilder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { Request } from 'ask-sdk-model'; 6 | import { v4 } from 'uuid'; 7 | import { RequestBuilder } from './RequestBuilder'; 8 | import { SkillSettings } from '../types'; 9 | 10 | export class LaunchRequestBuilder extends RequestBuilder { 11 | 12 | constructor(settings : SkillSettings) { 13 | super(settings); 14 | } 15 | 16 | protected buildRequest() : Request { 17 | return { 18 | type: 'LaunchRequest', 19 | requestId: `EdwRequestId.${v4()}`, 20 | timestamp: new Date().toISOString(), 21 | locale: this.settings.locale, 22 | }; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/factory/RequestBuilder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { Context, Request, RequestEnvelope, Session } from 'ask-sdk-model'; 6 | import { v4 } from 'uuid'; 7 | import { InterfaceSettings, SkillSettings } from '../types'; 8 | 9 | export abstract class RequestBuilder { 10 | 11 | protected settings: SkillSettings; 12 | 13 | constructor(settings: SkillSettings) { 14 | this.settings = JSON.parse(JSON.stringify(settings)); 15 | if (!this.settings.interfaces) { 16 | this.settings.interfaces = {}; 17 | } 18 | if (!this.settings.interfaces.hasOwnProperty('audio')) { 19 | this.settings.interfaces.audio = true; 20 | } 21 | } 22 | 23 | public build(): RequestEnvelope { 24 | const request: RequestEnvelope = { 25 | version: '1.0', 26 | session: this.getSessionData(), 27 | context: this.getContextData(), 28 | request: this.buildRequest(), 29 | }; 30 | this.modifyRequest(request); 31 | return request; 32 | } 33 | 34 | public withInterfaces(iface: InterfaceSettings): RequestBuilder { 35 | if (iface.hasOwnProperty('apl')) { 36 | this.settings.interfaces!.apl = iface.apl; 37 | } 38 | if (iface.hasOwnProperty('audio')) { 39 | this.settings.interfaces!.audio = iface.audio; 40 | } 41 | if (iface.hasOwnProperty('display')) { 42 | this.settings.interfaces!.display = iface.display; 43 | } 44 | if (iface.hasOwnProperty('video')) { 45 | this.settings.interfaces!.video = iface.video; 46 | } 47 | return this; 48 | } 49 | 50 | protected abstract buildRequest(): Request; 51 | 52 | protected modifyRequest(_request: RequestEnvelope): void { 53 | // override if needed 54 | } 55 | 56 | protected getSessionData(): Session { 57 | return { 58 | // randomized for every session and set before calling the handler 59 | sessionId: 'SessionId.00000000-0000-0000-0000-000000000000', 60 | application: { applicationId: this.settings.appId }, 61 | attributes: {}, 62 | user: { userId: this.settings.userId }, 63 | new: true, 64 | }; 65 | } 66 | 67 | protected getContextData(): Context { 68 | const ctx: Context = { 69 | System: { 70 | application: { applicationId: this.settings.appId }, 71 | user: { userId: this.settings.userId }, 72 | device: { 73 | deviceId: this.settings.deviceId, 74 | supportedInterfaces: {}, 75 | }, 76 | apiEndpoint: 'https://api.amazonalexa.com/', 77 | apiAccessToken: v4(), 78 | }, 79 | AudioPlayer: { 80 | playerActivity: 'IDLE', 81 | }, 82 | }; 83 | if (this.settings.interfaces!.audio) { 84 | ctx.System.device!.supportedInterfaces.AudioPlayer = {}; 85 | } 86 | if (this.settings.interfaces!.display) { 87 | ctx.System.device!.supportedInterfaces.Display = { templateVersion: '1.0', markupVersion: '1.0' }; 88 | } 89 | if (this.settings.interfaces!.video) { 90 | ctx.System.device!.supportedInterfaces.VideoApp = {}; 91 | ctx.Viewport = { video: { codecs: ['H_264_41', 'H_264_42'] } }; 92 | } 93 | if (this.settings.interfaces!.apl) { 94 | ctx.System.device!.supportedInterfaces['Alexa.Presentation.APL'] = { runtime: { maxVersion: '1.3' } }; 95 | } 96 | return ctx; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/factory/SessionEndedRequestBuilder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { Request, SessionEndedReason } from 'ask-sdk-model'; 6 | import { v4 } from 'uuid'; 7 | import { RequestBuilder } from './RequestBuilder'; 8 | import { SkillSettings } from '../types'; 9 | 10 | export class SessionEndedRequestBuilder extends RequestBuilder { 11 | 12 | private reason : SessionEndedReason; 13 | 14 | constructor(settings : SkillSettings, reason : SessionEndedReason) { 15 | super(settings); 16 | this.reason = reason; 17 | } 18 | 19 | protected buildRequest() : Request { 20 | /* 21 | { 22 | "version": this.version, 23 | "session": this._getSessionData(), 24 | "context": this._getContextData(), 25 | "request": { 26 | "type": "SessionEndedRequest", 27 | "requestId": "EdwRequestId." + uuid.v4(), 28 | "timestamp": new Date().toISOString(), 29 | "locale": locale || this.locale, 30 | "reason": reason 31 | //TODO: support error 32 | } 33 | } 34 | */ 35 | 36 | return { 37 | type: 'SessionEndedRequest', 38 | requestId: `EdwRequestId.${v4()}`, 39 | timestamp: new Date().toISOString(), 40 | locale: this.settings.locale, 41 | reason: this.reason, 42 | }; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | export { AlexaTest } from './tester/AlexaTest'; 6 | export { RequestBuilder } from './factory/RequestBuilder'; 7 | export { IntentRequestBuilder } from './factory/IntentRequestBuilder'; 8 | export { LaunchRequestBuilder } from './factory/LaunchRequestBuilder'; 9 | export { SessionEndedRequestBuilder } from './factory/SessionEndedRequestBuilder'; 10 | export { AudioPlayerPauseIntentRequestBuilder, AudioPlayerResumeIntentRequestBuilder } from './factory/AudioIntentRequestBuilder'; 11 | export { AplUserEventRequestBuilder } from './factory/AplUserEventRequestBuilder'; 12 | export { SkillSettings, SequenceItem, ResponseValidator } from './types'; 13 | -------------------------------------------------------------------------------- /src/tester/AlexaTest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { fail } from 'assert'; 6 | import { RequestEnvelope } from 'ask-sdk-model'; 7 | import { CloudFormation as CFNClient, Lambda as LambdaClient } from 'aws-sdk'; 8 | import { DocumentClient } from 'aws-sdk/lib/dynamodb/document_client'; 9 | import * as AWSMOCK from 'aws-sdk-mock'; 10 | import { expect } from 'chai'; 11 | import * as lambdaLocal from 'lambda-local'; 12 | import { it } from 'mocha'; 13 | import nock from 'nock'; 14 | import { v4 } from 'uuid'; 15 | import GetItemInput = DocumentClient.GetItemInput; 16 | import PutItemInput = DocumentClient.PutItemInput; 17 | import { AplValidator } from './AplValidator'; 18 | import { AudioPlayerValidator } from './AudioPlayerValidator'; 19 | import { CardValidator } from './CardValidator'; 20 | import { DialogValidator } from './DialogValidator'; 21 | import { EndSessionValidator } from './EndSessionValidator'; 22 | import { QuestionMarkValidator } from './QuestionMarkValidator'; 23 | import { SessionAttributeValidator } from './SessionAttributeValidator'; 24 | import { SpeechValidator } from './SpeechValidator'; 25 | import { VideoAppValidator } from './VideoAppValidator'; 26 | import { ResponseValidator, SequenceItem, SkillSettings } from '../types'; 27 | 28 | lambdaLocal.getLogger().level = 'error'; 29 | 30 | // Install mock for DynamoDB persistence 31 | const dynamoDBMock: { getMock: (params: GetItemInput, callback: Function) => void; putMock: (params: PutItemInput, callback: Function) => void } = { 32 | getMock: () => undefined, 33 | putMock: () => undefined, 34 | }; 35 | AWSMOCK.mock('DynamoDB.DocumentClient', 'get', (params: GetItemInput, callback: Function) => { 36 | // Do not inline; resolution has to take place on call 37 | dynamoDBMock.getMock(params, callback); 38 | }); 39 | AWSMOCK.mock('DynamoDB.DocumentClient', 'put', (params: PutItemInput, callback: Function) => { 40 | // Do not inline; resolution has to take place on call 41 | dynamoDBMock.putMock(params, callback); 42 | }); 43 | 44 | // Install UpsServiceClient mock 45 | // const upsServiceMock: { getProfileName: () => string | undefined; getProfileGivenName: () => string | undefined; getProfileEmail: () => string | undefined; getProfileMobileNumber: () => string | undefined; } = { 46 | // getProfileName: () => undefined, 47 | // getProfileGivenName: () => undefined, 48 | // getProfileEmail: () => undefined, 49 | // getProfileMobileNumber: () => undefined, 50 | // }; 51 | 52 | export class AlexaTest { 53 | 54 | private readonly handler: Function | string; 55 | private settings: SkillSettings; 56 | private validators: ResponseValidator[]; 57 | 58 | private dynamoDBTable: string | undefined; 59 | private partitionKeyName: string | undefined; 60 | private attributesName: string | undefined; 61 | 62 | constructor(handler: Function | string, settings: SkillSettings) { 63 | this.handler = handler; 64 | this.settings = settings; 65 | this.validators = [ 66 | new SpeechValidator(), 67 | new DialogValidator(), 68 | new SessionAttributeValidator(), 69 | new QuestionMarkValidator(), 70 | new EndSessionValidator(), 71 | new AplValidator(), 72 | new AudioPlayerValidator(), 73 | new VideoAppValidator(), 74 | new CardValidator(), 75 | ]; 76 | } 77 | 78 | public addValidator(validator: ResponseValidator): AlexaTest { 79 | this.validators.push(validator); 80 | return this; 81 | } 82 | 83 | /** 84 | * Activates mocking of DynamoDB backed attributes 85 | * @param {string} tableName name of the DynamoDB Table 86 | * @param {string} partitionKeyName the key to be used as id (default: id) 87 | * @param {string} attributesName the key to be used for the attributes (default: attributes) 88 | */ 89 | public withDynamoDBPersistence(tableName: string, partitionKeyName?: string, attributesName?: string): AlexaTest { 90 | 'use strict'; 91 | if (!tableName) { 92 | throw "'tableName' argument must be provided."; 93 | } 94 | this.dynamoDBTable = tableName; 95 | this.partitionKeyName = partitionKeyName || 'id'; 96 | this.attributesName = attributesName || 'attributes'; 97 | return this; 98 | } 99 | 100 | public test(sequence: SequenceItem[], testDescription?: string): void { 101 | if (!this.handler) { 102 | throw 'The module is not initialized.'; 103 | } 104 | if (!sequence) { 105 | throw "'sequence' argument must be provided."; 106 | } 107 | 108 | it(testDescription || 'returns the correct responses', (done) => { 109 | const testSettings: TestSettings = { 110 | appId: this.settings.appId, 111 | deviceId: this.settings.deviceId, 112 | userId: this.settings.userId, 113 | handler: this.handler, 114 | sessionId: `SessionId.${v4()}`, 115 | debug: this.settings.debug || false, 116 | }; 117 | this.runSingleTest(testSettings, sequence, 0, undefined, done); 118 | }); 119 | } 120 | 121 | private runSingleTest( 122 | settings: TestSettings, 123 | sequence: SequenceItem[], 124 | sequenceIndex: number, 125 | attributes: object | undefined, 126 | done: Function): void { 127 | 128 | if (sequenceIndex >= sequence.length) { 129 | // all requests were executed 130 | done(); 131 | } else { 132 | const currentItem = sequence[sequenceIndex]; 133 | 134 | const request: RequestEnvelope = currentItem.request; 135 | request.session!.new = (sequenceIndex === 0); 136 | request.session!.attributes = attributes ? JSON.parse(JSON.stringify(attributes)) : {}; 137 | request.session!.sessionId = settings.sessionId; 138 | 139 | // adds values from withSessionAttributes to the session 140 | if (currentItem.withSessionAttributes) { 141 | for (const newAttribute in currentItem.withSessionAttributes) { 142 | if (currentItem.withSessionAttributes.hasOwnProperty(newAttribute)) { 143 | request.session!.attributes![newAttribute] = currentItem.withSessionAttributes[newAttribute]; 144 | } 145 | } 146 | } 147 | 148 | if (currentItem.withUserAccessToken) { 149 | request.session!.user.accessToken = currentItem.withUserAccessToken; 150 | request.context.System.user.accessToken = currentItem.withUserAccessToken; 151 | } 152 | 153 | let invoker: any; 154 | if (typeof settings.handler === 'function') { 155 | invoker = this.invokeFunction(settings, currentItem, request); 156 | } else if (settings.handler.startsWith('lambda:')) { 157 | invoker = this.invokeLambda(settings.handler.substr(7), settings, currentItem, request); 158 | } else if (settings.handler.startsWith('cfn:')) { 159 | const cfnParts = settings.handler.split(':'); 160 | invoker = this.invokeLambdaCloudformation(cfnParts[1], cfnParts[2], settings, currentItem, request); 161 | } else { 162 | throw 'Invalid handler configuration'; 163 | } 164 | 165 | invoker.then((response: any) => { 166 | if (settings.debug) { 167 | console.log(JSON.stringify(response, null, 2)); 168 | } 169 | 170 | this.validators.forEach((validator) => { 171 | validator.validate(currentItem, response); 172 | }); 173 | 174 | if (currentItem.callback) { 175 | currentItem.callback(response); 176 | } 177 | this.runSingleTest(settings, sequence, sequenceIndex + 1, response.sessionAttributes, done); 178 | }).catch(done); 179 | } 180 | } 181 | 182 | private async invokeLambdaCloudformation( 183 | stackName: string, 184 | logicalName: string, 185 | settings: TestSettings, 186 | currentItem: SequenceItem, 187 | request: RequestEnvelope): Promise { 188 | 189 | const cfnRes = await new CFNClient().describeStackResource({ 190 | StackName: stackName, 191 | LogicalResourceId: logicalName, 192 | }).promise(); 193 | if (cfnRes.StackResourceDetail?.ResourceType !== 'AWS::Lambda::Function') { 194 | throw 'CloudFormation resource must be of type AWS::Lambda::Function'; 195 | } 196 | const lambdaName = cfnRes.StackResourceDetail.PhysicalResourceId!; 197 | return this.invokeLambda(lambdaName, settings, currentItem, request); 198 | } 199 | 200 | private async invokeLambda(name: string, _settings: TestSettings, currentItem: SequenceItem, request: RequestEnvelope): Promise { 201 | if (this.containsMockSettings(currentItem)) { 202 | throw 'Invalid test configuration found. Cannot mock DynamoDB or API calls when invoking remotely.'; 203 | } 204 | const res = await new LambdaClient().invoke({ 205 | FunctionName: name, 206 | InvocationType: 'RequestResponse', 207 | LogType: 'None', 208 | Payload: JSON.stringify(request), 209 | }).promise(); 210 | 211 | const parsedResponse = JSON.parse(res.Payload!.toString()); 212 | if (res.FunctionError) { 213 | console.log(JSON.stringify(parsedResponse, null, 2)); 214 | } 215 | return parsedResponse; 216 | } 217 | 218 | private containsMockSettings(currentItem: SequenceItem): boolean { 219 | return currentItem.hasOwnProperty('storesAttributes') || 220 | currentItem.hasOwnProperty('withProfile') || 221 | currentItem.hasOwnProperty('withStoredAttributes'); 222 | } 223 | 224 | private invokeFunction(settings: TestSettings, currentItem: SequenceItem, request: RequestEnvelope): Promise { 225 | this.mockDynamoDB(settings, currentItem); 226 | 227 | const interceptors = this.mockProfileAPI(currentItem); 228 | 229 | return lambdaLocal.execute({ 230 | event: request, 231 | lambdaFunc: settings, 232 | lambdaHandler: 'handler', 233 | timeoutMs: 5000, 234 | }).then((response) => { 235 | // if (response.toJSON) { 236 | // response = response.toJSON(); 237 | // } 238 | interceptors.forEach((value) => nock.removeInterceptor(value)); 239 | return response; 240 | }); 241 | } 242 | 243 | private mockDynamoDB(settings: TestSettings, currentItem: SequenceItem): void { 244 | if (this.dynamoDBTable) { 245 | dynamoDBMock.putMock = (params: PutItemInput, callback: Function) => { 246 | expect(params).to.have.property('TableName', this.dynamoDBTable); 247 | expect(params).to.haveOwnProperty('Item'); 248 | expect(params.Item).to.have.property(this.partitionKeyName!, settings.userId); 249 | const storesAttributes = currentItem.storesAttributes; 250 | if (storesAttributes) { 251 | for (const att in storesAttributes) { 252 | if (storesAttributes.hasOwnProperty(att)) { 253 | const storedAttr = params.Item[this.attributesName!][att]; 254 | const expectedAttr: any = storesAttributes[att]; 255 | if (typeof expectedAttr === 'function') { 256 | if (!expectedAttr(storedAttr)) { 257 | fail(`the stored attribute ${att} did not contain the correct value. Value was: ${storedAttr}`); 258 | } 259 | } else { 260 | expect(storedAttr).to.be.equal(expectedAttr); 261 | } 262 | } 263 | } 264 | } 265 | callback(null, {}); 266 | }; 267 | dynamoDBMock.getMock = (params: GetItemInput, callback) => { 268 | expect(params).to.have.property('TableName', this.dynamoDBTable); 269 | expect(params).to.haveOwnProperty('Key'); 270 | expect(params.Key).to.have.property(this.partitionKeyName!, settings.userId); 271 | if (!this.partitionKeyName || !this.attributesName) { 272 | fail(); 273 | return; 274 | } 275 | 276 | const Item: { [key: string]: any } = {}; 277 | Item[this.partitionKeyName] = settings.userId; 278 | Item[this.attributesName!] = currentItem.withStoredAttributes || {}; 279 | callback(null, { TableName: this.dynamoDBTable, Item }); 280 | }; 281 | } 282 | } 283 | 284 | private mockProfileAPI(currentItem: SequenceItem): object[] { 285 | const profileMock = nock('https://api.amazonalexa.com').persist(); 286 | const nameInterceptor = profileMock.get('/v2/accounts/~current/settings/Profile.name'); 287 | const givenNameInterceptor = profileMock.get('/v2/accounts/~current/settings/Profile.givenName'); 288 | const emailInterceptor = profileMock.get('/v2/accounts/~current/settings/Profile.email'); 289 | const mobileNumberInterceptor = profileMock.get('/v2/accounts/~current/settings/Profile.mobileNumber'); 290 | 291 | if (currentItem.withProfile && currentItem.withProfile.name) { 292 | nameInterceptor.reply(200, () => { 293 | return JSON.stringify(currentItem.withProfile!.name); 294 | }); 295 | } else { 296 | nameInterceptor.reply(401, {}); 297 | } 298 | if (currentItem.withProfile && currentItem.withProfile.givenName) { 299 | givenNameInterceptor.reply(200, () => { 300 | return JSON.stringify(currentItem.withProfile!.givenName); 301 | }); 302 | } else { 303 | givenNameInterceptor.reply(401, {}); 304 | } 305 | if (currentItem.withProfile && currentItem.withProfile.email) { 306 | emailInterceptor.reply(200, () => { 307 | return JSON.stringify(currentItem.withProfile!.email); 308 | }); 309 | } else { 310 | emailInterceptor.reply(401, {}); 311 | } 312 | if (currentItem.withProfile && currentItem.withProfile.mobileNumber) { 313 | mobileNumberInterceptor.reply(200, () => { 314 | return JSON.stringify(currentItem.withProfile!.mobileNumber); 315 | }); 316 | } else { 317 | mobileNumberInterceptor.reply(401, {}); 318 | } 319 | return [ 320 | nameInterceptor, givenNameInterceptor, emailInterceptor, mobileNumberInterceptor, 321 | ]; 322 | } 323 | 324 | } 325 | 326 | interface TestSettings { 327 | 328 | appId: string; 329 | userId: string; 330 | deviceId: string; 331 | handler: Function | string; 332 | sessionId: string; 333 | debug: boolean; 334 | 335 | } 336 | -------------------------------------------------------------------------------- /src/tester/AplValidator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { fail } from 'assert'; 6 | import { interfaces, ResponseEnvelope } from 'ask-sdk-model'; 7 | import { expect } from 'chai'; 8 | import { ResponseValidator, SequenceItem } from '../types'; 9 | 10 | export class AplValidator extends ResponseValidator { 11 | public validate(currentItem: SequenceItem, response: ResponseEnvelope): void { 12 | if (currentItem.renderDocument) { 13 | if (!response.response.directives) { 14 | fail('the response did not contain any directives'); 15 | } 16 | const renderedDocument = response.response.directives.find((value) => value.type === 'Alexa.Presentation.APL.RenderDocument' || value.type === 'Alexa.Presentation.APLA.RenderDocument'); 17 | if (!renderedDocument) { 18 | fail('the response did not contain a render directive'); 19 | } 20 | 21 | const renderConfig = currentItem.renderDocument; 22 | if (renderConfig.token) { 23 | expect(renderConfig.token).to.be.equal(renderConfig.token, 'token did not match'); 24 | } 25 | if (renderConfig.document) { 26 | if (!renderConfig.document(renderedDocument.document)) { 27 | fail(`document validation failed. Value was: ${JSON.stringify(renderedDocument.document)}`); 28 | } 29 | } 30 | if (renderConfig.hasDataSources) { 31 | for (const att in renderConfig.hasDataSources) { 32 | if (renderConfig.hasDataSources.hasOwnProperty(att)) { 33 | const func: any = renderConfig.hasDataSources[att]; 34 | const datasource = renderedDocument.datasources![att]; 35 | if (!func(datasource)) { 36 | fail(`The datasource ${att} did not contain the correct value. Value was: ${JSON.stringify(datasource)}`); 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/tester/AudioPlayerValidator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { fail } from 'assert'; 6 | import { interfaces, ResponseEnvelope } from 'ask-sdk-model'; 7 | import { expect } from 'chai'; 8 | import { ResponseValidator, SequenceItem } from '../types'; 9 | 10 | export class AudioPlayerValidator extends ResponseValidator { 11 | public validate(currentItem: SequenceItem, response: ResponseEnvelope): void { 12 | if (currentItem.playsStream) { 13 | if (!response.response.directives) { 14 | fail('the response did not contain any directives'); 15 | } 16 | const playDirective = response.response.directives.find((value) => value.type === 'AudioPlayer.Play'); 17 | if (!playDirective) { 18 | fail('the response did not play a stream'); 19 | } 20 | 21 | const playConfig = currentItem.playsStream; 22 | expect(playDirective.playBehavior).to.be.equal(playConfig.behavior, 'playBehavior did not match'); 23 | 24 | const stream = playDirective.audioItem?.stream; 25 | if (!stream || !stream.url.startsWith('https://')) { 26 | fail('the stream URL is not https'); 27 | } 28 | expect(stream.url).to.be.equal(playConfig.url, 'stream URL did not match'); 29 | 30 | if (playConfig.token) { 31 | expect(stream.token).to.be.equal(playConfig.token, 'token did not match'); 32 | } 33 | if (playConfig.previousToken) { 34 | expect(stream.expectedPreviousToken).to.be.equal(playConfig.previousToken, 'previous token did not match'); 35 | } 36 | if (playConfig.offset || playConfig.offset === 0) { 37 | expect(stream.offsetInMilliseconds).to.be.equal(playConfig.offset, 'stream offset did not match'); 38 | } 39 | } 40 | 41 | if (currentItem.stopsStream) { 42 | if (!response.response.directives || !response.response.directives.find((value) => value.type === 'AudioPlayer.Stop')) { 43 | fail('the response did not stop the stream'); 44 | } 45 | } 46 | 47 | if (currentItem.clearsQueue) { 48 | if (!response.response.directives) { 49 | fail('the response did not contain any directives'); 50 | } 51 | const clearDirective = response.response.directives.find((value) => value.type === 'AudioPlayer.ClearQueue'); 52 | if (!clearDirective) { 53 | fail('the response did not clear the audio queue'); 54 | } 55 | expect(clearDirective.clearBehavior).to.be.equal(currentItem.clearsQueue, 'clear queue behavior did not match'); 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/tester/CardValidator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { fail } from 'assert'; 6 | import { ResponseEnvelope } from 'ask-sdk-model'; 7 | import { expect } from 'chai'; 8 | import { ResponseValidator, SequenceItem } from '../types'; 9 | 10 | export class CardValidator extends ResponseValidator { 11 | 12 | public validate(currentItem: SequenceItem, response: ResponseEnvelope): void { 13 | /* 14 | 15 | hasCardTitle? : string; 16 | hasCardContent? : string; 17 | hasCardContentLike? : string; 18 | hasCardText? : string; 19 | hasCardTextLike? : string; 20 | hasSmallImageUrlLike? : string; 21 | hasLargeImageUrlLike? : string; 22 | */ 23 | 24 | if (currentItem.hasCardTitle) { 25 | if (!response.response.card || (response.response.card.type !== 'Simple' && response.response.card.type !== 'Standard')) { 26 | fail('the response did not contain a card'); 27 | } else { 28 | expect(response.response.card.title).to.be.equal(currentItem.hasCardTitle); 29 | } 30 | } 31 | 32 | if (currentItem.hasCardContent) { 33 | if (!response.response.card) { 34 | fail('the response did not contain a card'); 35 | } else if (response.response.card.type !== 'Simple') { 36 | fail('the card in the response was not a simple card'); 37 | } else { 38 | expect(response.response.card.content).to.be.equal(currentItem.hasCardContent); 39 | } 40 | } 41 | 42 | if (currentItem.hasCardContentLike) { 43 | if (!response.response.card) { 44 | fail('the response did not contain a card'); 45 | } else if (response.response.card.type !== 'Simple') { 46 | fail('the card in the response was not a simple card'); 47 | } else { 48 | expect(response.response.card.content!.indexOf(currentItem.hasCardContentLike) >= 0, 'Card content did not contain specified text').to.be.true; 49 | } 50 | } 51 | 52 | if (currentItem.hasCardText) { 53 | if (!response.response.card) { 54 | fail('the response did not contain a card'); 55 | } else if (response.response.card.type !== 'Standard') { 56 | fail('the card in the response was not a standard card'); 57 | } else { 58 | expect(response.response.card.text).to.be.equal(currentItem.hasCardText); 59 | } 60 | } 61 | 62 | if (currentItem.hasCardTextLike) { 63 | if (!response.response.card) { 64 | fail('the response did not contain a card'); 65 | } else if (response.response.card.type !== 'Standard') { 66 | fail('the card in the response was not a simple card'); 67 | } else { 68 | expect(response.response.card.text!.indexOf(currentItem.hasCardTextLike) >= 0, 'Card text did not contain specified text').to.be.true; 69 | } 70 | } 71 | 72 | if (currentItem.hasSmallImageUrlLike) { 73 | if (!response.response.card) { 74 | fail('the response did not contain a card'); 75 | } else if (response.response.card.type !== 'Standard') { 76 | fail('the card in the response was not a simple card'); 77 | } else if (!response.response.card.image) { 78 | fail('the card in the response did not contain an image'); 79 | } else { 80 | expect(response.response.card.image.smallImageUrl!.indexOf(currentItem.hasSmallImageUrlLike) >= 0, 'Card small image did not contain specified URL').to.be.true; 81 | } 82 | } 83 | 84 | if (currentItem.hasLargeImageUrlLike) { 85 | if (!response.response.card) { 86 | fail('the response did not contain a card'); 87 | } else if (response.response.card.type !== 'Standard') { 88 | fail('the card in the response was not a simple card'); 89 | } else if (!response.response.card.image) { 90 | fail('the card in the response did not contain an image'); 91 | } else { 92 | expect(response.response.card.image.largeImageUrl!.indexOf(currentItem.hasLargeImageUrlLike) >= 0, 'Card large image did not contain specified URL').to.be.true; 93 | } 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/tester/DialogValidator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { dialog, ResponseEnvelope } from 'ask-sdk-model'; 6 | import { expect } from 'chai'; 7 | import { ResponseValidator, SequenceItem } from '../types'; 8 | 9 | export class DialogValidator extends ResponseValidator { 10 | 11 | public validate(currentItem: SequenceItem, response: ResponseEnvelope): void { 12 | if (currentItem.elicitsSlot) { 13 | const elicitSlotDirective = response.response.directives 14 | ? response.response.directives.find((value) => value.type === 'Dialog.ElicitSlot') 15 | : undefined; 16 | const slot = elicitSlotDirective ? elicitSlotDirective.slotToElicit : ''; 17 | expect(slot).to.equal(currentItem.elicitsSlot, `The response did not ask Alexa to elicit the slot ${currentItem.elicitsSlot}`); 18 | } 19 | if (currentItem.elicitsForIntent) { 20 | const elicitSlotDirective = response.response.directives 21 | ? response.response.directives.find((value) => value.type === 'Dialog.ElicitSlot') 22 | : undefined; 23 | const intent = elicitSlotDirective ? elicitSlotDirective.updatedIntent!.name : ''; 24 | expect(intent).to.equal(currentItem.elicitsForIntent, `The response did not ask Alexa to elicit a slot for the intent ${currentItem.elicitsForIntent}`); 25 | } 26 | 27 | if (currentItem.confirmsSlot) { 28 | const confirmSlotDirective = response.response.directives 29 | ? response.response.directives.find((value) => value.type === 'Dialog.ConfirmSlot') 30 | : undefined; 31 | const slot = confirmSlotDirective ? confirmSlotDirective.slotToConfirm : ''; 32 | expect(slot).to.equal(currentItem.confirmsSlot, `The response did not ask Alexa to confirm the slot ${currentItem.confirmsSlot}`); 33 | } 34 | 35 | if (currentItem.confirmsIntent) { 36 | const confirmIntentDirective = response.response.directives 37 | ? response.response.directives.find((value) => value.type === 'Dialog.ConfirmIntent') 38 | : undefined; 39 | expect(confirmIntentDirective, 'The response did not ask Alexa to confirm the intent').not.to.be.undefined; 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/tester/EndSessionValidator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { fail } from 'assert'; 6 | import { ResponseEnvelope } from 'ask-sdk-model'; 7 | import { ResponseValidator, SequenceItem } from '../types'; 8 | 9 | export class EndSessionValidator extends ResponseValidator { 10 | public validate(currentItem : SequenceItem, response : ResponseEnvelope) : void { 11 | // check the shouldEndSession flag 12 | const actualShouldEndSession = response.response.shouldEndSession; 13 | if (currentItem.shouldEndSession === actualShouldEndSession) { 14 | return; 15 | } 16 | 17 | let failMessage = `shouldEndSession was in an unexpected state. Expected: ${currentItem.shouldEndSession}. Actual: ${actualShouldEndSession}`; 18 | if (currentItem.shouldEndSession === true && actualShouldEndSession !== true) { 19 | failMessage = 'the response did not end the session'; 20 | } else if (currentItem.shouldEndSession === false && actualShouldEndSession !== false) { 21 | failMessage = 'the response ended the session'; 22 | } else if (currentItem.shouldEndSession === undefined && actualShouldEndSession !== undefined) { 23 | failMessage = 'end of session was not left in an undefined state'; 24 | } 25 | fail(failMessage); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/tester/QuestionMarkValidator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { fail } from 'assert'; 6 | import { ResponseEnvelope } from 'ask-sdk-model'; 7 | import { ResponseValidator, SequenceItem } from '../types'; 8 | 9 | export class QuestionMarkValidator extends ResponseValidator { 10 | 11 | public validate(currentItem: SequenceItem, response: ResponseEnvelope): void { 12 | let actualSay: string; 13 | if (response.response && response.response.outputSpeech && response.response.outputSpeech.type === 'SSML') { 14 | actualSay = response.response.outputSpeech.ssml.substring(7); 15 | actualSay = actualSay.substring(0, actualSay.length - 8).trim(); 16 | } else if (response.response && response.response.outputSpeech && response.response.outputSpeech.type === 'PlainText') { 17 | actualSay = response.response.outputSpeech.text; 18 | } else { 19 | actualSay = ''; 20 | } 21 | 22 | let hasQuestionMark = false; 23 | for (let i = 0; actualSay && i < actualSay.length; i++) { 24 | const c = actualSay[i]; 25 | if (c === '?' || c === '\u055E' || c === '\u061F' || c === '\u2E2E' || c === '\uFF1F') { 26 | hasQuestionMark = true; 27 | break; 28 | } 29 | } 30 | if (!currentItem.ignoreQuestionCheck && response.response.shouldEndSession !== false && hasQuestionMark) { 31 | fail('Possible Certification Problem: The response ends the session but contains a question mark.'); 32 | } 33 | if (!currentItem.ignoreQuestionCheck && response.response.shouldEndSession === false && !hasQuestionMark) { 34 | fail('Possible Certification Problem: The response keeps the session open but does not contain a question mark.'); 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/tester/SessionAttributeValidator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { fail } from 'assert'; 6 | import { ResponseEnvelope } from 'ask-sdk-model'; 7 | import { expect } from 'chai'; 8 | import { ResponseValidator, SequenceItem } from '../types'; 9 | 10 | export class SessionAttributeValidator extends ResponseValidator { 11 | 12 | public validate(currentItem: SequenceItem, response: ResponseEnvelope): void { 13 | if (currentItem.hasAttributes) { 14 | for (const att in currentItem.hasAttributes) { 15 | if (currentItem.hasAttributes.hasOwnProperty(att)) { 16 | const attr = currentItem.hasAttributes[att]; 17 | if (typeof attr === 'function') { 18 | if (!attr(response.sessionAttributes![att])) { 19 | fail(`The attribute ${att} did not contain the correct value. Value was: ${response.sessionAttributes![att]}`); 20 | } 21 | } else { 22 | expect(response.sessionAttributes![att], `Session attribute ${att} failed`).to.deep.equal(attr); 23 | } 24 | } 25 | } 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/tester/SpeechValidator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { fail } from 'assert'; 6 | import { ResponseEnvelope } from 'ask-sdk-model'; 7 | import { expect } from 'chai'; 8 | import { ResponseValidator, SequenceItem } from '../types'; 9 | 10 | export class SpeechValidator extends ResponseValidator { 11 | 12 | public validate(currentItem: SequenceItem, response: ResponseEnvelope): void { 13 | let actualSay: string | undefined; 14 | if (response.response && response.response.outputSpeech && response.response.outputSpeech.type === 'SSML') { 15 | actualSay = response.response.outputSpeech.ssml.substring(7); 16 | actualSay = actualSay.substring(0, actualSay.length - 8).trim(); 17 | } else if (response.response && response.response.outputSpeech && response.response.outputSpeech.type === 'PlainText') { 18 | actualSay = response.response.outputSpeech.text; 19 | } 20 | if (currentItem.says !== undefined) { 21 | if (!actualSay) { 22 | fail(); 23 | } 24 | if (Array.isArray(currentItem.says)) { 25 | expect(actualSay).to.be.oneOf(currentItem.says); 26 | } else if (typeof currentItem.says === 'function') { 27 | if (!currentItem.says(actualSay)) { 28 | fail(`Speech validation failed. Value was: ${actualSay}`); 29 | } 30 | } else { 31 | expect(actualSay).to.equal(currentItem.says); 32 | } 33 | } 34 | if (currentItem.saysLike !== undefined) { 35 | expect(actualSay && actualSay.indexOf(currentItem.saysLike) >= 0, `Speech did not contain specified text. Expected ${actualSay} to be like ${currentItem.saysLike}`).to.be.true; 36 | } 37 | if (currentItem.saysNothing) { 38 | expect(actualSay, 'Should have said nothing').to.be.undefined; 39 | } 40 | 41 | let actualReprompt: string | undefined; 42 | if (response.response && response.response.reprompt && response.response.reprompt.outputSpeech && response.response.reprompt.outputSpeech.type === 'SSML') { 43 | actualReprompt = response.response.reprompt.outputSpeech.ssml.substring(7); 44 | actualReprompt = actualReprompt.substring(0, actualReprompt.length - 8).trim(); 45 | } else if (response.response && response.response.reprompt && response.response.reprompt.outputSpeech && response.response.reprompt.outputSpeech.type === 'PlainText') { 46 | actualReprompt = response.response.reprompt.outputSpeech.text; 47 | } 48 | if (currentItem.reprompts !== undefined) { 49 | if (!actualReprompt) { 50 | fail(); 51 | } 52 | if (Array.isArray(currentItem.reprompts)) { 53 | expect(actualReprompt).to.be.oneOf(currentItem.reprompts); 54 | } else if (typeof currentItem.reprompts === 'function') { 55 | if (!currentItem.reprompts(actualReprompt)) { 56 | fail(`Reprompt validation failed. Value was: ${actualReprompt}`); 57 | } 58 | } else { 59 | expect(actualReprompt).to.equal(currentItem.reprompts); 60 | } 61 | } 62 | if (currentItem.repromptsLike !== undefined) { 63 | expect(actualReprompt && actualReprompt.indexOf(currentItem.repromptsLike) >= 0, `Reprompt did not contain specified text. Expected ${actualReprompt} to be like ${currentItem.repromptsLike}`).to.be.true; 64 | } 65 | if (currentItem.repromptsNothing) { 66 | expect(actualReprompt, 'Should have reprompted nothing').to.be.undefined; 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/tester/VideoAppValidator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { fail } from 'assert'; 6 | import { interfaces, ResponseEnvelope } from 'ask-sdk-model'; 7 | import { expect } from 'chai'; 8 | import { ResponseValidator, SequenceItem } from '../types'; 9 | 10 | export class VideoAppValidator extends ResponseValidator { 11 | public validate(currentItem: SequenceItem, response: ResponseEnvelope): void { 12 | if (currentItem.playsVideo) { 13 | const directive = response.response.directives!.find((value) => value.type === 'VideoApp.Launch'); 14 | if (!directive) { 15 | fail('the response did not play a video'); 16 | } 17 | 18 | const playConfig = currentItem.playsVideo; 19 | 20 | const stream = directive.videoItem.source; 21 | if (!stream.startsWith('https://')) { 22 | fail('the stream URL is not https'); 23 | } 24 | expect(stream).to.be.equal(playConfig.source, 'video source did not match'); 25 | 26 | if (playConfig.titel) { 27 | expect(directive.videoItem.metadata!.title).to.be.equal(playConfig.titel, 'title did not match'); 28 | } 29 | if (playConfig.subtitle) { 30 | expect(directive.videoItem.metadata!.subtitle).to.be.equal(playConfig.subtitle, 'subtitle did not match'); 31 | } 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { RequestEnvelope, ResponseEnvelope } from 'ask-sdk-model'; 6 | 7 | export interface SkillSettings { 8 | /** The skill id */ 9 | appId : string; 10 | /** The user id to simulate */ 11 | userId : string; 12 | /** The device id to simulate */ 13 | deviceId : string; 14 | /** the locale to use when generating requests */ 15 | locale : string; 16 | /** the interfaces present for the test */ 17 | interfaces? : InterfaceSettings; 18 | /** true to print the response to the console */ 19 | debug? : boolean; 20 | } 21 | 22 | export interface InterfaceSettings { 23 | display? : boolean; 24 | audio? : boolean; 25 | video? : boolean; 26 | apl? : boolean; 27 | } 28 | 29 | export interface SequenceItem { 30 | /** The request to run. Generate these with one of the above `getFooRequest` methods. */ 31 | request : RequestEnvelope; 32 | 33 | /** Receives the response object from the request as a parameter. You can make custom checks against the response using any assertion library you like in here. */ 34 | callback? : (response : ResponseEnvelope) => void; 35 | /** Tests that the speech output from the request is the string specified. */ 36 | says? : string | string[] | ((speech : string) => boolean); 37 | /** Tests that the speech output from the request contains the string specified. */ 38 | saysLike? : string; 39 | /** If true, tests that the response has no speech output. */ 40 | saysNothing? : boolean; 41 | /** Tests that the reprompt output from the request is the string specified. */ 42 | reprompts? : string | string[] | ((speech : string) => boolean); 43 | /** Tests that the reprompt output from the request contains the string specified. */ 44 | repromptsLike? : string; 45 | /** If true, tests that the response has no reprompt output. */ 46 | repromptsNothing? : boolean; 47 | /** If true, tests that the response to the request ends or does not end the session. */ 48 | shouldEndSession? : boolean; 49 | /** If true, tests that the response speech contains a question mark when the session is kept open */ 50 | ignoreQuestionCheck? : boolean; 51 | 52 | /** Tests that the response asks Alexa to elicit the given slot. */ 53 | elicitsSlot? : string; 54 | /** Tests that the response asks Alexa to elicit the a slot of the given intent. */ 55 | elicitsForIntent? : string; 56 | /** Tests that the response asks Alexa to confirm the given slot. */ 57 | confirmsSlot? : string; 58 | /** Tests that the response asks Alexa to confirm the intent. */ 59 | confirmsIntent? : boolean; 60 | 61 | /** Tests that the response contains the given attributes and values. Values can be strings, numbers, booleans or functions testing the value. */ 62 | hasAttributes? : { [key : string] : (string | number | boolean | ((attribute : any) => boolean)) }; 63 | /** The session attributes to initialize the intent request with. */ 64 | withSessionAttributes? : { [key : string] : any }; 65 | /** Tests that the given attributes were stored in the DynamoDB. Values can be strings, numbers, booleans or functions testing the value. */ 66 | storesAttributes? : { [key : string] : (string | number | boolean | ((attribute : any) => boolean)) }; 67 | /** The attributes to initialize the handler with. Used with DynamoDB mock. */ 68 | withStoredAttributes? : { [key : string] : any }; 69 | 70 | /** Tests that the card sent by the response has the title specified. */ 71 | hasCardTitle? : string; 72 | /** Tests that the card sent by the response is a simple card and has the content specified. */ 73 | hasCardContent? : string; 74 | /** Tests that the card sent by the response is a simple card and contains the content specified. */ 75 | hasCardContentLike? : string; 76 | /** Tests that the card sent by the response is a standard card and has the text specified. */ 77 | hasCardText? : string; 78 | /** Tests that the card sent by the response is a standard card and contains the text specified. */ 79 | hasCardTextLike? : string; 80 | /** Tests that the card sent by the response is a standard card and has a small image URL containing the string specified. */ 81 | hasSmallImageUrlLike? : string; 82 | /** Tests that the card sent by the response is a standard card and has a large image URL containing the string specified. */ 83 | hasLargeImageUrlLike? : string; 84 | 85 | /** Tests that the AudioPlayer is used to play a stream. */ 86 | playsStream? : PlayStreamConfig; 87 | /** Tests that the AudioPlayer is stopped. */ 88 | stopsStream? : boolean; 89 | /** Tests that the AudioPlayer clears the queue with the given clear behavior. */ 90 | clearsQueue? : string; 91 | 92 | /** Tests that the VideoPlayer is used to play a stream. */ 93 | playsVideo? : PlayVideoConfig; 94 | /** Tests that the RenderDirective is used to . */ 95 | renderDocument? : RenderDocumentConfig; 96 | 97 | /** The profile information for API calls. Ups will be unauthorized when this is undefined */ 98 | withProfile? : ProfileInfo; 99 | /** The accessToken to provide for account linking */ 100 | withUserAccessToken? : string; 101 | 102 | /** Any additional fields for custom validators */ 103 | [key : string] : any; 104 | } 105 | 106 | export interface RenderDocumentConfig { 107 | token? : string; 108 | document? : (document : any) => boolean; 109 | hasDataSources? : { [key : string] : (datasource : any) => boolean }; 110 | } 111 | 112 | export interface PlayStreamConfig { 113 | behavior? : string; 114 | token? : string; 115 | previousToken? : string; 116 | url? : string; 117 | offset? : number; 118 | } 119 | 120 | export interface PlayVideoConfig { 121 | source? : string; 122 | titel? : string; 123 | subtitle? : string; 124 | } 125 | 126 | export interface ProfileInfo { 127 | name? : string; 128 | givenName? : string; 129 | email? : string; 130 | mobileNumber? : string; 131 | } 132 | 133 | export abstract class ResponseValidator { 134 | 135 | public abstract validate(currentItem : SequenceItem, response : ResponseEnvelope) : void; 136 | 137 | } 138 | -------------------------------------------------------------------------------- /test/interfaces.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Taimos GmbH http://www.taimos.de 3 | */ 4 | 5 | import { expect } from 'chai'; 6 | import { LaunchRequestBuilder, SkillSettings } from '../src'; 7 | 8 | // initialize the testing framework 9 | const skillSettings: SkillSettings = { 10 | appId: 'amzn1.ask.skill.00000000-0000-0000-0000-000000000000', 11 | userId: 'amzn1.ask.account.VOID', 12 | deviceId: 'amzn1.ask.device.VOID', 13 | locale: 'en-US', 14 | }; 15 | 16 | describe('Tests for request builder with interfaces', () => { 17 | 18 | it('should have AudioPlayer by default', () => { 19 | const request = new LaunchRequestBuilder(skillSettings).build(); 20 | expect(request.context.System.device!.supportedInterfaces).to.have.property('AudioPlayer'); 21 | }); 22 | 23 | it('should not have AudioPlayer when disabled', () => { 24 | const request = new LaunchRequestBuilder(skillSettings).withInterfaces({ audio: false }).build(); 25 | expect(request.context.System.device!.supportedInterfaces).to.not.have.property('AudioPlayer'); 26 | }); 27 | 28 | it('should have Display when activated', () => { 29 | const request = new LaunchRequestBuilder(skillSettings).withInterfaces({ display: true }).build(); 30 | expect(request.context.System.device!.supportedInterfaces).to.have.property('AudioPlayer'); 31 | expect(request.context.System.device!.supportedInterfaces).to.have.property('Display'); 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "inlineSourceMap": true, 9 | "inlineSources": true, 10 | "lib": [ 11 | "es2019" 12 | ], 13 | "module": "CommonJS", 14 | "noEmitOnError": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "strictNullChecks": true, 24 | "strictPropertyInitialization": true, 25 | "stripInternal": true, 26 | "target": "ES2019" 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "test/**/*.ts", 31 | ".projenrc.js" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "alwaysStrict": true, 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "inlineSourceMap": true, 11 | "inlineSources": true, 12 | "lib": [ 13 | "es2019" 14 | ], 15 | "module": "CommonJS", 16 | "noEmitOnError": false, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitAny": true, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "resolveJsonModule": true, 24 | "strict": true, 25 | "strictNullChecks": true, 26 | "strictPropertyInitialization": true, 27 | "stripInternal": true, 28 | "target": "ES2019" 29 | }, 30 | "include": [ 31 | "src/**/*.ts" 32 | ], 33 | "exclude": [] 34 | } 35 | --------------------------------------------------------------------------------