├── .npmignore ├── CONTRIBUTING.md ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .nycrc ├── tsup.config.ts ├── .eslintrc.json ├── tsconfig.json ├── package.json ├── src ├── types.ts └── index.ts ├── LICENSE ├── README.md └── test └── index.spec.ts /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | _**Please read** our_ [**contribution guide**](https://github.com/dwyl/contributing) (_thank you_!) 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "17:00" 8 | timezone: Europe/London -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Modules 2 | node_modules/ 3 | 4 | #C overage 5 | coverage/ 6 | 7 | # Build bundle 8 | dist/ 9 | 10 | # Misc 11 | .tap/ 12 | .DS_Store 13 | .nyc_output 14 | 15 | # Editor 16 | .vscode -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript", 3 | "all": true, 4 | 5 | "include": [ 6 | "src/**/*.ts" 7 | ], 8 | 9 | "check-coverage": true, 10 | "branches": 80, 11 | "lines": 100 12 | } -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | format: ['cjs', 'esm'], 5 | entry: ['src/index.ts'], 6 | dts: true, 7 | shims: true, 8 | skipNodeModulesBundle: true, 9 | clean: true, 10 | }) -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | node-version: [18.x, 20.x, 21.x] 15 | os: [ubuntu-latest, windows-latest, macos-latest] 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm i 22 | - run: npm run lint 23 | - run: npm run build 24 | - run: npm test 25 | - name: Upload coverage to Codecov 26 | uses: codecov/codecov-action@v4 27 | with: 28 | token: ${{ secrets.CODECOV_TOKEN }} 29 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "globals": { 7 | "Buffer": true, 8 | "escape": true 9 | }, 10 | "parser": "@typescript-eslint/parser", 11 | "plugins": [ 12 | "@typescript-eslint" 13 | ], 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "sourceType": "module" 17 | }, 18 | "rules": { 19 | "quotes": [2, "single"], 20 | "strict": 0, 21 | "curly": 0, 22 | "no-empty": 0, 23 | "no-multi-spaces": 2, 24 | "no-underscore-dangle": 0, 25 | "new-cap": 0, 26 | "dot-notation": 0, 27 | "no-use-before-define": 0, 28 | "keyword-spacing": [2, {"after": true, "before": true}], 29 | "no-trailing-spaces": 2, 30 | "space-unary-ops": [1, { "words": true, "nonwords": false }] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Check Matt Pocock's cheatsheet at https://www.totaltypescript.com/tsconfig-cheat-sheet */ 4 | /* Base Options: */ 5 | "esModuleInterop": true, 6 | "skipLibCheck": true, 7 | "target": "es2022", 8 | "allowJs": true, 9 | "resolveJsonModule": true, 10 | "moduleDetection": "force", 11 | "isolatedModules": true, 12 | 13 | /* Strictness */ 14 | "strict": true, 15 | "noUncheckedIndexedAccess": true, 16 | 17 | /* If transpiling with TypeScript: */ 18 | "moduleResolution": "NodeNext", 19 | "module": "NodeNext", 20 | "outDir": "dist", 21 | "sourceMap": true, 22 | 23 | /* If your code doesn't run in the DOM: */ 24 | "lib": ["es2022"], 25 | 26 | /* If you're building for a library: */ 27 | "declaration": true, 28 | 29 | /* Misc */ 30 | "allowSyntheticDefaultImports": true, 31 | "noEmit": true, 32 | "allowImportingTsExtensions": true 33 | }, 34 | "exclude": [ 35 | "babel.config.cjs", 36 | "node_modules/**/*", 37 | "dist/**/*", 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-sdk-mock", 3 | "version": "6.2.1", 4 | "description": "Functions to mock the JavaScript aws-sdk", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "files": [ 9 | "dist", 10 | "README.md", 11 | "LICENSE" 12 | ], 13 | "scripts": { 14 | "lint": "tsc", 15 | "nocov": "mocha --require ts-node/register test/**/*.spec.ts", 16 | "test": "nyc mocha --require ts-node/register test/**/*.spec.ts && tsd", 17 | "coverage": "nyc --report html mocha --require ts-node/register test/**/*.spec.ts && open coverage/index.html", 18 | "prepublishOnly": "npm run build", 19 | "build": "tsup" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/dwyl/aws-sdk-mock.git" 24 | }, 25 | "keywords": [ 26 | "aws-sdk", 27 | "aws", 28 | "Amazon", 29 | "Lambda", 30 | "API-Gateway", 31 | "S3", 32 | "DynamoDB", 33 | "SNS", 34 | "test", 35 | "mock", 36 | "Node.js" 37 | ], 38 | "author": "Nikhila Ravi, Jimmy Ruts & Friends!", 39 | "license": "Apache-2.0", 40 | "bugs": { 41 | "url": "https://github.com/dwyl/aws-sdk-mock/issues" 42 | }, 43 | "homepage": "https://github.com/dwyl/aws-sdk-mock#readme", 44 | "dependencies": { 45 | "aws-sdk": "^2.1231.0", 46 | "neotraverse": "^0.6.15", 47 | "sinon": "^21.0.0" 48 | }, 49 | "tsd": { 50 | "directory": "test" 51 | }, 52 | "mocha": { 53 | "require": "expectations" 54 | }, 55 | "devDependencies": { 56 | "@istanbuljs/nyc-config-typescript": "^1.0.2", 57 | "@types/concat-stream": "^2.0.1", 58 | "@types/expectations": "^0.2.32", 59 | "@types/mocha": "^10.0.6", 60 | "@types/node": "^25.0.3", 61 | "@types/sinon": "^21.0.0", 62 | "c8": "^10.1.3", 63 | "concat-stream": "^2.0.0", 64 | "eslint": "^9.17.0", 65 | "expectations": "^1.0.0", 66 | "is-node-stream": "^1.0.0", 67 | "mocha": "^11.1.0", 68 | "nyc": "^17.0.0", 69 | "ts-node": "^10.9.1", 70 | "tsd": "^0.33.0", 71 | "tsup": "^8.0.2", 72 | "typescript": "^5.2.2" 73 | }, 74 | "engines": { 75 | "node": ">=18.0.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { type Request, type AWSError } from 'aws-sdk/lib/core.js'; 2 | import AWS from 'aws-sdk/clients/all'; 3 | import {type SinonStub } from 'sinon'; 4 | 5 | /* 6 | * Don't make this a `d.ts` file - see https://www.youtube.com/watch?v=zu-EgnbmcLY&ab_channel=MattPocock 7 | * or https://github.com/microsoft/TypeScript/issues/52593#issuecomment-1419505081 8 | */ 9 | 10 | // Utility types 11 | export type ValueType = T[K]; 12 | 13 | // AWS clients 14 | export type ClientName = keyof typeof AWS; 15 | export type Client = InstanceType<(typeof AWS)[C]>; 16 | 17 | // Extract method utility type to get method from client 18 | export type ExtractMethod = { [K in keyof T]: T[K] extends (...args: any[]) => any ? T[K] : never }; 19 | 20 | // AWS Method types 21 | export type MethodName = keyof ExtractMethod>; 22 | export type Method> = ExtractMethod>[M]; 23 | 24 | // AWS request type 25 | export type AWSRequest> = Method extends AWSMethod ? P : never; 26 | 27 | // AWS callback type 28 | export type AWSCallback> = Method extends AWSMethod 29 | ? { 30 | (err: undefined, data: D): void; 31 | (err: AWSError, data?: undefined): void; 32 | } 33 | : any; 34 | 35 | // Replace function in mock/remock/restore functions. Can be a function, string or object 36 | export type ReplaceFn> = 37 | | ((params: AWSRequest, options: any, callback: AWSCallback) => any) 38 | | string 39 | | object; 40 | 41 | // Interface from AWS method type 42 | export type Callback = (err: AWSError | undefined, data: D) => void; 43 | export interface AWSMethod { 44 | (params: P, callback?: Callback): Request; 45 | (callback?: Callback): Request; 46 | } 47 | 48 | // Stub type that can possibly be used while stubbing the given replace function 49 | export type MaybeSoninProxy = { 50 | _isMockFunction: boolean; 51 | isSinonProxy: boolean; 52 | }; 53 | 54 | export type MethodMock = { 55 | [key: string]: Replace>; 56 | }; 57 | 58 | export type Replace> = { 59 | replace: ReplaceFn; 60 | stub?: SinonStub & Partial; 61 | }; 62 | 63 | // AWS service object type 64 | export interface Service { 65 | Constructor: new (...args: any[]) => any; 66 | methodMocks: MethodMock; 67 | invoked: boolean; 68 | clients?: Client[]; 69 | stub?: SinonStub; 70 | } 71 | 72 | // AWS client type with extended options (to cover edge cases) 73 | export type ExtendedClient = Client & { 74 | options: { 75 | attrValue: ClientName; 76 | paramValidation: boolean; 77 | }; 78 | api: { 79 | operations: any; 80 | }; 81 | }; 82 | 83 | // All possible services from `aws-sdk` 84 | export type SERVICES = { 85 | [key in T]: Service; 86 | }; 87 | 88 | // Nested clients and nested methods types 89 | 90 | /** 91 | * You can find in the commented block below the first implementation of nested types. 92 | * They *don't work* because it's impossible. The `aws-sdk` library exports clients and each "method" is a type, not a value. 93 | * Therefore, it's impossible to get these as string literals. 94 | * See our question on Stack Overflow answered in https://stackoverflow.com/questions/77413867/how-to-create-a-type-that-yields-a-dot-notation-of-nested-class-properties-as-st. 95 | * 96 | * Because we still want the Typescript compiler to show the original clients as suggestions 97 | * when using an IDE, we're using `OtherString` as a branded primitive type. 98 | * It allows us the compiler to not reduce the `ClientName` string literals to `string` 99 | * but still accept any string, all the while suggesting the string literals from `ClientName`. 100 | * 101 | * For more detailed explanation on this, check https://stackoverflow.com/questions/67757457/make-typescript-show-type-hint-for-string-literal-when-union-with-string-primiti. 102 | * For a simple explanation, check https://github.com/microsoft/TypeScript/issues/29729#issuecomment-567871939. 103 | */ 104 | 105 | type OtherString = string & {}; 106 | export type NestedClientName = ClientName | OtherString; 107 | export type NestedMethodName = OtherString; 108 | 109 | /** 110 | * This is the first implementation attempt. 111 | * 112 | * The SDK defines a class for each service as well as a namespace with the same name. 113 | * Nested clients, e.g. DynamoDB.DocumentClient, are defined on the namespace, not the class. 114 | * That is why we need to fetch these separately as defined below in the NestedClientName type 115 | * 116 | * The NestedClientFullName type supports validating strings representing a nested clients name in dot notation 117 | * 118 | * We add the ts-ignore comments to avoid the type system to trip over the many possible values for NestedClientName 119 | */ 120 | //export type NestedClientName = keyof typeof AWS[C]; 121 | //// @ts-ignore 122 | //export type NestedClientFullName> = `${C}.${NC}`; 123 | //// @ts-ignore 124 | //export type NestedClient> = InstanceType<(typeof AWS)[C][NC]>; 125 | //// @ts-ignore 126 | //export type NestedMethodName> = keyof ExtractMethod>; 127 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2012-2018 dwyl.com 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # aws-sdk-mock 4 | 5 | AWSome mocks for `Javascript` `aws-sdk` services. 6 | 7 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/dwyl/aws-sdk-mock/ci.yml?label=build&style=flat-square&branch=main)](https://github.com/dwyl/aws-sdk-mock/actions/workflows/ci.yml) 8 | [![codecov.io](https://img.shields.io/codecov/c/github/dwyl/aws-sdk-mock/main.svg?style=flat-square)](http://codecov.io/github/dwyl/aws-sdk-mock?branch=main) 9 | [![Known Vulnerabilities](https://snyk.io/test/github/dwyl/aws-sdk-mock/badge.svg?targetFile=package.json&style=flat-square)](https://snyk.io/test/github/dwyl/aws-sdk-mock?targetFile=package.json) 10 | [![npm package version](https://img.shields.io/npm/v/aws-sdk-mock.svg?style=flat-square&color=bright-green)](https://www.npmjs.com/package/aws-sdk-mock) 11 | [![Node.js Version](https://img.shields.io/node/v/aws-sdk-mock.svg?style=flat-square "Node.js 18.x, 20.x & 21.x supported")](http://nodejs.org/download/) 12 | ![npm monthly downloads](https://img.shields.io/npm/dm/aws-sdk-mock?style=flat-square) 13 | [![HitCount](https://hits.dwyl.com/dwyl/aws-sdk-mock.svg?style=flat-square)](http://hits.dwyl.com/dwyl/aws-sdk-mock) 14 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/aws-sdk-mock/issues) 15 | 16 |
17 | 18 | 19 | This module was created to help test AWS Lambda functions but can be used in any situation where the AWS SDK needs to be mocked. 20 | 21 | This library is best suited for `AWS SDK for Javascript (v2)` - see the [introductory post on the AWS blog](https://aws.amazon.com/blogs/developer/mocking-modular-aws-sdk-for-javascript-v3-in-unit-tests/) for more context. 22 | If you are using `AWS SDK v3` you might not _need_ this library, see: 23 | [aws-sdk-mock/issues#209](https://github.com/dwyl/aws-sdk-mock/issues/209#issuecomment-764841699) 24 | 25 | 26 | If you are *new* to Amazon WebServices Lambda 27 | (*or need a refresher*), 28 | please checkout our our 29 | ***Beginners Guide to AWS Lambda***: 30 | 31 | 32 | 33 | * [Why](#why) 34 | * [What](#what) 35 | * [Getting Started](#how) 36 | * [Documentation](#documentation) 37 | * [Background Reading](#background-reading) 38 | 39 | ## Why? 40 | 41 | Testing your code is *essential* everywhere you need *reliability*. 42 | 43 | Using stubs means you can prevent a specific method from being called directly. In our case we want to prevent the actual AWS services to be called while testing functions that use the AWS SDK. 44 | 45 | ## What? 46 | 47 | Uses [Sinon.js](https://sinonjs.org/) under the hood to mock the AWS SDK services and their associated methods. 48 | 49 | ## Documentation 50 | 51 | ### `AWS.mock(service, method, replace)` 52 | 53 | Replaces a method on an AWS service with a replacement function or string. 54 | 55 | | Param | Type | Optional/Required | Description | 56 | | :------------- | :------------- | :------------- | :------------- | 57 | | `service` | string | Required | AWS service to mock e.g. SNS, DynamoDB, S3 | 58 | | `method` | string | Required | method on AWS service to mock e.g. 'publish' (for SNS), 'putItem' for 'DynamoDB' | 59 | | `replace` | string or function | Required | A string or function to replace the method | 60 | 61 | ### `AWS.restore(service, method)` 62 | 63 | Removes the mock to restore the specified AWS service 64 | 65 | | Param | Type | Optional/Required | Description | 66 | | :------------- | :------------- | :------------- | :------------- | 67 | | `service` | string | Optional | AWS service to restore - If only the service is specified, all the methods are restored | 68 | | `method` | string | Optional | Method on AWS service to restore | 69 | 70 | If `AWS.restore` is called without arguments (`AWS.restore()`) then all the services and their associated methods are restored 71 | i.e. equivalent to a 'restore all' function. 72 | 73 | ### `AWS.remock(service, method, replace)` 74 | 75 | Updates the `replace` method on an existing mocked service. 76 | 77 | | Param | Type | Optional/Required | Description | 78 | | :------------- | :------------- | :------------- | :------------- | 79 | | `service` | string | Required | AWS service to mock e.g. SNS, DynamoDB, S3 | 80 | | `method` | string | Required | method on AWS service to mock e.g. 'publish' (for SNS), 'putItem' for 'DynamoDB' | 81 | | `replace` | string or function | Required | A string or function to replace the method | 82 | 83 | ### `AWS.setSDK(path)` 84 | 85 | Explicitly set the require path for the `aws-sdk` 86 | 87 | | Param | Type | Optional/Required | Description | 88 | | :------------- | :------------- | :------------- | :------------- | 89 | | `path` | string | Required | Path to a nested AWS SDK node module | 90 | 91 | ### `AWS.setSDKInstance(sdk)` 92 | 93 | Explicitly set the `aws-sdk` instance to use 94 | 95 | | Param | Type | Optional/Required | Description | 96 | | :------------- | :------------- | :------------- | :------------- | 97 | | `sdk` | object | Required | The AWS SDK object | 98 | 99 | 100 | ## *How*? (*Usage*) 101 | 102 | ### *install* `aws-sdk-mock` from NPM 103 | 104 | ```sh 105 | npm install aws-sdk-mock --save-dev 106 | ``` 107 | 108 | ### Use in your Tests 109 | 110 | #### Using plain JavaScript 111 | 112 | ```js 113 | 114 | const AWS = require('aws-sdk-mock'); 115 | 116 | AWS.mock('DynamoDB', 'putItem', function (params, callback){ 117 | callback(null, 'successfully put item in database'); 118 | }); 119 | 120 | AWS.mock('SNS', 'publish', 'test-message'); 121 | 122 | // S3 getObject mock - return a Buffer object with file data 123 | AWS.mock('S3', 'getObject', Buffer.from(require('fs').readFileSync('testFile.csv'))); 124 | 125 | 126 | /** 127 | TESTS 128 | **/ 129 | 130 | AWS.restore('SNS', 'publish'); 131 | AWS.restore('DynamoDB'); 132 | AWS.restore('S3'); 133 | // or AWS.restore(); this will restore all the methods and services 134 | ``` 135 | 136 | #### Using TypeScript 137 | 138 | ```typescript 139 | import AWSMock from 'aws-sdk-mock'; 140 | import AWS from 'aws-sdk'; 141 | import { GetItemInput } from 'aws-sdk/clients/dynamodb'; 142 | 143 | beforeAll(async (done) => { 144 | //get requires env vars 145 | done(); 146 | }); 147 | 148 | describe('the module', () => { 149 | 150 | /** 151 | TESTS below here 152 | **/ 153 | 154 | it('should mock getItem from DynamoDB', async () => { 155 | // Overwriting DynamoDB.getItem() 156 | AWSMock.setSDKInstance(AWS); 157 | AWSMock.mock('DynamoDB', 'getItem', (params: GetItemInput, callback: Function) => { 158 | console.log('DynamoDB', 'getItem', 'mock called'); 159 | callback(null, {pk: 'foo', sk: 'bar'}); 160 | }) 161 | 162 | const input:GetItemInput = { TableName: '', Key: {} }; 163 | const dynamodb = new AWS.DynamoDB({apiVersion: '2012-08-10'}); 164 | expect(await dynamodb.getItem(input).promise()).toStrictEqual({ pk: 'foo', sk: 'bar' }); 165 | 166 | AWSMock.restore('DynamoDB'); 167 | }); 168 | 169 | it('should mock reading from DocumentClient', async () => { 170 | // Overwriting DynamoDB.DocumentClient.get() 171 | AWSMock.setSDKInstance(AWS); 172 | AWSMock.mock('DynamoDB.DocumentClient', 'get', (params: GetItemInput, callback: Function) => { 173 | console.log('DynamoDB.DocumentClient', 'get', 'mock called'); 174 | callback(null, {pk: 'foo', sk: 'bar'}); 175 | }); 176 | 177 | const input:GetItemInput = { TableName: '', Key: {} }; 178 | const client = new AWS.DynamoDB.DocumentClient({apiVersion: '2012-08-10'}); 179 | expect(await client.get(input).promise()).toStrictEqual({ pk: 'foo', sk: 'bar' }); 180 | 181 | AWSMock.restore('DynamoDB.DocumentClient'); 182 | }); 183 | }); 184 | ``` 185 | 186 | #### Sinon 187 | 188 | You can also pass Sinon spies to the mock: 189 | 190 | ```js 191 | const updateTableSpy = sinon.spy(); 192 | AWS.mock('DynamoDB', 'updateTable', updateTableSpy); 193 | 194 | // Object under test 195 | myDynamoManager.scaleDownTable(); 196 | 197 | // Assert on your Sinon spy as normal 198 | assert.isTrue(updateTableSpy.calledOnce, 'should update dynamo table via AWS SDK'); 199 | const expectedParams = { 200 | TableName: 'testTableName', 201 | ProvisionedThroughput: { 202 | ReadCapacityUnits: 1, 203 | WriteCapacityUnits: 1 204 | } 205 | }; 206 | assert.isTrue(updateTableSpy.calledWith(expectedParams), 'should pass correct parameters'); 207 | ``` 208 | 209 | **NB: The AWS Service needs to be initialised inside the function being tested in order for the SDK method to be mocked** e.g for an AWS Lambda function example 1 will cause an error `ConfigError: Missing region in config` whereas in example 2 the sdk will be successfully mocked. 210 | 211 | Example 1: 212 | 213 | ```js 214 | const AWS = require('aws-sdk'); 215 | const sns = AWS.SNS(); 216 | const dynamoDb = AWS.DynamoDB(); 217 | 218 | exports.handler = function(event, context) { 219 | // do something with the services e.g. sns.publish 220 | } 221 | ``` 222 | 223 | Example 2: 224 | 225 | ```js 226 | const AWS = require('aws-sdk'); 227 | 228 | exports.handler = function(event, context) { 229 | const sns = AWS.SNS(); 230 | const dynamoDb = AWS.DynamoDB(); 231 | // do something with the services e.g. sns.publish 232 | } 233 | ``` 234 | 235 | Also note that if you initialise an AWS service inside a callback from an async function inside the handler function, that won't work either. 236 | 237 | Example 1 (won't work): 238 | 239 | ```js 240 | exports.handler = function(event, context) { 241 | someAsyncFunction(() => { 242 | const sns = AWS.SNS(); 243 | const dynamoDb = AWS.DynamoDB(); 244 | // do something with the services e.g. sns.publish 245 | }); 246 | } 247 | ``` 248 | 249 | Example 2 (will work): 250 | 251 | ```js 252 | exports.handler = function(event, context) { 253 | const sns = AWS.SNS(); 254 | const dynamoDb = AWS.DynamoDB(); 255 | someAsyncFunction(() => { 256 | // do something with the services e.g. sns.publish 257 | }); 258 | } 259 | ``` 260 | 261 | ### Nested services 262 | 263 | It is possible to mock nested services like `DynamoDB.DocumentClient`. Simply use this dot-notation name as the `service` parameter to the `mock()` and `restore()` methods: 264 | 265 | ```js 266 | AWS.mock('DynamoDB.DocumentClient', 'get', function(params, callback) { 267 | callback(null, {Item: {Key: 'Value'}}); 268 | }); 269 | ``` 270 | 271 | **NB: Use caution when mocking both a nested service and its parent service.** The nested service should be mocked before and restored after its parent: 272 | 273 | ```js 274 | // OK 275 | AWS.mock('DynamoDB.DocumentClient', 'get', 'message'); 276 | AWS.mock('DynamoDB', 'describeTable', 'message'); 277 | AWS.restore('DynamoDB'); 278 | AWS.restore('DynamoDB.DocumentClient'); 279 | 280 | // Not OK 281 | AWS.mock('DynamoDB', 'describeTable', 'message'); 282 | AWS.mock('DynamoDB.DocumentClient', 'get', 'message'); 283 | 284 | // Not OK 285 | AWS.restore('DynamoDB.DocumentClient'); 286 | AWS.restore('DynamoDB'); 287 | ``` 288 | 289 | ### Don't worry about the constructor configuration 290 | Some constructors of the aws-sdk will require you to pass through a configuration object. 291 | 292 | ```js 293 | const csd = new AWS.CloudSearchDomain({ 294 | endpoint: 'your.end.point', 295 | region: 'eu-west' 296 | }); 297 | ``` 298 | 299 | Most mocking solutions with throw an `InvalidEndpoint: AWS.CloudSearchDomain requires an explicit 'endpoint' configuration option` when you try to mock this. 300 | 301 | **aws-sdk-mock** will take care of this during mock creation so you **won't get any configuration errors**!
302 | If configurations errors still occur it means you passed wrong configuration in your implementation. 303 | 304 | ### Setting the `aws-sdk` module explicitly 305 | 306 | Project structures that don't include the `aws-sdk` at the top level `node_modules` project folder will not be properly mocked. An example of this would be installing the `aws-sdk` in a nested project directory. You can get around this by explicitly setting the path to a nested `aws-sdk` module using `setSDK()`. 307 | 308 | Example: 309 | 310 | ```js 311 | const path = require('path'); 312 | const AWS = require('aws-sdk-mock'); 313 | 314 | AWS.setSDK(path.resolve('../../functions/foo/node_modules/aws-sdk')); 315 | 316 | /** 317 | TESTS 318 | **/ 319 | ``` 320 | 321 | ### Setting the `aws-sdk` object explicitly 322 | 323 | Due to transpiling, code written in TypeScript or ES6 may not correctly mock because the `aws-sdk` object created within `aws-sdk-mock` will not be equal to the object created within the code to test. In addition, it is sometimes convenient to have multiple SDK instances in a test. For either scenario, it is possible to pass in the SDK object directly using `setSDKInstance()`. 324 | 325 | Example: 326 | 327 | ```js 328 | // test code 329 | const AWSMock = require('aws-sdk-mock'); 330 | import AWS from 'aws-sdk'; 331 | AWSMock.setSDKInstance(AWS); 332 | AWSMock.mock('SQS', /* ... */); 333 | 334 | // implementation code 335 | const sqs = new AWS.SQS(); 336 | ``` 337 | 338 | ### Configuring promises 339 | 340 | If your environment lacks a global Promise constructor (e.g. nodejs 0.10), you can explicitly set the promises on `aws-sdk-mock`. Set the value of `AWS.Promise` to the constructor for your chosen promise library. 341 | 342 | Example (if Q is your promise library of choice): 343 | 344 | ```js 345 | const AWS = require('aws-sdk-mock'), 346 | Q = require('q'); 347 | 348 | AWS.Promise = Q.Promise; 349 | 350 | 351 | /** 352 | TESTS 353 | **/ 354 | ``` 355 | 356 | ## Background Reading 357 | 358 | * [Mocking using Sinon.js](http://sinonjs.org/docs/) 359 | * [AWS Lambda](https://github.com/dwyl/learn-aws-lambda) 360 | 361 | **Contributions welcome! Please submit issues or PRs if you think of anything that needs updating/improving** 362 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 'use strict'; 3 | 4 | /** 5 | * Helpers to mock the AWS SDK Services using sinon.js under the hood 6 | * Mocking is done in two steps: 7 | * - mock of the constructor for the service on AWS. 8 | * - mock of the method on the service. 9 | **/ 10 | 11 | import type { SinonExpectation, SinonSpy, SinonStubbedInstance } from 'sinon'; 12 | import sinon from 'sinon'; 13 | import traverse from 'neotraverse/legacy'; 14 | import { Readable } from 'stream'; 15 | 16 | import AWS_SDK from 'aws-sdk'; 17 | 18 | import { 19 | type ReplaceFn, 20 | type ClientName, 21 | type MethodName, 22 | type NestedClientName, 23 | type NestedMethodName, 24 | type Client, 25 | type AWSCallback, 26 | type AWSRequest, 27 | type SERVICES, 28 | type Service, 29 | type ExtendedClient, 30 | type MaybeSoninProxy, 31 | type Replace, 32 | type ValueType, 33 | type MethodMock, 34 | } from './types.js'; 35 | 36 | // TYPES ----------------------------------- 37 | // AWS type to be exported 38 | type AWS_MOCK = { 39 | mock: typeof mock; 40 | remock: typeof remock; 41 | restore: typeof restore; 42 | setSDK: typeof setSDK; 43 | setSDKInstance: typeof setSDKInstance; 44 | Promise: Awaited>; 45 | }; 46 | 47 | // PACKAGE --------------------------------- 48 | // Real AWS instance from 'aws-sdk' 49 | let _AWS: typeof AWS_SDK = AWS_SDK; 50 | 51 | // AWS that is exported to the client 52 | const AWS: AWS_MOCK = { 53 | Promise: global.Promise, 54 | mock: mock, 55 | remock: remock, 56 | restore: restore, 57 | setSDK: setSDK, 58 | setSDKInstance: setSDKInstance, 59 | }; 60 | const services: Partial> = {}; 61 | 62 | /** 63 | * Explicitly sets the `aws-sdk` to be mocked. 64 | * @param path path for the `aws-sdk`. 65 | */ 66 | async function setSDK(path: string): Promise { 67 | _AWS = require(path); 68 | } 69 | 70 | /** 71 | * Explicitly sets the `aws-sdk` instance to be used. 72 | * @param sdk the `aws-sdk` instance. 73 | */ 74 | function setSDKInstance(sdk: typeof AWS_SDK): void { 75 | _AWS = sdk; 76 | } 77 | 78 | /** 79 | * Stubs the service and registers the method that needs to be mocked. 80 | * 81 | * @param service AWS service to mock (e.g. DynamoDB). 82 | * @param method method on AWS service to mock (e.g. `putItem` for DynamoDB). 83 | * @param replace string or function to replace the method. 84 | */ 85 | function mock( 86 | service: NestedClientName, 87 | method: NestedMethodName, 88 | replace: ReplaceFn> 89 | ): Replace>; 90 | function mock & string>( 91 | service: C, 92 | method: M, 93 | replace: ReplaceFn> 94 | ): Replace> { 95 | // If the service does not exist yet, we need to create and stub it. 96 | if (!services[service]) { 97 | const service_to_add: Service = { 98 | // Save the real constructor so we can invoke it later on. 99 | // Uses traverse for easy access to nested services (dot-separated) 100 | Constructor: traverse(_AWS).get(service.split('.')), 101 | methodMocks: {}, 102 | invoked: false, 103 | }; 104 | 105 | services[service] = service_to_add; 106 | mockService(service); 107 | } 108 | 109 | const serviceObj = services[service] as Service; // we know it's `Service` because `services[service]` is defined here 110 | const methodName = method as MethodName; 111 | 112 | // Register the method to be mocked out. 113 | if (!serviceObj.methodMocks[methodName]) { 114 | // Adding passed mock method 115 | if (serviceObj !== undefined) serviceObj.methodMocks[methodName] = { replace: replace }; 116 | 117 | // If the constructor was already invoked, we need to mock the method here. 118 | if (serviceObj.invoked) { 119 | serviceObj.clients?.forEach((client: Client) => { 120 | mockServiceMethod(service, client, methodName, replace); 121 | }); 122 | } 123 | } 124 | 125 | // we know it's defined because we've defined `serviceObj.methodMocks[methodName]` above. 126 | const methodMockObj = serviceObj.methodMocks[methodName] as ValueType; 127 | 128 | return methodMockObj; 129 | } 130 | 131 | /** 132 | * Stubs the service and registers the method that needs to be re-mocked. 133 | * 134 | * @param service AWS service to mock (e.g. DynamoDB). 135 | * @param method method on AWS service to mock (e.g. `putItem` for DynamoDB). 136 | * @param replace string or function to replace the method. 137 | */ 138 | function remock(service: NestedClientName, method: NestedMethodName, replace: ReplaceFn>): void; 139 | function remock & string>( 140 | service: C, 141 | method: M, 142 | replace: ReplaceFn> 143 | ) { 144 | // If the method is inside the service, we restore the method 145 | if (services[service]?.methodMocks[method]) { 146 | restoreMethod(service, method); 147 | 148 | const service_obj = services[service]; 149 | if (service_obj !== undefined) { 150 | service_obj.methodMocks[method] = { 151 | replace: replace, 152 | }; 153 | } 154 | } 155 | 156 | const methodName = method as MethodName; 157 | // We check if the service was invoked or not. If it was, we mock the service method with the `replace` function 158 | if (services[service]?.invoked) { 159 | services[service]?.clients?.forEach((client: Client) => { 160 | mockServiceMethod(service, client, methodName, replace); 161 | }); 162 | } 163 | 164 | return services[service]?.methodMocks[method]; 165 | } 166 | 167 | /** 168 | * Stub the constructor for the service on AWS. 169 | * For example, calls of the new `AWS.SNS()` are replaced. 170 | * 171 | * @param service AWS service to mock (e.g. DynamoDB). 172 | * @returns the stubbed service. 173 | */ 174 | function mockService(service: ClientName) { 175 | const nestedServices: string[] = service.split('.'); 176 | 177 | //TODO check for undefined behaviour. If "" is passed, it will be undefined 178 | const method = nestedServices.pop() as string; 179 | // Method type guard 180 | //if (!method) return; 181 | 182 | const object = traverse(_AWS).get(nestedServices); 183 | 184 | const service_obj = services[service]; 185 | 186 | if (service_obj) { 187 | const serviceStub = sinon.stub(object, method).callsFake(function (...args) { 188 | service_obj.invoked = true; 189 | 190 | /** 191 | * Create an instance of the service by calling the real constructor 192 | * we stored before. E.g. const client = new AWS.SNS() 193 | * This is necessary in order to mock methods on the service. 194 | */ 195 | const client = new service_obj.Constructor(...args); 196 | service_obj.clients = service_obj.clients || []; 197 | service_obj.clients.push(client); 198 | 199 | // Once this has been triggered we can mock out all the registered methods. 200 | for (const key in service_obj.methodMocks) { 201 | const methodKey = key as MethodName; 202 | const objectMethodMock = service_obj.methodMocks[key]; 203 | if (objectMethodMock) { 204 | mockServiceMethod(service, client, methodKey, objectMethodMock.replace); 205 | } 206 | } 207 | return client; 208 | }); 209 | service_obj.stub = serviceStub; 210 | } 211 | } 212 | 213 | /** 214 | * Wraps a sinon stub or jest mock function as a fully functional replacement function. 215 | * 216 | * **Note**: 217 | * 218 | * If you want to help us better define the `replace` function (without having to use any), please open a PR 219 | * (especially if you're a TS wizard 🧙‍♂️). 220 | * We're not entirely sure if `SinonStubbedInstance` is correct, 221 | * but we're adding this instead of `replace: any` to maintain specificity. 222 | * 223 | * @param replace function to wrap the stub with. 224 | * @returns the stub wrapped with the given function. 225 | */ 226 | function wrapTestStubReplaceFn(replace: ReplaceFn> | MaybeSoninProxy | SinonStubbedInstance) { 227 | if (typeof replace !== 'function' || !(replace._isMockFunction || replace.isSinonProxy)) { 228 | return replace; 229 | } 230 | 231 | return (params: AWSRequest>, callback: AWSCallback> | undefined) => { 232 | // If only one argument is provided, it is the callback 233 | let cb: typeof params | AWSCallback>; 234 | if (callback === undefined || !callback) { 235 | cb = params; 236 | } 237 | 238 | // If not, the callback is the passed cb 239 | else { 240 | cb = callback; 241 | } 242 | 243 | // Spy on the users callback so we can later on determine if it has been called in their replace 244 | const cbSpy = sinon.spy(cb); 245 | try { 246 | // The replace function can also be a `functionStub`. 247 | // Call the users replace, check how many parameters it expects to determine if we should pass in callback only, or also parameters 248 | const result = replace.length === 1 ? replace(cbSpy) : replace(params, cbSpy); 249 | // If the users replace already called the callback, there's no more need for us do it. 250 | if (cbSpy.called) { 251 | return; 252 | } 253 | if (typeof result.then === 'function') { 254 | result.then( 255 | /* istanbul ignore next */ 256 | (val: any) => cb(undefined, val), 257 | (err: any) => { 258 | return cb(err); 259 | } 260 | ); 261 | } else { 262 | cb(undefined, result); 263 | } 264 | } catch (err) { 265 | cb(err); 266 | } 267 | }; 268 | } 269 | 270 | /** 271 | * Stubs the method on a service. 272 | * 273 | * All AWS service methods take two arguments: 274 | * - `params`: an object. 275 | * - `callback`: of the form 'function(err, data) {}'. 276 | * 277 | * @param service service in which the method to stub resides in. 278 | * @param client AWS client. 279 | * @param method method to stub. 280 | * @param replace function to stub with. 281 | * @returns the stubbed service method. 282 | */ 283 | function mockServiceMethod( 284 | service: ClientName, 285 | client: Client, 286 | method: MethodName, 287 | replace: ReplaceFn> 288 | ) { 289 | replace = wrapTestStubReplaceFn(replace); 290 | 291 | //TODO check for undefined behaviour. If "" is passed, it will be undefined 292 | const service_obj = services[service] as Service; 293 | // Service type guard 294 | //if (!service_obj) return; 295 | 296 | //TODO check for undefined behaviour. If "" is passed, it will be undefined 297 | const serviceMethodMock = service_obj.methodMocks[method] as Replace>; 298 | // Service method mock type guard 299 | //if (!serviceMethodMock) return; 300 | 301 | serviceMethodMock.stub = sinon.stub(client, method).callsFake(function(this: any, ...args: any[]) { 302 | let userArgs: string | Function[]; 303 | let userCallback: Function; 304 | 305 | if (typeof args[(args.length || 1) - 1] === 'function') { 306 | userArgs = args.slice(0, -1); 307 | userCallback = args[(args.length || 1) - 1]; 308 | } else { 309 | userArgs = args; 310 | } 311 | 312 | const havePromises = typeof AWS.Promise === 'function'; 313 | 314 | let promise: typeof AWS.Promise; 315 | let resolve: (value: any) => any; 316 | let reject: (value: any) => any; 317 | let storedResult: Awaited>; 318 | 319 | const tryResolveFromStored = function () { 320 | if (storedResult && promise) { 321 | if (typeof storedResult.then === 'function') { 322 | storedResult.then(resolve, reject); 323 | } else if (storedResult.reject) { 324 | reject(storedResult.reject); 325 | } else { 326 | resolve(storedResult.resolve); 327 | } 328 | } 329 | }; 330 | 331 | const callback = function (err: unknown, data: unknown) { 332 | if (!storedResult) { 333 | if (err) { 334 | storedResult = { reject: err }; 335 | } else { 336 | storedResult = { resolve: data }; 337 | } 338 | } 339 | if (userCallback) { 340 | userCallback(err, data); 341 | } 342 | tryResolveFromStored(); 343 | }; 344 | 345 | const request = { 346 | promise: havePromises 347 | ? function () { 348 | if (!promise) { 349 | promise = new AWS.Promise(function (resolve_: any, reject_: any) { 350 | resolve = resolve_; 351 | reject = reject_; 352 | }); 353 | } 354 | tryResolveFromStored(); 355 | return promise; 356 | } 357 | /* istanbul ignore next */ : undefined, 358 | createReadStream: function () { 359 | if (storedResult instanceof Readable) { 360 | return storedResult; 361 | } 362 | if (replace instanceof Readable) { 363 | return replace; 364 | } else { 365 | const stream = new Readable(); 366 | stream._read = function () { 367 | if (typeof replace === 'string' || Buffer.isBuffer(replace)) { 368 | this.push(replace); 369 | } 370 | this.push(null); 371 | }; 372 | return stream; 373 | } 374 | }, 375 | on: function (eventName: string, callback: Function) { 376 | return this; 377 | }, 378 | send: function (callback: Function) { 379 | callback(storedResult.reject, storedResult.resolve); 380 | }, 381 | abort: function () {}, 382 | }; 383 | 384 | // different locations for the paramValidation property 385 | const _client = client as ExtendedClient; 386 | const config = _client.config || _client.options || _AWS.config; 387 | if (config.paramValidation) { 388 | try { 389 | // different strategies to find method, depending on whether the service is nested/unnested 390 | const inputRules = ((_client.api && _client.api.operations[method]) || _client[method] || {}).input; 391 | if (inputRules) { 392 | const params = userArgs[(userArgs.length || 1) - 1]; 393 | // @ts-expect-error 394 | new _AWS.ParamValidator((_client.config || _AWS.config).paramValidation).validate(inputRules, params); 395 | } 396 | } catch (e) { 397 | callback(e, null); 398 | return request; 399 | } 400 | } 401 | 402 | // If the value of 'replace' is a function we call it with the arguments. 403 | if (replace instanceof Function) { 404 | const concatUserArgs = userArgs.concat([callback]) as [params: never, options: any, callback: any]; 405 | const result = replace.apply(replace, concatUserArgs); 406 | if ( 407 | storedResult === undefined && 408 | result != null && 409 | ((typeof result === 'object' && result.then instanceof Function) || result instanceof Readable) 410 | ) { 411 | storedResult = result; 412 | } 413 | } 414 | // Else we call the callback with the value of 'replace'. 415 | else { 416 | callback(null, replace); 417 | } 418 | return request as any; 419 | }); 420 | } 421 | 422 | /** 423 | * Restores the mocks for just one method on a service, the entire service, or all mocks. 424 | * 425 | * When no parameters are passed, everything will be reset. 426 | * When only the service is passed, that specific service will be reset. 427 | * When a service and method are passed, only that method will be reset. 428 | * 429 | * @param service service to be restored. 430 | * @param method method of the service to be restored. 431 | */ 432 | function restore(service?: NestedClientName, method?: NestedMethodName): void; 433 | function restore(service?: C, method?: MethodName) { 434 | if (!service) { 435 | restoreAllServices(); 436 | } else { 437 | if (method) { 438 | restoreMethod(service, method); 439 | } else { 440 | restoreService(service); 441 | } 442 | } 443 | } 444 | 445 | /** 446 | * Restores all mocked service and their corresponding methods. 447 | */ 448 | function restoreAllServices() { 449 | for (let serviceKey in services) { 450 | const service = serviceKey as ClientName; 451 | restoreService(service); 452 | } 453 | } 454 | 455 | /** 456 | * Restores a single mocked service and its corresponding methods. 457 | * @param service service to be restored. 458 | */ 459 | function restoreService(service: ClientName) { 460 | if (services[service]) { 461 | restoreAllMethods(service); 462 | 463 | const serviceObj = services[service]; 464 | if (serviceObj) { 465 | const stubFun = services[service]?.stub as SinonExpectation; 466 | if (stubFun) { 467 | stubFun.restore(); 468 | } 469 | } 470 | 471 | delete services[service]; 472 | } else { 473 | console.log('Service ' + service + ' was never instantiated yet you try to restore it.'); 474 | } 475 | } 476 | 477 | /** 478 | * Restores all mocked methods on a service. 479 | * @param service service with the methods to be restored. 480 | */ 481 | function restoreAllMethods(service: ClientName) { 482 | for (const method in services[service]?.methodMocks) { 483 | const methodName = method as MethodName; 484 | restoreMethod(service, methodName); 485 | } 486 | } 487 | 488 | /** 489 | * Restores a single mocked method on a service. 490 | * @param service service of the method to be restored. 491 | * @param method method to be restored. 492 | * @returns the restored method. 493 | */ 494 | function restoreMethod>(service: C, method: M) { 495 | const methodName = method as string; 496 | 497 | const serviceObj = services[service]; 498 | 499 | // Service type guard 500 | if (!serviceObj) { 501 | console.log('Method ' + service + ' was never instantiated yet you try to restore it.'); 502 | return; 503 | } 504 | 505 | // restore this method on all clients 506 | const serviceClients = services[service]?.clients; 507 | if (serviceClients) { 508 | // Iterate over each client and get the mocked method and restore it 509 | serviceClients.forEach((client: Client) => { 510 | const mockedClientMethod = client[methodName as keyof typeof client] as SinonSpy; 511 | if (mockedClientMethod && typeof mockedClientMethod.restore === 'function') { 512 | mockedClientMethod.restore(); 513 | } 514 | }); 515 | } 516 | delete services[service]?.methodMocks[methodName]; 517 | } 518 | 519 | (function () { 520 | const setPromisesDependency = _AWS.config.setPromisesDependency; 521 | /* only to support for older versions of aws-sdk */ 522 | if (typeof setPromisesDependency === 'function') { 523 | AWS.Promise = global.Promise; 524 | _AWS.config.setPromisesDependency = function (p) { 525 | AWS.Promise = p; 526 | return setPromisesDependency(p); 527 | }; 528 | } 529 | })(); 530 | 531 | export = AWS; 532 | -------------------------------------------------------------------------------- /test/index.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Library imports 4 | import concatStream from 'concat-stream'; 5 | import sinon from 'sinon'; 6 | 7 | // `aws-sdk-mock` import 8 | import awsMock from '../src/index.ts'; 9 | // AWS import to be used in a test 10 | import aws2 from 'aws-sdk'; 11 | 12 | // Const imports 13 | const isNodeStream = require('is-node-stream'); 14 | import AWS from 'aws-sdk'; 15 | import { Readable } from 'stream'; 16 | 17 | // Type imports 18 | import type { SNS, S3, CloudSearchDomain, DynamoDB } from 'aws-sdk'; 19 | import type { Readable as ReadableType } from 'stream'; 20 | import type { MaybeSoninProxy } from '../src/types.ts'; 21 | 22 | AWS.config.paramValidation = false; 23 | 24 | describe('TESTS', function () { 25 | afterEach(() => { 26 | awsMock.restore(); 27 | }); 28 | 29 | describe('AWS.mock function should mock AWS service and method on the service', function () { 30 | it('mock function replaces method with a function that returns replace string', function () { 31 | awsMock.mock('SNS', 'publish', 'message'); 32 | const sns: SNS = new AWS.SNS(); 33 | sns.publish({ Message: '' }, function (err, data) { 34 | expect(data).toEqual('message'); 35 | }); 36 | }); 37 | 38 | it('mock function replaces method with replace function', function () { 39 | awsMock.mock('SNS', 'publish', function (params, callback) { 40 | callback(null, 'message'); 41 | }); 42 | const sns: SNS = new AWS.SNS(); 43 | sns.publish({ Message: '' }, function (err, data) { 44 | expect(data).toEqual('message'); 45 | }); 46 | }); 47 | 48 | it('method which accepts any number of arguments can be mocked', function () { 49 | awsMock.mock('S3', 'getSignedUrl', 'message'); 50 | 51 | const s3 = new AWS.S3(); 52 | 53 | s3.getSignedUrl('getObject', {}, function (err: any, data: any) { 54 | expect(data).toEqual('message'); 55 | awsMock.mock('S3', 'upload', function (params, options, callback) { 56 | callback(null, options); 57 | }); 58 | s3.upload({ Bucket: 'b', Key: 'k' }, { partSize: 1 }, function (err: any, data: any) { 59 | expect(data.partSize).toEqual(1); 60 | }); 61 | }); 62 | }); 63 | 64 | it('method fails on invalid input if paramValidation is set', function () { 65 | awsMock.mock('S3', 'getObject', { Body: 'body' }); 66 | const s3: S3 = new AWS.S3({ paramValidation: true }); 67 | // We're ignoring because if using typescript, it will complain that `notKey` is not a valid property 68 | // @ts-ignore 69 | s3.getObject({ Bucket: 'b', notKey: 'k' }, function (err: any, data: any) { 70 | expect(err).toBeTruthy(); 71 | expect(data).toBeFalsy(); 72 | }); 73 | }); 74 | 75 | it('method with no input rules can be mocked even if paramValidation is set', function () { 76 | awsMock.mock('S3', 'getSignedUrl', 'message'); 77 | const s3: S3 = new AWS.S3({ paramValidation: true }); 78 | s3.getSignedUrl('getObject', {}, function (err, data) { 79 | expect(data).toEqual('message'); 80 | }); 81 | }); 82 | 83 | it('method succeeds on valid input when paramValidation is set', function () { 84 | awsMock.mock('S3', 'getObject', { Body: 'body' }); 85 | const s3: S3 = new AWS.S3({ paramValidation: true }); 86 | s3.getObject({ Bucket: 'b', Key: 'k' }, function (err, data) { 87 | expect(err).toBeFalsy(); 88 | expect(data.Body).toEqual('body'); 89 | }); 90 | }); 91 | 92 | it('method is not re-mocked if a mock already exists', function () { 93 | awsMock.mock('SNS', 'publish', function (params, callback) { 94 | callback(null, 'message'); 95 | }); 96 | const sns: SNS = new AWS.SNS(); 97 | awsMock.mock('SNS', 'publish', function (params, callback) { 98 | callback(null, 'test'); 99 | }); 100 | sns.publish({ Message: '' }, function (err, data) { 101 | expect(data).toEqual('message'); 102 | }); 103 | }); 104 | 105 | it('service is not re-mocked if a mock already exists', function () { 106 | awsMock.mock('SNS', 'publish', function (params, callback) { 107 | callback(null, 'message'); 108 | }); 109 | const sns: SNS = new AWS.SNS(); 110 | awsMock.mock('SNS', 'subscribe', function (params, callback) { 111 | callback(null, 'test'); 112 | }); 113 | sns.subscribe({ Protocol: '', TopicArn: '' }, function (err, data) { 114 | expect(data).toEqual('test'); 115 | }); 116 | }); 117 | 118 | it('service is re-mocked when remock called', function () { 119 | awsMock.mock('SNS', 'subscribe', function (params, callback) { 120 | callback(null, 'message 1'); 121 | }); 122 | const sns: SNS = new AWS.SNS(); 123 | awsMock.remock('SNS', 'subscribe', function (params, callback) { 124 | callback(null, 'message 2'); 125 | }); 126 | sns.subscribe({ Protocol: '', TopicArn: '' }, function (err, data) { 127 | expect(data).toEqual('message 2'); 128 | }); 129 | }); 130 | it('all instances of service are re-mocked when remock called', function () { 131 | awsMock.mock('SNS', 'subscribe', function (params, callback) { 132 | callback(null, 'message 1'); 133 | }); 134 | const sns1: SNS = new AWS.SNS(); 135 | const sns2: SNS = new AWS.SNS(); 136 | 137 | awsMock.remock('SNS', 'subscribe', function (params, callback) { 138 | callback(null, 'message 2'); 139 | }); 140 | 141 | sns1.subscribe({ Protocol: '', TopicArn: '' }, function (err, data) { 142 | expect(data).toEqual('message 2'); 143 | 144 | sns2.subscribe({ Protocol: '', TopicArn: '' }, function (err, data) { 145 | expect(data).toEqual('message 2'); 146 | }); 147 | }); 148 | }); 149 | it('multiple methods can be mocked on the same service', function () { 150 | awsMock.mock('Lambda', 'getFunction', function (params, callback) { 151 | callback(null, 'message'); 152 | }); 153 | awsMock.mock('Lambda', 'createFunction', function (params, callback) { 154 | callback(null, 'message'); 155 | }); 156 | const lambda = new AWS.Lambda(); 157 | lambda.getFunction({ FunctionName: '' }, function (err: any, data: any) { 158 | expect(data).toEqual('message'); 159 | lambda.createFunction({ Role: '', Code: {}, FunctionName: '' }, function (err: any, data: any) { 160 | expect(data).toEqual('message'); 161 | }); 162 | }); 163 | }); 164 | if (typeof Promise === 'function') { 165 | it('promises are supported', function () { 166 | const error = new Error('on purpose'); 167 | awsMock.mock('Lambda', 'getFunction', function (params, callback) { 168 | callback(null, 'message'); 169 | }); 170 | awsMock.mock('Lambda', 'createFunction', function (params, callback) { 171 | callback(error, 'message'); 172 | }); 173 | const lambda = new AWS.Lambda(); 174 | lambda 175 | .getFunction({ FunctionName: '' }) 176 | .promise() 177 | .then(function (data: any) { 178 | expect(data).toEqual('message'); 179 | }) 180 | .then(function () { 181 | return lambda.createFunction({ Role: '', Code: {}, FunctionName: '' }).promise(); 182 | }) 183 | .catch(function (data: any) { 184 | expect(data).toEqual(error); 185 | }); 186 | }); 187 | it('replacement returns thennable', function () { 188 | const error = new Error('on purpose'); 189 | awsMock.mock('Lambda', 'getFunction', function (params) { 190 | return Promise.resolve('message'); 191 | }); 192 | awsMock.mock('Lambda', 'createFunction', function (params, callback) { 193 | return Promise.reject(error); 194 | }); 195 | const lambda = new AWS.Lambda(); 196 | lambda 197 | .getFunction({ FunctionName: '' }) 198 | .promise() 199 | .then(function (data: any) { 200 | expect(data).toEqual('message'); 201 | }) 202 | .then(function () { 203 | return lambda.createFunction({ Role: '', Code: {}, FunctionName: '' }).promise(); 204 | }) 205 | .catch(function (data: any) { 206 | expect(data).toEqual(error); 207 | }); 208 | }); 209 | it('no unhandled promise rejections when promises are not used', function () { 210 | process.on('unhandledRejection', function (reason, promise) { 211 | throw 'unhandledRejection: ' + reason; 212 | }); 213 | awsMock.mock('S3', 'getObject', function (params, callback) { 214 | callback('This is a test error to see if promise rejections go unhandled'); 215 | }); 216 | const S3: S3 = new AWS.S3(); 217 | S3.getObject({ Bucket: '', Key: '' }, function (err, data) {}); 218 | }); 219 | it('promises work with async completion', function () { 220 | const error = new Error('on purpose'); 221 | awsMock.mock('Lambda', 'getFunction', function (this: any, params, callback) { 222 | setTimeout(callback.bind(this, null, 'message'), 10); 223 | }); 224 | awsMock.mock('Lambda', 'createFunction', function (this: any, params, callback) { 225 | setTimeout(callback.bind(this, error, 'message'), 10); 226 | }); 227 | const lambda = new AWS.Lambda(); 228 | lambda 229 | .getFunction({ FunctionName: '' }) 230 | .promise() 231 | .then(function (data: any) { 232 | expect(data).toEqual('message'); 233 | }) 234 | .then(function () { 235 | return lambda.createFunction({ Role: '', Code: {}, FunctionName: '' }).promise(); 236 | }) 237 | .catch(function (data: any) { 238 | expect(data).toEqual(error); 239 | }); 240 | }); 241 | it('promises can be configured', function () { 242 | awsMock.mock('Lambda', 'getFunction', function (params, callback) { 243 | callback(null, 'message'); 244 | }); 245 | const lambda = new AWS.Lambda(); 246 | function P(this: any, handler: any) { 247 | const self = this; 248 | function yay(value: any) { 249 | self.value = value; 250 | } 251 | handler(yay, function () {}); 252 | } 253 | P.prototype.then = function (yay: any) { 254 | if (this.value) yay(this.value); 255 | }; 256 | AWS.config.setPromisesDependency(P); 257 | const promise = lambda.getFunction({ FunctionName: '' }).promise(); 258 | expect(promise.constructor.name).toEqual('P'); 259 | promise.then(function (data: any) { 260 | expect(data).toEqual('message'); 261 | }); 262 | }); 263 | } 264 | it('request object supports createReadStream', function () { 265 | awsMock.mock('S3', 'getObject', 'body'); 266 | const s3 = new AWS.S3(); 267 | let req = s3.getObject({ Bucket: '', Key: '' }, function (err: any, data: any) {}); 268 | expect(isNodeStream(req.createReadStream())).toBeTruthy(); 269 | // with or without callback 270 | req = s3.getObject({ Bucket: '', Key: '' }); 271 | expect(isNodeStream(req.createReadStream())).toBeTruthy(); 272 | // stream is currently always empty but that's subject to change. 273 | // let's just consume it and ignore the contents 274 | req = s3.getObject({ Bucket: '', Key: '' }); 275 | const stream = req.createReadStream(); 276 | stream.pipe(concatStream(function () {})); 277 | }); 278 | it('request object createReadStream works with streams', function () { 279 | const bodyStream: ReadableType = new Readable(); 280 | bodyStream.push('body'); 281 | bodyStream.push(null); 282 | awsMock.mock('S3', 'getObject', bodyStream); 283 | const stream = new AWS.S3().getObject({ Bucket: '', Key: '' }).createReadStream(); 284 | stream.pipe( 285 | concatStream(function (actual: any) { 286 | expect(actual.toString()).toEqual('body'); 287 | }) 288 | ); 289 | }); 290 | it('request object createReadStream works with returned streams', function () { 291 | awsMock.mock('S3', 'getObject', () => { 292 | const bodyStream: ReadableType = new Readable(); 293 | bodyStream.push('body'); 294 | bodyStream.push(null); 295 | return bodyStream; 296 | }); 297 | const stream = new AWS.S3().getObject({ Bucket: '', Key: '' }).createReadStream(); 298 | stream.pipe( 299 | concatStream(function (actual: any) { 300 | expect(actual.toString()).toEqual('body'); 301 | }) 302 | ); 303 | }); 304 | it('request object createReadStream works with strings', function () { 305 | awsMock.mock('S3', 'getObject', 'body'); 306 | const s3: S3 = new AWS.S3(); 307 | const req = s3.getObject({ Bucket: '', Key: '' }, () => {}); 308 | const stream = req.createReadStream(); 309 | stream.pipe( 310 | concatStream(function (actual: any) { 311 | expect(actual.toString()).toEqual('body'); 312 | }) 313 | ); 314 | }); 315 | it('request object createReadStream works with buffers', function () { 316 | awsMock.mock('S3', 'getObject', Buffer.alloc(4, 'body')); 317 | const s3: S3 = new AWS.S3(); 318 | const req = s3.getObject({ Bucket: '', Key: '' }, () => {}); 319 | const stream = req.createReadStream(); 320 | stream.pipe( 321 | concatStream(function (actual: any) { 322 | expect(actual.toString()).toEqual('body'); 323 | }) 324 | ); 325 | }); 326 | it('request object createReadStream ignores functions', function () { 327 | awsMock.mock('S3', 'getObject', function () {}); 328 | const s3: S3 = new AWS.S3(); 329 | const req = s3.getObject({ Bucket: '', Key: '' }, () => {}); 330 | const stream = req.createReadStream(); 331 | stream.pipe( 332 | concatStream(function (actual: any) { 333 | expect(actual.toString()).toEqual(''); 334 | }) 335 | ); 336 | }); 337 | it('request object createReadStream ignores non-buffer objects', function () { 338 | awsMock.mock('S3', 'getObject', { Body: 'body' }); 339 | const s3: S3 = new AWS.S3(); 340 | const req = s3.getObject({ Bucket: '', Key: '' }, () => {}); 341 | const stream = req.createReadStream(); 342 | stream.pipe( 343 | concatStream(function (actual: any) { 344 | expect(actual.toString()).toEqual(''); 345 | }) 346 | ); 347 | }); 348 | it('call on method of request object', function () { 349 | awsMock.mock('S3', 'getObject', { Body: 'body' }); 350 | const s3 = new AWS.S3(); 351 | const req = s3.getObject({ Bucket: '', Key: '' }, () => {}); 352 | expect(typeof req.on).toEqual('function'); 353 | }); 354 | it('call send method of request object', function () { 355 | awsMock.mock('S3', 'getObject', { Body: 'body' }); 356 | const s3 = new AWS.S3(); 357 | const req = s3.getObject({ Bucket: '', Key: '' }, () => {}); 358 | expect(typeof req.on).toEqual('function'); 359 | }); 360 | it('all the methods on a service are restored', function () { 361 | awsMock.mock('SNS', 'publish', function (params, callback) { 362 | callback(null, 'message'); 363 | }); 364 | expect((AWS.SNS as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 365 | 366 | awsMock.restore('SNS'); 367 | 368 | expect(AWS.SNS.hasOwnProperty('isSinonProxy')).toEqual(false); 369 | }); 370 | it('only the method on the service is restored', function () { 371 | awsMock.mock('SNS', 'publish', function (params, callback) { 372 | callback(null, 'message'); 373 | }); 374 | const sns = new AWS.SNS(); 375 | expect((AWS.SNS as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 376 | expect((sns.publish as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 377 | 378 | awsMock.restore('SNS', 'publish'); 379 | 380 | expect(AWS.SNS.hasOwnProperty('isSinonProxy')).toEqual(true); 381 | expect(sns.publish.hasOwnProperty('isSinonProxy')).toEqual(false); 382 | }); 383 | it('method on all service instances are restored', function () { 384 | awsMock.mock('SNS', 'publish', function (params, callback) { 385 | callback(null, 'message'); 386 | }); 387 | 388 | const sns1 = new AWS.SNS(); 389 | const sns2 = new AWS.SNS(); 390 | 391 | expect((AWS.SNS as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 392 | expect((sns1.publish as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 393 | expect((sns2.publish as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 394 | 395 | awsMock.restore('SNS', 'publish'); 396 | 397 | expect(AWS.SNS.hasOwnProperty('isSinonProxy')).toEqual(true); 398 | expect(sns1.publish.hasOwnProperty('isSinonProxy')).toEqual(false); 399 | expect(sns2.publish.hasOwnProperty('isSinonProxy')).toEqual(false); 400 | }); 401 | it('all methods on all service instances are restored', function () { 402 | awsMock.mock('SNS', 'publish', function (params, callback) { 403 | callback(null, 'message'); 404 | }); 405 | 406 | const sns1 = new AWS.SNS(); 407 | const sns2 = new AWS.SNS(); 408 | 409 | expect((AWS.SNS as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 410 | expect((sns1.publish as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 411 | expect((sns2.publish as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 412 | 413 | awsMock.restore('SNS'); 414 | 415 | expect(AWS.SNS.hasOwnProperty('isSinonProxy')).toEqual(false); 416 | expect(sns1.publish.hasOwnProperty('isSinonProxy')).toEqual(false); 417 | expect(sns2.publish.hasOwnProperty('isSinonProxy')).toEqual(false); 418 | }); 419 | it('all the services are restored when no arguments given to awsMock.restore', function () { 420 | awsMock.mock('SNS', 'publish', function (params, callback) { 421 | callback(null, 'message'); 422 | }); 423 | awsMock.mock('DynamoDB', 'putItem', function (params, callback) { 424 | callback(null, 'test'); 425 | }); 426 | awsMock.mock('DynamoDB.DocumentClient', 'put', function (params, callback) { 427 | callback(null, 'test'); 428 | }); 429 | const sns = new AWS.SNS(); 430 | const docClient = new AWS.DynamoDB.DocumentClient(); 431 | const dynamoDb = new AWS.DynamoDB(); 432 | 433 | expect((AWS.SNS as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 434 | expect((AWS.DynamoDB.DocumentClient as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 435 | expect((AWS.DynamoDB as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 436 | expect((sns.publish as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 437 | expect((docClient.put as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 438 | expect((dynamoDb.putItem as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 439 | 440 | awsMock.restore(); 441 | 442 | expect(AWS.SNS.hasOwnProperty('isSinonProxy')).toEqual(false); 443 | expect(AWS.DynamoDB.DocumentClient.hasOwnProperty('isSinonProxy')).toEqual(false); 444 | expect(AWS.DynamoDB.hasOwnProperty('isSinonProxy')).toEqual(false); 445 | expect(sns.publish.hasOwnProperty('isSinonProxy')).toEqual(false); 446 | expect(docClient.put.hasOwnProperty('isSinonProxy')).toEqual(false); 447 | expect(dynamoDb.putItem.hasOwnProperty('isSinonProxy')).toEqual(false); 448 | }); 449 | it('a nested service can be mocked properly', function () { 450 | awsMock.mock('DynamoDB.DocumentClient', 'put', 'message'); 451 | const docClient = new AWS.DynamoDB.DocumentClient(); 452 | awsMock.mock('DynamoDB.DocumentClient', 'put', function (params, callback) { 453 | callback(null, 'test'); 454 | }); 455 | awsMock.mock('DynamoDB.DocumentClient', 'get', function (params, callback) { 456 | callback(null, 'test'); 457 | }); 458 | 459 | expect((AWS.DynamoDB.DocumentClient as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 460 | expect((docClient.put as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 461 | expect((docClient.get as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 462 | 463 | docClient.put({ Item: {}, TableName: '' }, function (err: any, data: any) { 464 | expect(data).toEqual('message'); 465 | docClient.get({ Key: {}, TableName: '' }, function (err: any, data: any) { 466 | expect(data).toEqual('test'); 467 | 468 | awsMock.restore('DynamoDB.DocumentClient', 'get'); 469 | expect((AWS.DynamoDB.DocumentClient as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 470 | expect(docClient.get.hasOwnProperty('isSinonProxy')).toEqual(false); 471 | 472 | awsMock.restore('DynamoDB.DocumentClient'); 473 | expect(AWS.DynamoDB.DocumentClient.hasOwnProperty('isSinonProxy')).toEqual(false); 474 | expect(docClient.put.hasOwnProperty('isSinonProxy')).toEqual(false); 475 | }); 476 | }); 477 | }); 478 | it('a nested service can be mocked properly even when paramValidation is set', function () { 479 | awsMock.mock('DynamoDB.DocumentClient', 'query', function (params, callback) { 480 | callback(null, 'test'); 481 | }); 482 | const docClient = new AWS.DynamoDB.DocumentClient({ paramValidation: true }); 483 | 484 | expect((AWS.DynamoDB.DocumentClient as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 485 | expect((docClient.query as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 486 | docClient.query({ TableName: '' }, function (err: any, data: any) { 487 | expect(err).toEqual(null); 488 | expect(data).toEqual('test'); 489 | }); 490 | }); 491 | it('a mocked service and a mocked nested service can coexist as long as the nested service is mocked first', function () { 492 | awsMock.mock('DynamoDB.DocumentClient', 'get', 'message'); 493 | awsMock.mock('DynamoDB', 'getItem', 'test'); 494 | const docClient = new AWS.DynamoDB.DocumentClient(); 495 | let dynamoDb = new AWS.DynamoDB(); 496 | 497 | expect((AWS.DynamoDB.DocumentClient as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 498 | expect((AWS.DynamoDB as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 499 | expect((docClient.get as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 500 | expect((dynamoDb.getItem as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 501 | 502 | awsMock.restore('DynamoDB'); 503 | expect((AWS.DynamoDB.DocumentClient as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 504 | expect(AWS.DynamoDB.hasOwnProperty('isSinonProxy')).toEqual(false); 505 | expect((docClient.get as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 506 | expect(dynamoDb.getItem.hasOwnProperty('isSinonProxy')).toEqual(false); 507 | 508 | awsMock.mock('DynamoDB', 'getItem', 'test'); 509 | dynamoDb = new AWS.DynamoDB(); 510 | expect((AWS.DynamoDB.DocumentClient as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 511 | expect((AWS.DynamoDB as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 512 | expect((docClient.get as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 513 | expect((dynamoDb.getItem as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 514 | 515 | awsMock.restore('DynamoDB.DocumentClient'); 516 | 517 | // the first assertion is true because DynamoDB is still mocked 518 | expect(AWS.DynamoDB.DocumentClient.hasOwnProperty('isSinonProxy')).toEqual(true); 519 | expect((AWS.DynamoDB as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 520 | expect(docClient.get.hasOwnProperty('isSinonProxy')).toEqual(false); 521 | expect((dynamoDb.getItem as unknown as MaybeSoninProxy).isSinonProxy).toEqual(true); 522 | 523 | awsMock.restore('DynamoDB'); 524 | expect(AWS.DynamoDB.DocumentClient.hasOwnProperty('isSinonProxy')).toEqual(false); 525 | expect(AWS.DynamoDB.hasOwnProperty('isSinonProxy')).toEqual(false); 526 | expect(docClient.get.hasOwnProperty('isSinonProxy')).toEqual(false); 527 | expect(dynamoDb.getItem.hasOwnProperty('isSinonProxy')).toEqual(false); 528 | }); 529 | it('Mocked services should use the implementation configuration arguments without complaining they are missing', function () { 530 | awsMock.mock('CloudSearchDomain', 'search', function (params, callback) { 531 | return callback(null, 'message'); 532 | }); 533 | 534 | const csd: CloudSearchDomain = new AWS.CloudSearchDomain({ 535 | endpoint: 'some endpoint', 536 | region: 'eu-west', 537 | }); 538 | 539 | awsMock.mock('CloudSearchDomain', 'suggest', function (params, callback) { 540 | return callback(null, 'message'); 541 | }); 542 | 543 | csd.search({ query: '' }, function (err, data) { 544 | expect(data).toEqual('message'); 545 | }); 546 | 547 | csd.search({ query: '' }, function (err, data) { 548 | expect(data).toEqual('message'); 549 | }); 550 | }); 551 | 552 | //it.skip('Mocked service should return the sinon stub', function () { 553 | // // TODO: the stub is only returned if an instance was already constructed 554 | // const stub = awsMock.mock('CloudSearchDomain', 'search', ''); 555 | // st.equal(stub.stub?.isSinonProxy, true); 556 | // 557 | //}); 558 | 559 | it('Restore should not fail when the stub did not exist.', function () { 560 | // This test will fail when restoring throws unneeded errors. 561 | try { 562 | awsMock.restore(''); 563 | awsMock.restore('', ''); 564 | awsMock.restore('Lambda'); 565 | awsMock.restore('SES', 'sendEmail'); 566 | awsMock.restore('CloudSearchDomain', 'doesnotexist'); 567 | } catch (e) { 568 | console.log(e); 569 | } 570 | }); 571 | 572 | it('Restore should not fail when service was not mocked', function () { 573 | // This test will fail when restoring throws unneeded errors. 574 | try { 575 | awsMock.restore('CloudFormation'); 576 | awsMock.restore('UnknownService'); 577 | } catch (e) { 578 | console.log(e); 579 | } 580 | }); 581 | 582 | it('Mocked service should allow chained calls after listening to events', function () { 583 | awsMock.mock('S3', 'getObject', ''); 584 | const s3 = new AWS.S3(); 585 | const req = s3.getObject({ Bucket: 'b', Key: '' }); 586 | expect(req.on('httpHeaders', () => {})).toEqual(req); 587 | }); 588 | 589 | it('Mocked service should return replaced function when request send is called', function () { 590 | awsMock.mock('S3', 'getObject', { Body: 'body' }); 591 | let returnedValue = ''; 592 | const s3 = new AWS.S3(); 593 | const req = s3.getObject({ Bucket: 'b', Key: '' }, () => {}); 594 | req.send(async (err: any, data: any) => { 595 | returnedValue = data.Body; 596 | }); 597 | expect(returnedValue).toEqual('body'); 598 | }); 599 | 600 | it('mock function replaces method with a sinon stub and returns successfully using callback', function () { 601 | const sinonStub = sinon.stub(); 602 | sinonStub.returns('message'); 603 | awsMock.mock('DynamoDB', 'getItem', sinonStub); 604 | const db: DynamoDB = new AWS.DynamoDB(); 605 | db.getItem({ TableName: '', Key: {} }, function (err, data) { 606 | expect(data).toEqual('message'); 607 | expect(sinonStub.called).toEqual(true); 608 | }); 609 | }); 610 | 611 | it('mock function replaces method with a sinon stub and returns successfully using promise', function () { 612 | const sinonStub = sinon.stub(); 613 | sinonStub.returns('message'); 614 | awsMock.mock('DynamoDB', 'getItem', sinonStub); 615 | const db: DynamoDB = new AWS.DynamoDB(); 616 | db.getItem({ TableName: '', Key: {} }) 617 | .promise() 618 | .then(function (data) { 619 | expect(data).toEqual('message'); 620 | expect(sinonStub.called).toEqual(true); 621 | }); 622 | }); 623 | 624 | it('mock function replaces method with a mock and returns successfully', function () { 625 | const sinonStub = sinon.stub().returns('message'); 626 | awsMock.mock('DynamoDB', 'getItem', sinonStub); 627 | const db: DynamoDB = new AWS.DynamoDB(); 628 | db.getItem({ TableName: '', Key: {} }, function (err, data) { 629 | expect(data).toEqual('message'); 630 | expect(sinonStub.callCount).toEqual(1); 631 | }); 632 | }); 633 | 634 | it('mock function replaces method with a mock returning successfully and allows mocked method to be called with only callback', function () { 635 | const sinonStub = sinon.stub().returns('message'); 636 | awsMock.mock('DynamoDB', 'getItem', sinonStub); 637 | const db: DynamoDB = new AWS.DynamoDB(); 638 | db.getItem(function (err, data) { 639 | expect(data).toEqual('message'); 640 | expect(sinonStub.callCount).toEqual(1); 641 | }); 642 | }); 643 | 644 | it('mock function replaces method with a mock and resolves successfully', function () { 645 | const sinonStub = sinon.stub().returns('message'); 646 | awsMock.mock('DynamoDB', 'getItem', sinonStub); 647 | const db: DynamoDB = new AWS.DynamoDB(); 648 | db.getItem({ TableName: '', Key: {} }, function (err, data) { 649 | expect(data).toEqual('message'); 650 | expect(sinonStub.callCount).toEqual(1); 651 | }); 652 | }); 653 | 654 | it('mock function replaces method with a mock and fails successfully', function () { 655 | const sinonStub = sinon.stub().throws(new Error('something went wrong')); 656 | awsMock.mock('DynamoDB', 'getItem', sinonStub); 657 | const db: DynamoDB = new AWS.DynamoDB(); 658 | db.getItem({ TableName: '', Key: {} }, function (err) { 659 | expect(err.message).toEqual('something went wrong'); 660 | expect(sinonStub.callCount).toEqual(1); 661 | }); 662 | }); 663 | 664 | it('mock function replaces method with a mock and rejects successfully', function () { 665 | const sinonStub = sinon.stub().rejects(new Error('something went wrong')); 666 | awsMock.mock('DynamoDB', 'getItem', sinonStub); 667 | const db: DynamoDB = new AWS.DynamoDB(); 668 | db.getItem({ TableName: '', Key: {} }, function (err) { 669 | expect(err.message).toEqual('something went wrong'); 670 | expect(sinonStub.callCount).toEqual(1); 671 | }); 672 | }); 673 | 674 | it('mock function replaces method with a mock with implementation', function () { 675 | const sinonStub = sinon.stub().yields(null, 'item'); 676 | awsMock.mock('DynamoDB', 'getItem', sinonStub); 677 | const db: DynamoDB = new AWS.DynamoDB(); 678 | db.getItem({ TableName: '', Key: {} }, function (err, data) { 679 | expect(sinonStub.callCount).toEqual(1); 680 | expect(data).toEqual('item'); 681 | }); 682 | }); 683 | 684 | it('mock function replaces method with a mock with implementation and allows mocked method to be called with only callback', function () { 685 | const sinonStub = sinon.stub().returns('item'); 686 | awsMock.mock('DynamoDB', 'getItem', sinonStub); 687 | const db: DynamoDB = new AWS.DynamoDB(); 688 | db.getItem(function (err, data) { 689 | expect(sinonStub.callCount).toEqual(1); 690 | expect(data).toEqual('item'); 691 | }); 692 | }); 693 | 694 | it('mock function replaces method with a mock with implementation expecting only a callback', function () { 695 | const sinonStub = sinon.stub().returns('item'); 696 | awsMock.mock('DynamoDB', 'getItem', sinonStub); 697 | const db: DynamoDB = new AWS.DynamoDB(); 698 | db.getItem(function (err, data) { 699 | expect(sinonStub.callCount).toEqual(1); 700 | expect(data).toEqual('item'); 701 | }); 702 | }); 703 | 704 | it('Mocked service should allow abort call', function () { 705 | awsMock.mock('S3', 'upload', ''); 706 | const s3 = new AWS.S3(); 707 | const req = s3.upload({Bucket: '', Key: ''}, { leavePartsOnError: true }, function () {}); 708 | req.abort(); 709 | }); 710 | }); 711 | 712 | describe('AWS.setSDK function should mock a specific AWS module', function () { 713 | it('Specific Modules can be set for mocking', async function () { 714 | awsMock.setSDK('aws-sdk'); 715 | awsMock.mock('SNS', 'publish', 'message'); 716 | const sns: SNS = new AWS.SNS(); 717 | sns.publish({ Message: '' }, function (err, data) { 718 | expect(data).toEqual('message'); 719 | }); 720 | }); 721 | 722 | it('Modules with multi-parameter constructors can be set for mocking', async function () { 723 | awsMock.setSDK('aws-sdk'); 724 | awsMock.mock('CloudFront.Signer', 'getSignedUrl', ''); 725 | const signer = new AWS.CloudFront.Signer('key-pair-id', 'private-key'); 726 | expect(signer).toBeDefined(); 727 | }); 728 | 729 | it('Setting the aws-sdk to the wrong module can cause an exception when mocking', async function () { 730 | awsMock.setSDK('sinon'); 731 | try { 732 | awsMock.mock('SNS', 'publish', 'message'); 733 | throw 'Mocking should have thrown an error for an invalid module' 734 | } catch (error) { 735 | // No error was tossed 736 | expect(true).toBeTruthy(); 737 | } 738 | awsMock.setSDK('aws-sdk'); 739 | }); 740 | }); 741 | 742 | describe('AWS.setSDKInstance function should mock a specific AWS module', function () { 743 | it('Specific Modules can be set for mocking', function () { 744 | awsMock.setSDKInstance(aws2); 745 | awsMock.mock('SNS', 'publish', 'message2'); 746 | const sns: SNS = new AWS.SNS(); 747 | sns.publish({ Message: '' }, function (err, data) { 748 | expect(data).toEqual('message2'); 749 | }); 750 | }); 751 | 752 | it('Setting the aws-sdk to the wrong instance can cause an exception when mocking', function () { 753 | const bad = {}; 754 | //@ts-ignore This won't be possible with typescript but in case someone tries to override it, we'll test it this way 755 | awsMock.setSDKInstance(bad); 756 | expect(function () { 757 | awsMock.mock('SNS', 'publish', 'message'); 758 | }).toThrow(); 759 | awsMock.setSDKInstance(AWS); 760 | }); 761 | }); 762 | }); 763 | --------------------------------------------------------------------------------