├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ └── pipeline.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .nvmrc ├── .prettierignore ├── .vscode └── settings.json ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-postinstall-dev.cjs │ │ └── plugin-version.cjs ├── releases │ └── yarn-berry.cjs └── versions │ ├── 5de4ad7d.yml │ ├── 8c36e5ca.yml │ ├── a06bc2e2.yml │ ├── a857885b.yml │ ├── ab14bd23.yml │ ├── ada3831a.yml │ └── adb1294a.yml ├── .yarnrc.yml ├── LICENSE ├── README.md ├── babel.config.js ├── clearAfterEach.js ├── commitlint.config.js ├── e2e ├── global-setup │ ├── globalSetup.js │ ├── globalTeardown.js │ ├── jest-dynalite-config.js │ ├── jest.config.js │ ├── package.json │ └── src │ │ ├── keystore.js │ │ └── keystore.test.js ├── jest-27 │ ├── jest-dynalite-config.js │ ├── jest.config.js │ ├── package.json │ └── src │ │ ├── keystore.js │ │ └── keystore.test.js ├── monorepo │ ├── jest.config.js │ ├── package.json │ └── packages │ │ └── keystore │ │ ├── jest-dynalite-config.js │ │ ├── jest.config.js │ │ ├── package.json │ │ └── src │ │ ├── keystore.js │ │ └── keystore.test.js └── preset │ ├── jest-dynalite-config.js │ ├── jest.config.js │ ├── package.json │ └── src │ ├── keystore.js │ └── keystore.test.js ├── environment.js ├── jest-preset.js ├── jest.base.ts ├── jest.config.ts ├── jest.unit.config.ts ├── package.json ├── setupTables.js ├── src ├── __snapshots__ │ └── environment.spec.ts.snap ├── __testdir__ │ └── jest-dynalite-config.js ├── clearAfterEach.ts ├── config.spec.ts ├── config.ts ├── db.ts ├── dynamodb │ ├── v2.ts │ └── v3.ts ├── environment.spec.ts ├── environment.ts ├── index.ts ├── setup.ts ├── setupTables.ts ├── types.ts └── utils.ts ├── tests ├── configs │ ├── cjs │ │ ├── jest-dynalite-config.cjs │ │ └── tables.js │ ├── javascript │ │ ├── jest-dynalite-config.js │ │ └── tables.js │ ├── tables-function-async │ │ └── jest-dynalite-config.ts │ ├── tables-function │ │ └── jest-dynalite-config.ts │ └── tables.ts ├── jest-advanced.config.ts ├── jest-cjs.config.ts ├── jest-dynalite-config.ts ├── jest-jsdom-environment.ts ├── jest-sdk-v2.config.ts ├── jest-simple.config.ts ├── jest-tables-function-async.config.ts ├── jest-tables-function-js.config.ts ├── jest-tables-function.config.ts ├── setups │ ├── setupAdvanced.ts │ ├── setupAdvancedEnv.ts │ ├── setupCjs.ts │ ├── setupDynamodbV2.ts │ ├── setupSimple.ts │ ├── setupTablesFunction.ts │ ├── setupTablesFunctionAsync.ts │ └── setupTablesFunctionJs.ts └── suites │ ├── modern-timers.test.ts │ ├── suite1.test.ts │ ├── suite2.test.ts │ ├── table-data.test.ts │ └── timers.test.ts ├── tsconfig.build.json ├── tsconfig.eslint.json ├── tsconfig.json ├── types └── dynalite.d.ts ├── withDb.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | !.jest -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["@typescript-eslint", "prettier", "import", "jest"], 3 | extends: [ 4 | "airbnb-typescript/base", 5 | "plugin:jest/recommended", 6 | "plugin:prettier/recommended", 7 | ], 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { 10 | extraFileExtensions: [".cjs"], 11 | warnOnUnsupportedTypeScriptVersion: false, 12 | project: "tsconfig.eslint.json", 13 | }, 14 | env: { 15 | jest: true, 16 | browser: true, 17 | }, 18 | rules: { 19 | "jest/expect-expect": "off", 20 | "@typescript-eslint/no-unused-vars": "error", 21 | "@typescript-eslint/explicit-function-return-type": [ 22 | "error", 23 | { 24 | allowExpressions: true, 25 | allowTypedFunctionExpressions: true, 26 | }, 27 | ], 28 | }, 29 | 30 | overrides: [ 31 | { 32 | files: ["**/*.js"], 33 | rules: { 34 | "@typescript-eslint/no-var-requires": "off", 35 | "@typescript-eslint/explicit-function-return-type": "off", 36 | }, 37 | }, 38 | ], 39 | }; 40 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Pipeline 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: Use Node.js 16 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: "16.x" 15 | - name: Lint 16 | run: | 17 | yarn install --immutable --mode=skip-build 18 | yarn lint 19 | env: 20 | CI: true 21 | 22 | build: 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - uses: actions/checkout@v1 27 | - name: Use Node.js 16 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: "16.x" 31 | - name: Build 32 | run: | 33 | yarn install --immutable --mode=skip-build 34 | yarn test:types 35 | env: 36 | CI: true 37 | 38 | test: 39 | runs-on: ubuntu-latest 40 | needs: build 41 | 42 | strategy: 43 | matrix: 44 | node-version: [14.x, 16.x] 45 | 46 | steps: 47 | - uses: actions/checkout@v1 48 | - name: Use Node.js 12 49 | uses: actions/setup-node@v1 50 | with: 51 | node-version: ${{ matrix.node-version }} 52 | - name: Test 53 | run: | 54 | yarn install --immutable --mode=skip-build 55 | yarn test --coverage 56 | env: 57 | CI: true 58 | - name: Publish Coverage 59 | uses: coverallsapp/github-action@master 60 | with: 61 | github-token: ${{ secrets.GITHUB_TOKEN }} 62 | 63 | e2e: 64 | runs-on: ubuntu-latest 65 | needs: build 66 | 67 | strategy: 68 | matrix: 69 | node-version: [14.x, 16.x] 70 | 71 | steps: 72 | - uses: actions/checkout@v1 73 | - name: Use Node.js 12 74 | uses: actions/setup-node@v1 75 | with: 76 | node-version: ${{ matrix.node-version }} 77 | - name: E2E Tests 78 | run: | 79 | yarn 80 | yarn e2e 81 | env: 82 | CI: true 83 | 84 | publish: 85 | runs-on: ubuntu-latest 86 | 87 | needs: [lint, test, e2e] 88 | 89 | if: startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '.') 90 | steps: 91 | - uses: actions/checkout@master 92 | - name: Use Node.js 16 93 | uses: actions/setup-node@v1 94 | with: 95 | node-version: "16.x" 96 | registry-url: "https://registry.npmjs.org" 97 | - name: Publish 98 | run: | 99 | yarn --immutable 100 | yarn config set -H 'npmAuthToken' "${{secrets.NPM_TOKEN}}" 101 | yarn npm publish 102 | env: 103 | CI: true 104 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | coverage/ 3 | node_modules/ 4 | temp 5 | *.log 6 | .DS_Store 7 | dist/ 8 | 9 | .yarn/* 10 | !.yarn/patches 11 | !.yarn/releases 12 | !.yarn/plugins 13 | !.yarn/sdks 14 | !.yarn/versions 15 | .pnp.* 16 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true 5 | }, 6 | "files.exclude": { 7 | "**/.git": true, 8 | "**/.svn": true, 9 | "**/.hg": true, 10 | "**/CVS": true, 11 | "**/.DS_Store": true, 12 | "**/node_modules": true 13 | }, 14 | "typescript.tsdk": "node_modules/typescript/lib" 15 | } 16 | -------------------------------------------------------------------------------- /.yarn/plugins/@yarnpkg/plugin-postinstall-dev.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | name: "@yarnpkg/plugin-postinstall-dev", 4 | factory: function (require) { 5 | var plugin;(()=>{var t={846:(t,r,e)=>{"use strict";const n=e(81),o=e(782),s=e(682);function c(t,r,e){const c=o(t,r,e),i=n.spawn(c.command,c.args,c.options);return s.hookChildProcess(i,c),i}t.exports=c,t.exports.spawn=c,t.exports.sync=function(t,r,e){const c=o(t,r,e),i=n.spawnSync(c.command,c.args,c.options);return i.error=i.error||s.verifyENOENTSync(i.status,c),i},t.exports._parse=o,t.exports._enoent=s},682:t=>{"use strict";const r="win32"===process.platform;function e(t,r){return Object.assign(new Error(`${r} ${t.command} ENOENT`),{code:"ENOENT",errno:"ENOENT",syscall:`${r} ${t.command}`,path:t.command,spawnargs:t.args})}function n(t,n){return r&&1===t&&!n.file?e(n.original,"spawn"):null}t.exports={hookChildProcess:function(t,e){if(!r)return;const o=t.emit;t.emit=function(r,s){if("exit"===r){const r=n(s,e);if(r)return o.call(t,"error",r)}return o.apply(t,arguments)}},verifyENOENT:n,verifyENOENTSync:function(t,n){return r&&1===t&&!n.file?e(n.original,"spawnSync"):null},notFoundError:e}},782:(t,r,e)=>{"use strict";const n=e(17),o=e(326),s=e(541),c=e(449),i="win32"===process.platform,a=/\.(?:com|exe)$/i,p=/node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;function u(t){if(!i)return t;const r=function(t){t.file=o(t);const r=t.file&&c(t.file);return r?(t.args.unshift(t.file),t.command=r,o(t)):t.file}(t),e=!a.test(r);if(t.options.forceShell||e){const e=p.test(r);t.command=n.normalize(t.command),t.command=s.command(t.command),t.args=t.args.map(t=>s.argument(t,e));const o=[t.command].concat(t.args).join(" ");t.args=["/d","/s","/c",`"${o}"`],t.command=process.env.comspec||"cmd.exe",t.options.windowsVerbatimArguments=!0}return t}t.exports=function(t,r,e){r&&!Array.isArray(r)&&(e=r,r=null);const n={command:t,args:r=r?r.slice(0):[],options:e=Object.assign({},e),file:void 0,original:{command:t,args:r}};return e.shell?n:u(n)}},541:t=>{"use strict";const r=/([()\][%!^"`<>&|;, *?])/g;t.exports.command=function(t){return t=t.replace(r,"^$1")},t.exports.argument=function(t,e){return t=(t=`"${t=(t=(t=""+t).replace(/(\\*)"/g,'$1$1\\"')).replace(/(\\*)$/,"$1$1")}"`).replace(r,"^$1"),e&&(t=t.replace(r,"^$1")),t}},449:(t,r,e)=>{"use strict";const n=e(147),o=e(104);t.exports=function(t){const r=Buffer.alloc(150);let e;try{e=n.openSync(t,"r"),n.readSync(e,r,0,150,0),n.closeSync(e)}catch(t){}return o(r.toString())}},326:(t,r,e)=>{"use strict";const n=e(17),o=e(658),s=e(687);function c(t,r){const e=t.options.env||process.env,c=process.cwd(),i=null!=t.options.cwd,a=i&&void 0!==process.chdir&&!process.chdir.disabled;if(a)try{process.chdir(t.options.cwd)}catch(t){}let p;try{p=o.sync(t.command,{path:e[s({env:e})],pathExt:r?n.delimiter:void 0})}catch(t){}finally{a&&process.chdir(c)}return p&&(p=n.resolve(i?t.options.cwd:"",p)),p}t.exports=function(t){return c(t)||c(t,!0)}},768:(t,r,e)=>{var n;e(147);function o(t,r,e){if("function"==typeof r&&(e=r,r={}),!e){if("function"!=typeof Promise)throw new TypeError("callback not provided");return new Promise((function(e,n){o(t,r||{},(function(t,r){t?n(t):e(r)}))}))}n(t,r||{},(function(t,n){t&&("EACCES"===t.code||r&&r.ignoreErrors)&&(t=null,n=!1),e(t,n)}))}n="win32"===process.platform||global.TESTING_WINDOWS?e(73):e(721),t.exports=o,o.sync=function(t,r){try{return n.sync(t,r||{})}catch(t){if(r&&r.ignoreErrors||"EACCES"===t.code)return!1;throw t}}},721:(t,r,e)=>{t.exports=o,o.sync=function(t,r){return s(n.statSync(t),r)};var n=e(147);function o(t,r,e){n.stat(t,(function(t,n){e(t,!t&&s(n,r))}))}function s(t,r){return t.isFile()&&function(t,r){var e=t.mode,n=t.uid,o=t.gid,s=void 0!==r.uid?r.uid:process.getuid&&process.getuid(),c=void 0!==r.gid?r.gid:process.getgid&&process.getgid(),i=parseInt("100",8),a=parseInt("010",8),p=parseInt("001",8),u=i|a;return e&p||e&a&&o===c||e&i&&n===s||e&u&&0===s}(t,r)}},73:(t,r,e)=>{t.exports=s,s.sync=function(t,r){return o(n.statSync(t),t,r)};var n=e(147);function o(t,r,e){return!(!t.isSymbolicLink()&&!t.isFile())&&function(t,r){var e=void 0!==r.pathExt?r.pathExt:process.env.PATHEXT;if(!e)return!0;if(-1!==(e=e.split(";")).indexOf(""))return!0;for(var n=0;n{"use strict";const r=(t={})=>{const r=t.env||process.env;return"win32"!==(t.platform||process.platform)?"PATH":Object.keys(r).reverse().find(t=>"PATH"===t.toUpperCase())||"Path"};t.exports=r,t.exports.default=r},104:(t,r,e)=>{"use strict";const n=e(367);t.exports=(t="")=>{const r=t.match(n);if(!r)return null;const[e,o]=r[0].replace(/#! ?/,"").split(" "),s=e.split("/").pop();return"env"===s?o:o?`${s} ${o}`:s}},367:t=>{"use strict";t.exports=/^#!(.*)/},658:(t,r,e)=>{const n="win32"===process.platform||"cygwin"===process.env.OSTYPE||"msys"===process.env.OSTYPE,o=e(17),s=n?";":":",c=e(768),i=t=>Object.assign(new Error("not found: "+t),{code:"ENOENT"}),a=(t,r)=>{const e=r.colon||s,o=t.match(/\//)||n&&t.match(/\\/)?[""]:[...n?[process.cwd()]:[],...(r.path||process.env.PATH||"").split(e)],c=n?r.pathExt||process.env.PATHEXT||".EXE;.CMD;.BAT;.COM":"",i=n?c.split(e):[""];return n&&-1!==t.indexOf(".")&&""!==i[0]&&i.unshift(""),{pathEnv:o,pathExt:i,pathExtExe:c}},p=(t,r,e)=>{"function"==typeof r&&(e=r,r={}),r||(r={});const{pathEnv:n,pathExt:s,pathExtExe:p}=a(t,r),u=[],l=e=>new Promise((s,c)=>{if(e===n.length)return r.all&&u.length?s(u):c(i(t));const a=n[e],p=/^".*"$/.test(a)?a.slice(1,-1):a,l=o.join(p,t),d=!p&&/^\.[\\\/]/.test(t)?t.slice(0,2)+l:l;s(f(d,e,0))}),f=(t,e,n)=>new Promise((o,i)=>{if(n===s.length)return o(l(e+1));const a=s[n];c(t+a,{pathExt:p},(s,c)=>{if(!s&&c){if(!r.all)return o(t+a);u.push(t+a)}return o(f(t,e,n+1))})});return e?l(0).then(t=>e(null,t),e):l(0)};t.exports=p,p.sync=(t,r)=>{r=r||{};const{pathEnv:e,pathExt:n,pathExtExe:s}=a(t,r),p=[];for(let i=0;i{"use strict";t.exports=require("child_process")},147:t=>{"use strict";t.exports=require("fs")},17:t=>{"use strict";t.exports=require("path")}},r={};function e(n){var o=r[n];if(void 0!==o)return o.exports;var s=r[n]={exports:{}};return t[n](s,s.exports,e),s.exports}e.n=t=>{var r=t&&t.__esModule?()=>t.default:()=>t;return e.d(r,{a:r}),r},e.d=(t,r)=>{for(var n in r)e.o(r,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:r[n]})},e.o=(t,r)=>Object.prototype.hasOwnProperty.call(t,r),e.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var n={};(()=>{"use strict";e.r(n),e.d(n,{default:()=>r});var t=e(846);const r={hooks:{async afterAllInstalled(r){await new Promise(e=>{const n=(0,t.spawn)("yarn",["run","postinstallDev"],{cwd:r.cwd});n.stdout.pipe(process.stdout),n.stderr.pipe(process.stderr),n.addListener("exit",()=>e())})}}}})(),plugin=n})(); 6 | return plugin; 7 | } 8 | }; -------------------------------------------------------------------------------- /.yarn/versions/5de4ad7d.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freshollie/jest-dynalite/8f69405625becb8fe36eda0bb6175efa60e0a365/.yarn/versions/5de4ad7d.yml -------------------------------------------------------------------------------- /.yarn/versions/8c36e5ca.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freshollie/jest-dynalite/8f69405625becb8fe36eda0bb6175efa60e0a365/.yarn/versions/8c36e5ca.yml -------------------------------------------------------------------------------- /.yarn/versions/a06bc2e2.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freshollie/jest-dynalite/8f69405625becb8fe36eda0bb6175efa60e0a365/.yarn/versions/a06bc2e2.yml -------------------------------------------------------------------------------- /.yarn/versions/a857885b.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freshollie/jest-dynalite/8f69405625becb8fe36eda0bb6175efa60e0a365/.yarn/versions/a857885b.yml -------------------------------------------------------------------------------- /.yarn/versions/ab14bd23.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freshollie/jest-dynalite/8f69405625becb8fe36eda0bb6175efa60e0a365/.yarn/versions/ab14bd23.yml -------------------------------------------------------------------------------- /.yarn/versions/ada3831a.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freshollie/jest-dynalite/8f69405625becb8fe36eda0bb6175efa60e0a365/.yarn/versions/ada3831a.yml -------------------------------------------------------------------------------- /.yarn/versions/adb1294a.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freshollie/jest-dynalite/8f69405625becb8fe36eda0bb6175efa60e0a365/.yarn/versions/adb1294a.yml -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-version.cjs 5 | spec: "@yarnpkg/plugin-version" 6 | - path: .yarn/plugins/@yarnpkg/plugin-postinstall-dev.cjs 7 | spec: "https://raw.githubusercontent.com/sachinraja/yarn-plugin-postinstall-dev/main/bundles/%40yarnpkg/plugin-postinstall-dev.js" 8 | 9 | yarnPath: .yarn/releases/yarn-berry.cjs 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Gemshelf Inc. (shelf.io) & Oliver Bell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jest-dynalite 2 | 3 | [![Pipeline status](https://github.com/freshollie/jest-dynalite/workflows/Pipeline/badge.svg)](https://github.com/freshollie/jest-dynalite/actions) 4 | [![Coverage Status](https://coveralls.io/repos/github/freshollie/jest-dynalite/badge.svg?branch=master)](https://coveralls.io/github/freshollie/jest-dynalite?branch=master) 5 | [![Npm version](https://img.shields.io/npm/v/jest-dynalite)](https://www.npmjs.com/package/jest-dynalite) 6 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 7 | 8 | > Enchaned unit testing, with a mock DynamoDB instance 9 | 10 | `jest-dynalite` is a fork of [@shelf/jest-dynamodb](https://github.com/shelfio/jest-dynamodb) that allows unit tests to execute real 11 | queries against a local DynamoDB instance. It was created in an attempt to address some of the most important missing 12 | features of `@shelf/jest-dynamodb`, such as requiring all your tests to use a single shared database. See [this issue](https://github.com/shelfio/jest-dynamodb/issues/55) for more motivation. 13 | 14 | ## Why should I use this? 15 | 16 | Using this `jest-dynalite` makes writing queries with DynamoDB very easy, your tests can really 17 | check if your data is manipulated in the way you expect it to be. This means that queries and mutations 18 | can be developed without ever having to deploy or run your application, and significantly speeds up 19 | writing code which interacts with DynamoDB. 20 | 21 | This in turn makes your tests much more robust, because a change to a data structure or 22 | db query in your application will be reflected by failing tests, instead of using mocks to check 23 | if calls were made correctly. 24 | 25 | This library could almost be seen as an integration test, but without the overhead of typical integration tests. 26 | 27 | ## Features 28 | 29 | - Optionally clear tables between tests 30 | - Isolated tables between test runners 31 | - Ability to specify config directory 32 | - No `java` requirement 33 | - Works with both `@aws-sdk/client-dynamodb` and `aws-sdk` 34 | 35 | ## **BREAKING CHANGES** 36 | 37 | From `v2.0.0` `jest-dynalite` now uses a JavaScript file for table configuration. This change makes it possible to set the dynalite config programatically (enabling things such as reading the parameters from a cloudformation template) while also improving compatibility with jest-dynamodb. Thanks to [@corollari](https://github.com/corollari) for this change. 38 | 39 | From `v3.0.0` you can now use the preset in a monorepo. The `jest-dynalite-config.js` will be picked up from your jest ``, which should be the same directory as your jest config. 40 | 41 | ### `@aws-sdk/client-dynamodb` 42 | 43 | With the release of `v3.3.0` it is now possible to use `@aws-sdk/client-dynamodb` instead of `aws-sdk`. 44 | 45 | However, it seems that with this new version the dynamodb client connection stays active for a few seconds after your tests have finished and thus stops `dynalite` from being able to teardown after each test suite (test file). 46 | 47 | Make sure you run `client.destroy()` on your client after every test suite to mitigate this issue. See an example [here](#Update-your-sourcecode) 48 | 49 | ## Installation 50 | 51 | ```bash 52 | $ yarn add jest-dynalite -D 53 | ``` 54 | 55 | (Make sure you have `@aws-sdk/client-dynamodb` or `aws-sdk` also installed) 56 | 57 | ## Examples 58 | 59 | Please follow the [below config](#config) to setup your tests to use `jest-dynalite`. However, if you are looking for 60 | some example project structures, please see the [examples](https://github.com/freshollie/jest-dynalite/tree/master/e2e). 61 | 62 | ## Timeouts 63 | 64 | Because jest has a default timeout of 5000ms per test, `jest-dynalite` can sometimes cause failures due to the timeout 65 | being exceeded. This can happen when there are many tests or lots of tables to create between tests. 66 | 67 | If this happens, try increasing your test timeouts `jest.setTimeout(10000)`. Another option is to selectively 68 | run the database only for suites which use it. Please see [advanced config](###Advanced-setup). 69 | 70 | ## Config 71 | 72 | In your jest project root (next to your `jest.config.js`), create a `jest-dynalite-config.js` (or `.cjs` or `.ts`) with the tables schemas, 73 | and an optional `basePort` to run dynalite on: 74 | 75 | ```js 76 | // use export default for ts based configs 77 | module.exports = { 78 | tables: [ 79 | { 80 | TableName: "table", 81 | KeySchema: [{ AttributeName: "id", KeyType: "HASH" }], 82 | AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }], 83 | ProvisionedThroughput: { 84 | ReadCapacityUnits: 1, 85 | WriteCapacityUnits: 1, 86 | }, 87 | }, 88 | ], 89 | basePort: 8000, 90 | }; 91 | ``` 92 | 93 | Some data can be given to exist in the table before each test: 94 | 95 | ```js 96 | module.exports = { 97 | tables: [ 98 | { 99 | TableName: "table", 100 | KeySchema: [{ AttributeName: "id", KeyType: "HASH" }], 101 | AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }], 102 | ProvisionedThroughput: { 103 | ReadCapacityUnits: 1, 104 | WriteCapacityUnits: 1, 105 | }, 106 | data: [ 107 | { 108 | id: "a", 109 | someattribute: "hello world", 110 | }, 111 | ], 112 | }, 113 | ], 114 | basePort: 8000, 115 | }; 116 | ``` 117 | 118 | Your tables can also be resolved from an optionally async function: 119 | 120 | ```js 121 | module.exports = { 122 | // Please note, this function is resolved 123 | // once per test file 124 | tables: async () => { 125 | const myTables = await someFunction(); 126 | if (myTables.find((table) => ...)) { 127 | return someOtherFunction(); 128 | } 129 | return myTables; 130 | }, 131 | basePort: 8000 132 | }; 133 | ``` 134 | 135 | ## Update your sourcecode 136 | 137 | ```javascript 138 | const client = new DynamoDB({ 139 | ...yourConfig, 140 | ...(process.env.MOCK_DYNAMODB_ENDPOINT && { 141 | endpoint: process.env.MOCK_DYNAMODB_ENDPOINT, 142 | sslEnabled: false, 143 | region: "local", 144 | }), 145 | }); 146 | ``` 147 | 148 | `process.env.MOCK_DYNAMODB_ENDPOINT` is unqiue to each test runner. 149 | 150 | After all your tests, make sure you destroy your client. 151 | You can even do this by adding an `afterAll` in a [`setupFilesAfterEnv`](https://jestjs.io/docs/en/configuration#setupfilesafterenv-array) file. 152 | 153 | ```javascript 154 | afterAll(() => { 155 | client.destroy(); 156 | }); 157 | ``` 158 | 159 | ## Jest config 160 | 161 | ### Simple usage (preset) 162 | 163 | jest.config.js 164 | 165 | ```javascript 166 | module.exports = { 167 | ... 168 | preset: "jest-dynalite" 169 | } 170 | ``` 171 | 172 | The simple preset config will use the config and clear tables 173 | between tests. 174 | 175 | **Important**: Only use this option if you don't have a custom `testEnvironment` set in your `jest.config.js` file. 176 | 177 | [Please see example](example/) 178 | 179 | ### Advanced setup 180 | 181 | If you are using your own `testEnvironment` in your Jest configuration, then you must setup 182 | `jest-dynalite` manually. You should also use this manual configuration if you don't want a DynamoDB mock to run 183 | for all your tests (faster). 184 | 185 | setupBeforeEnv.js 186 | 187 | ```javascript 188 | import { setup } from "jest-dynalite"; 189 | 190 | // You must give it a config directory 191 | setup(__dirname); 192 | ``` 193 | 194 | In every test suite where you are using DynamoDB, apply `import "jest-dynalite/withDb"` to the top of 195 | that test suite to run the db for all the tests in the suite. 196 | 197 | If you want the tables to exist for all your suites, create a 198 | `setupAfterEnv.js` file with the content: 199 | 200 | ```javascript 201 | import "jest-dynalite/withDb"; 202 | ``` 203 | 204 | You then must add the setup files to your jest config 205 | 206 | jest.config.js 207 | 208 | ```javascript 209 | module.exports = { 210 | ... 211 | setupFiles: ["./setupBeforeEnv.js"], 212 | setupFilesAfterEnv: ["./setupAfterEnv.js"] 213 | } 214 | ``` 215 | 216 | If you want to be even more granular, you can start 217 | the db yourself at any point. 218 | 219 | ```javascript 220 | import { startDb, stopDb, createTables, deleteTables } from "jest-dynalite"; 221 | 222 | beforeAll(startDb); 223 | 224 | // Create tables but don't delete them after tests 225 | beforeAll(createTables); 226 | 227 | // or 228 | beforeEach(createTables); 229 | afterEach(deleteTables); 230 | 231 | afterAll(stopDb); 232 | ``` 233 | 234 | ### Other options 235 | 236 | jest.config.js 237 | 238 | ```javascript 239 | module.exports = { 240 | ... 241 | testEnvironment: "jest-dynalite/environment", 242 | 243 | setupFilesAfterEnv: [ 244 | "jest-dynalite/setupTables", 245 | // Optional (but recommended) 246 | "jest-dynalite/clearAfterEach" 247 | ] 248 | } 249 | ``` 250 | 251 | This setup should be used if you want to override the default config of `clearAfterEach`, but still want to use the most simple configuration. 252 | 253 | #### One dynalite instance 254 | 255 | If you want to start & setup the db **only** once for all your suites, 256 | create a `setup.js` and `teardown.js` files with the following content: 257 | 258 | ```javascript 259 | // setup.js 260 | 261 | import { startDb, createTables, setup } from "jest-dynalite"; 262 | 263 | module.exports = async () => { 264 | // You must provide a config directory 265 | setup(__dirname); 266 | await startDb(); 267 | await createTables(); 268 | }; 269 | ``` 270 | 271 | ```javascript 272 | // teardown.js 273 | 274 | import { stopDb, deleteTables } from "jest-dynalite"; 275 | 276 | module.exports = async () => { 277 | // Cleanup after tests 278 | await deleteTables(); 279 | await stopDb(); 280 | }; 281 | ``` 282 | 283 | You then must add the setup files to your jest config 284 | 285 | jest.config.js 286 | 287 | ```javascript 288 | module.exports = { 289 | ... 290 | globalSetup: ["./setup.js"], 291 | globalTeardown: ["./teardown.js"], 292 | } 293 | ``` 294 | 295 | **IMPORTANT NOTE** 296 | 297 | Be aware that the only one instance of dynalite will start, which may cause test issues if multiple runners are editing the same data. 298 | 299 | ## Development 300 | 301 | Clone the repo and install dependencies 302 | 303 | ``` 304 | yarn 305 | ``` 306 | 307 | Run tests 308 | 309 | ``` 310 | yarn test 311 | ``` 312 | 313 | ### Tests 314 | 315 | Tests are designed as a mix of unit, integration tests, and e2e tests. 316 | 317 | `yarn test` will run all unit and integration tests 318 | 319 | Integration tests are configured under the `tests` directory, with 320 | [jest projects](https://jestjs.io/docs/en/configuration#projects-arraystring--projectconfig) used to managed 321 | testing different configurations for jest-dynalite. 322 | 323 | `yarn e2e` will run e2e tests 324 | 325 | ## License 326 | 327 | `MIT` 328 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /clearAfterEach.js: -------------------------------------------------------------------------------- 1 | require("./dist/clearAfterEach"); 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /e2e/global-setup/globalSetup.js: -------------------------------------------------------------------------------- 1 | const { startDb, createTables, setup } = require("../.."); 2 | 3 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 4 | module.exports = async () => { 5 | setup(__dirname); 6 | await startDb(); 7 | await createTables(); 8 | }; 9 | -------------------------------------------------------------------------------- /e2e/global-setup/globalTeardown.js: -------------------------------------------------------------------------------- 1 | const { stopDb, deleteTables } = require("../.."); 2 | 3 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 4 | module.exports = async () => { 5 | await deleteTables(); 6 | await stopDb(); 7 | }; 8 | -------------------------------------------------------------------------------- /e2e/global-setup/jest-dynalite-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tables: [ 3 | { 4 | TableName: "keys", 5 | KeySchema: [{ AttributeName: "id", KeyType: "HASH" }], 6 | AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }], 7 | ProvisionedThroughput: { 8 | ReadCapacityUnits: 1, 9 | WriteCapacityUnits: 1, 10 | }, 11 | data: [ 12 | { 13 | id: "50", 14 | value: { name: "already exists" }, 15 | }, 16 | ], 17 | }, 18 | ], 19 | basePort: 8001, 20 | }; 21 | -------------------------------------------------------------------------------- /e2e/global-setup/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globalSetup: `./globalSetup.js`, 3 | globalTeardown: `./globalTeardown.js`, 4 | displayName: { 5 | name: "global-setup", 6 | color: "blue", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /e2e/global-setup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-dynalite-e2e-global-setup", 3 | "private": true, 4 | "scripts": { 5 | "test": "jest" 6 | }, 7 | "devDependencies": { 8 | "jest": "^28.0.2", 9 | "jest-dynalite": "portal:../../" 10 | }, 11 | "dependencies": { 12 | "aws-sdk": "^2.971.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /e2e/global-setup/src/keystore.js: -------------------------------------------------------------------------------- 1 | const { DocumentClient } = require("aws-sdk/clients/dynamodb"); 2 | 3 | const ddb = new DocumentClient({ 4 | convertEmptyValues: true, 5 | endpoint: process.env.MOCK_DYNAMODB_ENDPOINT, 6 | sslEnabled: false, 7 | region: "local", 8 | }); 9 | 10 | module.exports = { 11 | getItem: async (byId) => { 12 | const { Item } = await ddb 13 | .get({ TableName: "keys", Key: { id: byId } }) 14 | .promise(); 15 | 16 | return Item && Item.value; 17 | }, 18 | putItem: (id, value) => 19 | ddb.put({ TableName: "keys", Item: { id, value } }).promise(), 20 | }; 21 | -------------------------------------------------------------------------------- /e2e/global-setup/src/keystore.test.js: -------------------------------------------------------------------------------- 1 | const keystore = require("./keystore"); 2 | 3 | describe("keystore", () => { 4 | it("should allow items to be placed in the store", async () => { 5 | await Promise.all([ 6 | keystore.putItem("1", { name: "value" }), 7 | keystore.putItem("2", { name: "another value" }), 8 | ]); 9 | 10 | expect(await keystore.getItem("1")).toEqual({ name: "value" }); 11 | expect(await keystore.getItem("2")).toEqual({ name: "another value" }); 12 | }); 13 | 14 | it("should handle no value for key", async () => { 15 | expect(await keystore.getItem("a")).toBeUndefined(); 16 | }); 17 | 18 | it("should contain the existing key from example data", async () => { 19 | expect(await keystore.getItem("50")).toEqual({ name: "already exists" }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /e2e/jest-27/jest-dynalite-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tables: [ 3 | { 4 | TableName: "keys", 5 | KeySchema: [{ AttributeName: "id", KeyType: "HASH" }], 6 | AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }], 7 | ProvisionedThroughput: { 8 | ReadCapacityUnits: 1, 9 | WriteCapacityUnits: 1, 10 | }, 11 | data: [ 12 | { 13 | id: "50", 14 | value: { name: "already exists" }, 15 | }, 16 | ], 17 | }, 18 | ], 19 | basePort: 8000, 20 | }; 21 | -------------------------------------------------------------------------------- /e2e/jest-27/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "jest-dynalite", 3 | displayName: { 4 | name: "preset", 5 | color: "yellow", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /e2e/jest-27/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-dynalite-e2e-jest-27", 3 | "private": true, 4 | "scripts": { 5 | "test": "jest" 6 | }, 7 | "devDependencies": { 8 | "jest": "^27.0.2", 9 | "jest-dynalite": "portal:../../" 10 | }, 11 | "dependencies": { 12 | "aws-sdk": "^2.971.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /e2e/jest-27/src/keystore.js: -------------------------------------------------------------------------------- 1 | const { DocumentClient } = require("aws-sdk/clients/dynamodb"); 2 | 3 | const ddb = new DocumentClient({ 4 | convertEmptyValues: true, 5 | endpoint: process.env.MOCK_DYNAMODB_ENDPOINT, 6 | sslEnabled: false, 7 | region: "local", 8 | }); 9 | 10 | module.exports = { 11 | getItem: async (byId) => { 12 | const { Item } = await ddb 13 | .get({ TableName: "keys", Key: { id: byId } }) 14 | .promise(); 15 | 16 | return Item && Item.value; 17 | }, 18 | putItem: (id, value) => 19 | ddb.put({ TableName: "keys", Item: { id, value } }).promise(), 20 | }; 21 | -------------------------------------------------------------------------------- /e2e/jest-27/src/keystore.test.js: -------------------------------------------------------------------------------- 1 | const keystore = require("./keystore"); 2 | 3 | describe("keystore", () => { 4 | it("should allow items to be placed in the store", async () => { 5 | await Promise.all([ 6 | keystore.putItem("1", { name: "value" }), 7 | keystore.putItem("2", { name: "another value" }), 8 | ]); 9 | 10 | expect(await keystore.getItem("1")).toEqual({ name: "value" }); 11 | expect(await keystore.getItem("2")).toEqual({ name: "another value" }); 12 | }); 13 | 14 | it("should handle no value for key", async () => { 15 | expect(await keystore.getItem("a")).toBeUndefined(); 16 | }); 17 | 18 | it("should contain the existing key from example data", async () => { 19 | expect(await keystore.getItem("50")).toEqual({ name: "already exists" }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /e2e/monorepo/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: ["/packages/*/jest.config.js"], 3 | }; 4 | -------------------------------------------------------------------------------- /e2e/monorepo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-dynalite-e2e-monorepo", 3 | "private": true, 4 | "workspaces": [ 5 | "./packages/*" 6 | ], 7 | "scripts": { 8 | "test": "jest" 9 | }, 10 | "devDependencies": { 11 | "jest": "^28.0.2" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /e2e/monorepo/packages/keystore/jest-dynalite-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tables: [ 3 | { 4 | TableName: "keys", 5 | KeySchema: [{ AttributeName: "id", KeyType: "HASH" }], 6 | AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }], 7 | ProvisionedThroughput: { 8 | ReadCapacityUnits: 1, 9 | WriteCapacityUnits: 1, 10 | }, 11 | data: [ 12 | { 13 | id: "50", 14 | value: { name: "already exists" }, 15 | }, 16 | ], 17 | }, 18 | ], 19 | basePort: 8000, 20 | }; 21 | -------------------------------------------------------------------------------- /e2e/monorepo/packages/keystore/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "jest-dynalite", 3 | displayName: { 4 | name: "monorepo", 5 | color: "red", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /e2e/monorepo/packages/keystore/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo-package", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "jest" 7 | }, 8 | "devDependencies": { 9 | "jest-dynalite": "portal:../../../../" 10 | }, 11 | "dependencies": { 12 | "aws-sdk": "^2.971.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /e2e/monorepo/packages/keystore/src/keystore.js: -------------------------------------------------------------------------------- 1 | const { DocumentClient } = require("aws-sdk/clients/dynamodb"); 2 | 3 | const ddb = new DocumentClient({ 4 | convertEmptyValues: true, 5 | endpoint: process.env.MOCK_DYNAMODB_ENDPOINT, 6 | sslEnabled: false, 7 | region: "local", 8 | }); 9 | 10 | module.exports = { 11 | getItem: async (byId) => { 12 | const { Item } = await ddb 13 | .get({ TableName: "keys", Key: { id: byId } }) 14 | .promise(); 15 | 16 | return Item && Item.value; 17 | }, 18 | putItem: (id, value) => 19 | ddb.put({ TableName: "keys", Item: { id, value } }).promise(), 20 | }; 21 | -------------------------------------------------------------------------------- /e2e/monorepo/packages/keystore/src/keystore.test.js: -------------------------------------------------------------------------------- 1 | const keystore = require("./keystore"); 2 | 3 | describe("keystore", () => { 4 | it("should allow items to be placed in the store", async () => { 5 | await Promise.all([ 6 | keystore.putItem("1", { name: "value" }), 7 | keystore.putItem("2", { name: "another value" }), 8 | ]); 9 | 10 | expect(await keystore.getItem("1")).toEqual({ name: "value" }); 11 | expect(await keystore.getItem("2")).toEqual({ name: "another value" }); 12 | }); 13 | 14 | it("should handle no value for key", async () => { 15 | expect(await keystore.getItem("a")).toBeUndefined(); 16 | }); 17 | 18 | it("should contain the existing key from example data", async () => { 19 | expect(await keystore.getItem("50")).toEqual({ name: "already exists" }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /e2e/preset/jest-dynalite-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tables: [ 3 | { 4 | TableName: "keys", 5 | KeySchema: [{ AttributeName: "id", KeyType: "HASH" }], 6 | AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }], 7 | ProvisionedThroughput: { 8 | ReadCapacityUnits: 1, 9 | WriteCapacityUnits: 1, 10 | }, 11 | data: [ 12 | { 13 | id: "50", 14 | value: { name: "already exists" }, 15 | }, 16 | ], 17 | }, 18 | ], 19 | basePort: 8000, 20 | }; 21 | -------------------------------------------------------------------------------- /e2e/preset/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "jest-dynalite", 3 | displayName: { 4 | name: "preset", 5 | color: "yellow", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /e2e/preset/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-dynalite-e2e-preset", 3 | "private": true, 4 | "scripts": { 5 | "test": "jest" 6 | }, 7 | "devDependencies": { 8 | "jest": "^28.0.2", 9 | "jest-dynalite": "portal:../../" 10 | }, 11 | "dependencies": { 12 | "aws-sdk": "^2.971.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /e2e/preset/src/keystore.js: -------------------------------------------------------------------------------- 1 | const { DocumentClient } = require("aws-sdk/clients/dynamodb"); 2 | 3 | const ddb = new DocumentClient({ 4 | convertEmptyValues: true, 5 | endpoint: process.env.MOCK_DYNAMODB_ENDPOINT, 6 | sslEnabled: false, 7 | region: "local", 8 | }); 9 | 10 | module.exports = { 11 | getItem: async (byId) => { 12 | const { Item } = await ddb 13 | .get({ TableName: "keys", Key: { id: byId } }) 14 | .promise(); 15 | 16 | return Item && Item.value; 17 | }, 18 | putItem: (id, value) => 19 | ddb.put({ TableName: "keys", Item: { id, value } }).promise(), 20 | }; 21 | -------------------------------------------------------------------------------- /e2e/preset/src/keystore.test.js: -------------------------------------------------------------------------------- 1 | const keystore = require("./keystore"); 2 | 3 | describe("keystore", () => { 4 | it("should allow items to be placed in the store", async () => { 5 | await Promise.all([ 6 | keystore.putItem("1", { name: "value" }), 7 | keystore.putItem("2", { name: "another value" }), 8 | ]); 9 | 10 | expect(await keystore.getItem("1")).toEqual({ name: "value" }); 11 | expect(await keystore.getItem("2")).toEqual({ name: "another value" }); 12 | }); 13 | 14 | it("should handle no value for key", async () => { 15 | expect(await keystore.getItem("a")).toBeUndefined(); 16 | }); 17 | 18 | it("should contain the existing key from example data", async () => { 19 | expect(await keystore.getItem("50")).toEqual({ name: "already exists" }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /environment.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./dist/environment"); 2 | -------------------------------------------------------------------------------- /jest-preset.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | 3 | module.exports = { 4 | setupFilesAfterEnv: [ 5 | resolve(__dirname, "./setupTables.js"), 6 | resolve(__dirname, "./clearAfterEach.js"), 7 | ], 8 | testEnvironment: resolve(__dirname, "./environment.js"), 9 | }; 10 | -------------------------------------------------------------------------------- /jest.base.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | rootDir: __dirname, 3 | coveragePathIgnorePatterns: ["/tests/", "/__testdir__/"], 4 | collectCoverageFrom: ["/src/**/*.ts", "!/**/*.js"], 5 | testPathIgnorePatterns: ["/node_modules/", "/e2e/", "/src/"], 6 | }; 7 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | projects: ["./tests/jest-*.config.ts", "./jest.unit.config.ts"], 3 | }; 4 | -------------------------------------------------------------------------------- /jest.unit.config.ts: -------------------------------------------------------------------------------- 1 | import base from "./jest.base"; 2 | 3 | export default { 4 | ...base, 5 | testPathIgnorePatterns: ["/node_modules/", "/tests/", "/e2e/"], 6 | displayName: { 7 | name: "unit", 8 | color: "white", 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-dynalite", 3 | "version": "3.6.1", 4 | "description": "Run your tests using Jest & Dynalite", 5 | "license": "MIT", 6 | "repository": "https://github.com/freshollie/jest-dynalite", 7 | "main": "dist/index.js", 8 | "typings": "dist/index.d.ts", 9 | "author": { 10 | "name": "Oliver Bell", 11 | "email": "freshollie@gmail.com", 12 | "url": "https://obell.dev" 13 | }, 14 | "engines": { 15 | "node": ">=8" 16 | }, 17 | "workspaces": [ 18 | "./e2e/*" 19 | ], 20 | "scripts": { 21 | "lint": "yarn build && eslint --ext js,jsx,ts,tsx .", 22 | "test": "jest", 23 | "test:types": "tsc --noEmit", 24 | "e2e": "echo 'Testing simple preset'&& cd e2e/preset && yarn test && echo 'Testing global setup' && cd ../global-setup && yarn test && echo 'Testing simple preset in monorepo' && cd ../monorepo && yarn test && echo 'Testing jest <=27' && cd ../jest-27 && yarn test", 25 | "build": "rm -rf dist && tsc -p tsconfig.build.json", 26 | "postinstallDev": "yarn build && husky install", 27 | "prepack": "yarn build" 28 | }, 29 | "files": [ 30 | "dist", 31 | "types", 32 | "withDb.js", 33 | "environment.js", 34 | "clearAfterEach.js", 35 | "setupTables.js", 36 | "jest-preset.js" 37 | ], 38 | "keywords": [ 39 | "jest", 40 | "dynamodb", 41 | "dynamodb local", 42 | "dynalite", 43 | "jest preset", 44 | "jest environment" 45 | ], 46 | "dependencies": { 47 | "@aws/dynamodb-auto-marshaller": "^0.7.1", 48 | "dynalite": "^3.2.1", 49 | "setimmediate": "^1.0.5" 50 | }, 51 | "peerDependencies": { 52 | "@aws-sdk/client-dynamodb": ">=3", 53 | "aws-sdk": "2.x.x", 54 | "jest": ">=20" 55 | }, 56 | "peerDependenciesMeta": { 57 | "@aws-sdk/client-dynamodb": { 58 | "optional": true 59 | }, 60 | "aws-sdk": { 61 | "optional": true 62 | } 63 | }, 64 | "devDependencies": { 65 | "@aws-sdk/client-dynamodb": "^3.26.0", 66 | "@babel/core": "^7.17.9", 67 | "@babel/preset-env": "^7.16.11", 68 | "@babel/preset-typescript": "^7.16.7", 69 | "@commitlint/cli": "^13.1.0", 70 | "@commitlint/config-conventional": "^13.1.0", 71 | "@jest/environment": "^28.0.2", 72 | "@types/jest": "^27.4.1", 73 | "@typescript-eslint/eslint-plugin": "^4.29.2", 74 | "@typescript-eslint/parser": "^4.29.2", 75 | "aws-sdk": "^2.971.0", 76 | "babel-jest": "^28.0.2", 77 | "commitlint": "^13.1.0", 78 | "eslint": "^7.32.0", 79 | "eslint-config-airbnb-base": "^14.2.1", 80 | "eslint-config-airbnb-typescript": "^12.3.1", 81 | "eslint-config-prettier": "^8.3.0", 82 | "eslint-plugin-import": "^2.24.0", 83 | "eslint-plugin-jest": "^24.4.0", 84 | "eslint-plugin-prettier": "^3.4.0", 85 | "husky": "^7.0.1", 86 | "jest": "^28.0.2", 87 | "jest-environment-node": "^28.0.2", 88 | "lint-staged": "^11.1.2", 89 | "prettier": "^2.3.2", 90 | "ts-node": "^10.7.0", 91 | "typescript": "^4.6.3" 92 | }, 93 | "lint-staged": { 94 | "*.{js,ts}": [ 95 | "eslint --fix" 96 | ], 97 | "*.{html,json,md,yml}": [ 98 | "prettier --write" 99 | ] 100 | }, 101 | "publishConfig": { 102 | "access": "public" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /setupTables.js: -------------------------------------------------------------------------------- 1 | require("./dist/setupTables"); 2 | -------------------------------------------------------------------------------- /src/__snapshots__/environment.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Environment should throw an error if config file could not be located 1`] = ` 4 | " 5 | jest-dynalite could not find \\"jest-dynalite-config.js\\" or \\"jest-dynalite-config.cjs\\" or \\"jest-dynalite-config.ts\\" in the jest (somebaddirectory). 6 | 7 | If you didn't intend to be using this directory for the config, please specify a custom 8 | directory: https://github.com/freshollie/jest-dynalite/#advanced-setup 9 | 10 | If you are already using a custom config directory, you should apply 'import \\"jest-dynalite/withDb\\"' 11 | to your \\"setupFilesAfterEnv\\" instead of using the preset. 12 | 13 | For more information, please see https://github.com/freshollie/jest-dynalite/#breaking-changes. 14 | " 15 | `; 16 | -------------------------------------------------------------------------------- /src/__testdir__/jest-dynalite-config.js: -------------------------------------------------------------------------------- 1 | // fake config 2 | module.exports = {}; 3 | -------------------------------------------------------------------------------- /src/clearAfterEach.ts: -------------------------------------------------------------------------------- 1 | import { deleteTables, createTables } from "./db"; 2 | 3 | afterEach(async () => { 4 | await deleteTables(); 5 | await createTables(); 6 | }); 7 | -------------------------------------------------------------------------------- /src/config.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { Config } from "./types"; 3 | import { getDynalitePort, setConfigDir } from "./config"; 4 | 5 | const BASE_PORT = 8443; 6 | 7 | const mockedConfig = jest.fn((): Config => ({ basePort: BASE_PORT })); 8 | const configPath = "/fakepath/jest-dynalite-config.js"; 9 | 10 | jest.mock("fs"); 11 | (fs.existsSync as jest.Mock).mockReturnValue(true); 12 | 13 | jest.mock("path", () => { 14 | const original = jest.requireActual("path"); 15 | return { 16 | ...original, 17 | resolve: jest.fn(() => configPath), 18 | }; 19 | }); 20 | 21 | jest.mock(configPath, () => mockedConfig(), { virtual: true }); 22 | 23 | describe("Config", () => { 24 | beforeAll(() => { 25 | setConfigDir("/whatever"); 26 | }); 27 | 28 | test("a different port is returned for each worker", () => { 29 | const expectedPort = 30 | BASE_PORT + parseInt(process.env.JEST_WORKER_ID as string, 10); 31 | 32 | expect(getDynalitePort()).toBe(expectedPort); 33 | }); 34 | 35 | test("should return dynalite port even there is no JEST_WORKER_ID", () => { 36 | const workerId = process.env.JEST_WORKER_ID; 37 | delete process.env.JEST_WORKER_ID; 38 | 39 | const port = getDynalitePort(); 40 | 41 | expect(port).not.toBeNaN(); 42 | expect(port).toBe(BASE_PORT + 1); 43 | 44 | process.env.JEST_WORKER_ID = workerId; 45 | }); 46 | 47 | test("if basePort is not defined then port 8001 will be used as a default", () => { 48 | const workerId = process.env.JEST_WORKER_ID; 49 | delete process.env.JEST_WORKER_ID; 50 | 51 | jest.resetModules(); 52 | mockedConfig.mockReturnValue({}); 53 | 54 | expect(getDynalitePort()).toBe(8001); 55 | 56 | process.env.JEST_WORKER_ID = workerId; 57 | }); 58 | 59 | test("should throw an error if basePort in config file is invalid", () => { 60 | jest.resetModules(); 61 | // @ts-ignore 62 | mockedConfig.mockReturnValue({ basePort: "this is not a number" }); 63 | 64 | expect(getDynalitePort).toThrowError(TypeError); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { resolve } from "path"; 3 | import { Config, TableConfig } from "./types"; 4 | import { isFunction } from "./utils"; 5 | 6 | export const CONFIG_FILE_NAME = "jest-dynalite-config.js"; 7 | export const CONFIG_FILE_NAME_CJS = "jest-dynalite-config.cjs"; 8 | export const CONFIG_FILE_NAME_TS = "jest-dynalite-config.ts"; 9 | 10 | export class NotFoundError extends Error { 11 | constructor(dir: string) { 12 | super( 13 | `Could not find '${CONFIG_FILE_NAME}' or '${CONFIG_FILE_NAME_TS}' in dir ${dir}` 14 | ); 15 | } 16 | } 17 | 18 | if (!process.env.JEST_DYNALITE_CONFIG_DIRECTORY) { 19 | process.env.JEST_DYNALITE_CONFIG_DIRECTORY = process.cwd(); 20 | } 21 | 22 | const findConfigOrError = ( 23 | directory: string 24 | ): 25 | | typeof CONFIG_FILE_NAME 26 | | typeof CONFIG_FILE_NAME_CJS 27 | | typeof CONFIG_FILE_NAME_TS => { 28 | const foundFile = ( 29 | [CONFIG_FILE_NAME, CONFIG_FILE_NAME_CJS, CONFIG_FILE_NAME_TS] as const 30 | ).find((config) => { 31 | const file = resolve(directory, config); 32 | return fs.existsSync(file); 33 | }); 34 | 35 | if (!foundFile) { 36 | throw new NotFoundError(resolve(directory)); 37 | } 38 | 39 | return foundFile; 40 | }; 41 | 42 | const readConfig = (): Config => { 43 | const configFile = findConfigOrError( 44 | process.env.JEST_DYNALITE_CONFIG_DIRECTORY! 45 | ); 46 | const file = resolve(process.env.JEST_DYNALITE_CONFIG_DIRECTORY!, configFile); 47 | 48 | try { 49 | const importedConfig = require(file); // eslint-disable-line import/no-dynamic-require, global-require 50 | if ("default" in importedConfig) { 51 | return importedConfig.default; 52 | } 53 | return importedConfig; 54 | } catch (e) { 55 | throw new Error( 56 | `Something went wrong reading your ${configFile}: ${(e as Error).message}` 57 | ); 58 | } 59 | }; 60 | 61 | export const setConfigDir = (directory: string): void => { 62 | // Only allow this directory to be set if a config exists 63 | findConfigOrError(directory); 64 | process.env.JEST_DYNALITE_CONFIG_DIRECTORY = directory; 65 | }; 66 | 67 | export const getDynalitePort = (): number => { 68 | const { basePort = 8000 } = readConfig(); 69 | if (Number.isInteger(basePort) && basePort > 0 && basePort <= 65535) { 70 | return basePort + parseInt(process.env.JEST_WORKER_ID || "1", 10); 71 | } 72 | 73 | throw new TypeError( 74 | `Option "basePort" must be an number between 1 and 65535. Received "${basePort.toString()}"` 75 | ); 76 | }; 77 | 78 | // Cache the tables result from the config function, so that we 79 | // are not calling it over and over 80 | let tablesCache: TableConfig[] | undefined; 81 | 82 | export const getTables = async (): Promise => { 83 | if (tablesCache) { 84 | return tablesCache; 85 | } 86 | 87 | const tablesConfig = readConfig().tables; 88 | if (isFunction(tablesConfig)) { 89 | tablesCache = await tablesConfig(); 90 | } else { 91 | tablesCache = tablesConfig ?? []; 92 | } 93 | 94 | if (!Array.isArray(tablesCache)) { 95 | throw new Error( 96 | "jest-dynalite requires that the tables configuration is an array" 97 | ); 98 | } 99 | 100 | return tablesCache; 101 | }; 102 | -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | // setimmediate polyfill must be imported first as `dynalite` depends on it 2 | import "setimmediate"; 3 | import dynalite from "dynalite"; 4 | import { getTables, getDynalitePort } from "./config"; 5 | import { hasV3 } from "./utils"; 6 | 7 | export const dynaliteInstance = dynalite({ 8 | createTableMs: 0, 9 | deleteTableMs: 0, 10 | updateTableMs: 0, 11 | }); 12 | 13 | export const start = async (): Promise => { 14 | if (!dynaliteInstance.listening) { 15 | await new Promise((resolve) => 16 | dynaliteInstance.listen(process.env.MOCK_DYNAMODB_PORT, resolve) 17 | ); 18 | } 19 | }; 20 | 21 | export const stop = async (): Promise => { 22 | if (hasV3()) { 23 | // v3 does something to prevent dynalite 24 | // from shutting down until we have 25 | // killed the dynamodb connection 26 | (await import("./dynamodb/v3")).killConnection(); 27 | } 28 | if (dynaliteInstance.listening) { 29 | await new Promise((resolve) => 30 | dynaliteInstance.close(() => resolve()) 31 | ); 32 | } 33 | }; 34 | 35 | export const deleteTables = async (): Promise => { 36 | const tablesNames = (await getTables()).map((table) => table.TableName); 37 | if (hasV3()) { 38 | await ( 39 | await import("./dynamodb/v3") 40 | ).deleteTables(tablesNames, getDynalitePort()); 41 | } else { 42 | await ( 43 | await import("./dynamodb/v2") 44 | ).deleteTables(tablesNames, getDynalitePort()); 45 | } 46 | }; 47 | 48 | export const createTables = async (): Promise => { 49 | const tables = await getTables(); 50 | if (hasV3()) { 51 | await ( 52 | await import("./dynamodb/v3") 53 | ).createTables(tables, getDynalitePort()); 54 | } else { 55 | await ( 56 | await import("./dynamodb/v2") 57 | ).createTables(tables, getDynalitePort()); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/dynamodb/v2.ts: -------------------------------------------------------------------------------- 1 | import DynamoDB, { DocumentClient } from "aws-sdk/clients/dynamodb"; 2 | import { TableConfig } from "../types"; 3 | import { omit, runWithRealTimers, sleep } from "../utils"; 4 | 5 | type Connection = { 6 | dynamoDB: DynamoDB; 7 | documentDB: DocumentClient; 8 | }; 9 | 10 | let connection: Connection | undefined; 11 | 12 | const dbConnection = (port: number): Connection => { 13 | if (connection) { 14 | return connection; 15 | } 16 | const options = { 17 | endpoint: `localhost:${port}`, 18 | sslEnabled: false, 19 | region: "local", 20 | }; 21 | 22 | connection = { 23 | dynamoDB: new DynamoDB(options), 24 | documentDB: new DocumentClient(options), 25 | }; 26 | 27 | return connection; 28 | }; 29 | 30 | const waitForTable = async ( 31 | client: DynamoDB, 32 | tableName: string 33 | ): Promise => { 34 | // eslint-disable-next-line no-constant-condition 35 | while (true) { 36 | // eslint-disable-next-line no-await-in-loop 37 | const details = await client 38 | .describeTable({ TableName: tableName }) 39 | .promise() 40 | .catch(() => undefined); 41 | 42 | if (details?.Table?.TableStatus === "ACTIVE") { 43 | // eslint-disable-next-line no-await-in-loop 44 | await sleep(10); 45 | break; 46 | } 47 | // eslint-disable-next-line no-await-in-loop 48 | await sleep(10); 49 | } 50 | }; 51 | 52 | /** 53 | * Poll the tables list to ensure that the given list of tables exists 54 | */ 55 | const waitForDeleted = async ( 56 | client: DynamoDB, 57 | tableName: string 58 | ): Promise => { 59 | // eslint-disable-next-line no-constant-condition 60 | while (true) { 61 | // eslint-disable-next-line no-await-in-loop 62 | const details = await client 63 | .describeTable({ TableName: tableName }) 64 | .promise() 65 | .catch((e) => e.name === "ResourceInUseException"); 66 | 67 | // eslint-disable-next-line no-await-in-loop 68 | await sleep(100); 69 | 70 | if (!details) { 71 | break; 72 | } 73 | } 74 | }; 75 | 76 | export const deleteTables = ( 77 | tableNames: string[], 78 | port: number 79 | ): Promise => 80 | runWithRealTimers(async () => { 81 | const { dynamoDB } = dbConnection(port); 82 | await Promise.all( 83 | tableNames.map((table) => 84 | dynamoDB 85 | .deleteTable({ TableName: table }) 86 | .promise() 87 | .catch(() => {}) 88 | ) 89 | ); 90 | await Promise.all( 91 | tableNames.map((table) => waitForDeleted(dynamoDB, table)) 92 | ); 93 | }); 94 | 95 | export const createTables = ( 96 | tables: TableConfig[], 97 | port: number 98 | ): Promise => 99 | runWithRealTimers(async () => { 100 | const { dynamoDB, documentDB } = dbConnection(port); 101 | 102 | await Promise.all( 103 | tables.map((table) => dynamoDB.createTable(omit(table, "data")).promise()) 104 | ); 105 | await Promise.all( 106 | tables.map((table) => waitForTable(dynamoDB, table.TableName)) 107 | ); 108 | await Promise.all( 109 | tables.map( 110 | (table) => 111 | table.data && 112 | Promise.all( 113 | table.data.map((row) => 114 | documentDB 115 | .put({ TableName: table.TableName, Item: row as any }) 116 | .promise() 117 | .catch((e) => { 118 | throw new Error( 119 | `Could not add ${JSON.stringify(row)} to "${ 120 | table.TableName 121 | }": ${e.message}` 122 | ); 123 | }) 124 | ) 125 | ) 126 | ) 127 | ); 128 | }); 129 | -------------------------------------------------------------------------------- /src/dynamodb/v3.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDB } from "@aws-sdk/client-dynamodb"; 2 | import { Marshaller } from "@aws/dynamodb-auto-marshaller"; 3 | import { TableConfig } from "../types"; 4 | import { omit, runWithRealTimers, sleep } from "../utils"; 5 | 6 | type Connection = { 7 | dynamoDB: DynamoDB; 8 | }; 9 | 10 | let connection: Connection | undefined; 11 | 12 | const dbConnection = (port: number): Connection => { 13 | if (connection) { 14 | return connection; 15 | } 16 | const options = { 17 | endpoint: `http://localhost:${port}`, 18 | sslEnabled: false, 19 | region: "local", 20 | credentials: { 21 | accessKeyId: "accessKeyId", 22 | secretAccessKey: "secretAccessKey", 23 | }, 24 | }; 25 | 26 | connection = { 27 | dynamoDB: new DynamoDB(options), 28 | }; 29 | 30 | return connection; 31 | }; 32 | 33 | const waitForTable = async ( 34 | client: DynamoDB, 35 | tableName: string 36 | ): Promise => { 37 | // eslint-disable-next-line no-constant-condition 38 | while (true) { 39 | // eslint-disable-next-line no-await-in-loop 40 | const details = await client 41 | .describeTable({ TableName: tableName }) 42 | .catch(() => undefined); 43 | 44 | if (details?.Table?.TableStatus === "ACTIVE") { 45 | // eslint-disable-next-line no-await-in-loop 46 | await sleep(10); 47 | break; 48 | } 49 | // eslint-disable-next-line no-await-in-loop 50 | await sleep(10); 51 | } 52 | }; 53 | 54 | /** 55 | * Poll the tables list to ensure that the given list of tables exists 56 | */ 57 | const waitForDeleted = async ( 58 | client: DynamoDB, 59 | tableName: string 60 | ): Promise => { 61 | // eslint-disable-next-line no-constant-condition 62 | while (true) { 63 | // eslint-disable-next-line no-await-in-loop 64 | const details = await client 65 | .describeTable({ TableName: tableName }) 66 | .catch((e) => e.name === "ResourceInUseException"); 67 | 68 | // eslint-disable-next-line no-await-in-loop 69 | await sleep(100); 70 | 71 | if (!details) { 72 | break; 73 | } 74 | } 75 | }; 76 | 77 | export const deleteTables = ( 78 | tableNames: string[], 79 | port: number 80 | ): Promise => 81 | runWithRealTimers(async () => { 82 | const { dynamoDB } = dbConnection(port); 83 | await Promise.all( 84 | tableNames.map((table) => 85 | dynamoDB.deleteTable({ TableName: table }).catch(() => {}) 86 | ) 87 | ); 88 | await Promise.all( 89 | tableNames.map((table) => waitForDeleted(dynamoDB, table)) 90 | ); 91 | }); 92 | 93 | export const createTables = ( 94 | tables: TableConfig[], 95 | port: number 96 | ): Promise => 97 | runWithRealTimers(async () => { 98 | const { dynamoDB } = dbConnection(port); 99 | 100 | await Promise.all( 101 | tables.map((table) => dynamoDB.createTable(omit(table, "data"))) 102 | ); 103 | 104 | await Promise.all( 105 | tables.map((table) => waitForTable(dynamoDB, table.TableName)) 106 | ); 107 | await Promise.all( 108 | tables.map( 109 | (table) => 110 | table.data && 111 | Promise.all( 112 | table.data.map((row) => 113 | dynamoDB 114 | .putItem({ 115 | TableName: table.TableName, 116 | Item: new Marshaller().marshallItem(row) as any, 117 | }) 118 | .catch((e) => { 119 | throw new Error( 120 | `Could not add ${JSON.stringify(row)} to "${ 121 | table.TableName 122 | }": ${e.message}` 123 | ); 124 | }) 125 | ) 126 | ) 127 | ) 128 | ); 129 | }); 130 | 131 | export const killConnection = (): void => { 132 | connection?.dynamoDB.destroy(); 133 | }; 134 | -------------------------------------------------------------------------------- /src/environment.spec.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import Environment from "./environment"; 3 | import { dynaliteInstance } from "./db"; 4 | 5 | jest.mock("dynalite", () => () => { 6 | let listening = false; 7 | return { 8 | get listening() { 9 | return listening; 10 | }, 11 | listen: (_: void, resolve: () => void) => { 12 | listening = true; 13 | resolve(); 14 | }, 15 | close: (resolve: () => void) => { 16 | listening = false; 17 | resolve(); 18 | }, 19 | }; 20 | }); 21 | 22 | describe("Environment", () => { 23 | it("should throw an error if config file could not be located", () => { 24 | expect( 25 | () => 26 | new Environment( 27 | { 28 | projectConfig: { rootDir: "somebaddirectory" }, 29 | } as any, 30 | {} as any 31 | ) 32 | ).toThrowErrorMatchingSnapshot(); 33 | }); 34 | 35 | it("should not throw an error if a valid config directory is given", () => { 36 | expect( 37 | () => 38 | new Environment( 39 | { 40 | projectConfig: { rootDir: join(__dirname, "__testdir__") }, 41 | } as any, 42 | {} as any 43 | ) 44 | ).not.toThrowError(); 45 | }); 46 | 47 | it("should start the database when 'setup' is called and stop the db when 'teardown' is called", async () => { 48 | const environment = new Environment( 49 | { 50 | projectConfig: { rootDir: join(__dirname, "__testdir__") }, 51 | } as any, 52 | {} as any 53 | ); 54 | await environment.setup(); 55 | expect(dynaliteInstance.listening).toBeTruthy(); 56 | await environment.teardown(); 57 | expect(dynaliteInstance.listening).toBeFalsy(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/environment.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import NodeEnvironment from "jest-environment-node"; 3 | import type { 4 | EnvironmentContext, 5 | JestEnvironmentConfig, 6 | } from "@jest/environment"; 7 | import setup from "./setup"; 8 | import { start, stop } from "./db"; 9 | import { 10 | CONFIG_FILE_NAME, 11 | CONFIG_FILE_NAME_CJS, 12 | CONFIG_FILE_NAME_TS, 13 | NotFoundError, 14 | } from "./config"; 15 | 16 | class DynaliteEnvironment extends NodeEnvironment { 17 | constructor(config: JestEnvironmentConfig, _context: EnvironmentContext) { 18 | // The config directory is based on the root directory 19 | // of the project config 20 | 21 | const compatConfig = 22 | "projectConfig" in config 23 | ? config 24 | : // For jest <= 27 the config was the project config 25 | // so use that 26 | ({ projectConfig: config } as unknown as JestEnvironmentConfig); 27 | 28 | const { rootDir } = compatConfig.projectConfig; 29 | 30 | try { 31 | setup(rootDir); 32 | } catch (e) { 33 | if (e instanceof NotFoundError) { 34 | throw new Error(` 35 | jest-dynalite could not find "${CONFIG_FILE_NAME}" or "${CONFIG_FILE_NAME_CJS}" or "${CONFIG_FILE_NAME_TS}" in the jest (${rootDir}). 36 | 37 | If you didn't intend to be using this directory for the config, please specify a custom 38 | directory: https://github.com/freshollie/jest-dynalite/#advanced-setup 39 | 40 | If you are already using a custom config directory, you should apply 'import "jest-dynalite/withDb"' 41 | to your "setupFilesAfterEnv" instead of using the preset. 42 | 43 | For more information, please see https://github.com/freshollie/jest-dynalite/#breaking-changes. 44 | `); 45 | } 46 | throw e; 47 | } 48 | 49 | super(compatConfig, _context); 50 | } 51 | 52 | public async setup(): Promise { 53 | await super.setup(); 54 | await start(); 55 | } 56 | 57 | public async teardown(): Promise { 58 | await stop(); 59 | await super.teardown(); 60 | } 61 | } 62 | 63 | export default DynaliteEnvironment; 64 | module.exports = DynaliteEnvironment; 65 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { deleteTables, createTables } from "./db"; 2 | 3 | export { default as setup } from "./setup"; 4 | export { start as startDb, stop as stopDb } from "./db"; 5 | -------------------------------------------------------------------------------- /src/setup.ts: -------------------------------------------------------------------------------- 1 | import { setConfigDir, getDynalitePort } from "./config"; 2 | 3 | export default (withConfigDir: string): void => { 4 | setConfigDir(withConfigDir); 5 | 6 | const port = getDynalitePort(); 7 | 8 | // Provide environment variables before other scripts are executed 9 | process.env.MOCK_DYNAMODB_PORT = port.toString(); 10 | process.env.MOCK_DYNAMODB_ENDPOINT = `http://localhost:${port}`; 11 | 12 | // aws-sdk requires access and secret key to be able to call DDB 13 | if (!process.env.AWS_ACCESS_KEY_ID) { 14 | process.env.AWS_ACCESS_KEY_ID = "access-key"; 15 | } 16 | 17 | if (!process.env.AWS_SECRET_ACCESS_KEY) { 18 | process.env.AWS_SECRET_ACCESS_KEY = "secret-key"; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/setupTables.ts: -------------------------------------------------------------------------------- 1 | import { createTables } from "./db"; 2 | 3 | beforeAll(createTables); 4 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { CreateTableInput as LegacyCreateTableInput } from "aws-sdk/clients/dynamodb"; 2 | 3 | export type TableConfig = LegacyCreateTableInput & { 4 | data?: Record[]; 5 | TableName: string; 6 | }; 7 | 8 | export type Config = { 9 | tables?: TableConfig[] | (() => TableConfig[] | Promise); 10 | basePort?: number; 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const isPromise = (p: unknown | Promise): p is Promise => 2 | !!p && Object.prototype.toString.call(p) === "[object Promise]"; 3 | 4 | export const isFunction = (f: unknown | (() => F)): f is () => F => 5 | !!f && typeof f === "function"; 6 | 7 | const convertToNumbers = ( 8 | keys: Array, 9 | value: string | number 10 | ): number | string => { 11 | if (!Number.isNaN(Number(value)) && keys.some((v) => v === Number(value))) { 12 | return Number(value); 13 | } 14 | 15 | return value; 16 | }; 17 | 18 | // credit: https://stackoverflow.com/a/62362002/1741602 19 | export const omit = ( 20 | obj: T, 21 | ...keys: K 22 | ): { [P in Exclude]: T[P] } => { 23 | return (Object.getOwnPropertySymbols(obj) as Array) 24 | .concat( 25 | Object.keys(obj).map((key) => convertToNumbers(keys, key)) as Array< 26 | keyof T 27 | > 28 | ) 29 | .filter((key) => !keys.includes(key)) 30 | .reduce((agg, key) => ({ ...agg, [key]: obj[key] }), {}) as { 31 | [P in Exclude]: T[P]; 32 | }; 33 | }; 34 | 35 | const globalObj = (typeof window === "undefined" ? global : window) as { 36 | setTimeout: { 37 | _isMockFunction?: boolean; 38 | clock?: boolean; 39 | }; 40 | }; 41 | 42 | const detectTimers = (): { legacy: boolean; modern: boolean } => { 43 | const usingJestAndTimers = 44 | typeof jest !== "undefined" && typeof globalObj.setTimeout !== "undefined"; 45 | const usingLegacyJestFakeTimers = 46 | usingJestAndTimers && 47 | // eslint-disable-next-line no-underscore-dangle 48 | typeof globalObj.setTimeout._isMockFunction !== "undefined" && 49 | // eslint-disable-next-line no-underscore-dangle 50 | globalObj.setTimeout._isMockFunction; 51 | 52 | let usingModernJestFakeTimers = false; 53 | if ( 54 | usingJestAndTimers && 55 | typeof globalObj.setTimeout.clock !== "undefined" && 56 | typeof jest.getRealSystemTime !== "undefined" 57 | ) { 58 | try { 59 | // jest.getRealSystemTime is only supported for Jest's `modern` fake timers and otherwise throws 60 | jest.getRealSystemTime(); 61 | usingModernJestFakeTimers = true; 62 | } catch { 63 | // not using Jest's modern fake timers 64 | } 65 | } 66 | 67 | return { 68 | legacy: usingLegacyJestFakeTimers, 69 | modern: usingModernJestFakeTimers, 70 | }; 71 | }; 72 | 73 | // stolen from https://github.com/testing-library/dom-testing-library/blob/master/src/helpers.js 74 | export const runWithRealTimers = ( 75 | callback: () => T | Promise 76 | ): T | Promise => { 77 | const { modern, legacy } = detectTimers(); 78 | 79 | const usingJestFakeTimers = modern || legacy; 80 | 81 | if (usingJestFakeTimers) { 82 | jest.useRealTimers(); 83 | } 84 | 85 | const callbackReturnValue = callback(); 86 | 87 | if (isPromise(callbackReturnValue)) { 88 | return callbackReturnValue.then((value) => { 89 | if (usingJestFakeTimers) { 90 | jest.useFakeTimers(modern ? "modern" : "legacy"); 91 | } 92 | 93 | return value; 94 | }); 95 | } 96 | 97 | if (usingJestFakeTimers) { 98 | jest.useFakeTimers(modern ? "modern" : "legacy"); 99 | } 100 | 101 | return callbackReturnValue; 102 | }; 103 | 104 | export const hasV3 = (): boolean => { 105 | try { 106 | // eslint-disable-next-line global-require 107 | require("@aws-sdk/client-dynamodb"); 108 | return true; 109 | } catch (_) { 110 | return false; 111 | } 112 | }; 113 | 114 | export const sleep = (time: number): Promise => 115 | new Promise((resolve) => setTimeout(resolve, time)); 116 | -------------------------------------------------------------------------------- /tests/configs/cjs/jest-dynalite-config.cjs: -------------------------------------------------------------------------------- 1 | const tables = require("./tables"); 2 | 3 | module.exports = { 4 | tables: () => tables, 5 | basePort: 10500, 6 | }; 7 | -------------------------------------------------------------------------------- /tests/configs/cjs/tables.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | TableName: "files", 4 | KeySchema: [{ AttributeName: "id", KeyType: "HASH" }], 5 | AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }], 6 | ProvisionedThroughput: { 7 | ReadCapacityUnits: 1, 8 | WriteCapacityUnits: 1, 9 | }, 10 | }, 11 | { 12 | TableName: "images", 13 | KeySchema: [{ AttributeName: "url", KeyType: "HASH" }], 14 | AttributeDefinitions: [{ AttributeName: "url", AttributeType: "S" }], 15 | ProvisionedThroughput: { 16 | ReadCapacityUnits: 1, 17 | WriteCapacityUnits: 1, 18 | }, 19 | data: [ 20 | { 21 | url: "https://something.com/something/image.jpg", 22 | width: 100, 23 | height: 200, 24 | }, 25 | { 26 | url: "https://something.com/something/image2.jpg", 27 | width: 150, 28 | height: 250, 29 | }, 30 | ], 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /tests/configs/javascript/jest-dynalite-config.js: -------------------------------------------------------------------------------- 1 | const tables = require("./tables"); 2 | 3 | module.exports = { 4 | tables: () => tables, 5 | basePort: 10500, 6 | }; 7 | -------------------------------------------------------------------------------- /tests/configs/javascript/tables.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | TableName: "files", 4 | KeySchema: [{ AttributeName: "id", KeyType: "HASH" }], 5 | AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }], 6 | ProvisionedThroughput: { 7 | ReadCapacityUnits: 1, 8 | WriteCapacityUnits: 1, 9 | }, 10 | }, 11 | { 12 | TableName: "images", 13 | KeySchema: [{ AttributeName: "url", KeyType: "HASH" }], 14 | AttributeDefinitions: [{ AttributeName: "url", AttributeType: "S" }], 15 | ProvisionedThroughput: { 16 | ReadCapacityUnits: 1, 17 | WriteCapacityUnits: 1, 18 | }, 19 | data: [ 20 | { 21 | url: "https://something.com/something/image.jpg", 22 | width: 100, 23 | height: 200, 24 | }, 25 | { 26 | url: "https://something.com/something/image2.jpg", 27 | width: 150, 28 | height: 250, 29 | }, 30 | ], 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /tests/configs/tables-function-async/jest-dynalite-config.ts: -------------------------------------------------------------------------------- 1 | import tables from "../tables"; 2 | 3 | const realTimeout = setTimeout; 4 | const sleep = (time: number): Promise => 5 | new Promise((resolve) => realTimeout(resolve, time)); 6 | 7 | export default { 8 | tables: async () => { 9 | await sleep(300); 10 | return tables; 11 | }, 12 | basePort: 10500, 13 | }; 14 | -------------------------------------------------------------------------------- /tests/configs/tables-function/jest-dynalite-config.ts: -------------------------------------------------------------------------------- 1 | import tables from "../tables"; 2 | 3 | export default { 4 | tables: () => tables, 5 | basePort: 10500, 6 | }; 7 | -------------------------------------------------------------------------------- /tests/configs/tables.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | TableName: "files", 4 | KeySchema: [{ AttributeName: "id", KeyType: "HASH" }], 5 | AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }], 6 | ProvisionedThroughput: { 7 | ReadCapacityUnits: 1, 8 | WriteCapacityUnits: 1, 9 | }, 10 | }, 11 | { 12 | TableName: "images", 13 | KeySchema: [{ AttributeName: "url", KeyType: "HASH" }], 14 | AttributeDefinitions: [{ AttributeName: "url", AttributeType: "S" }], 15 | ProvisionedThroughput: { 16 | ReadCapacityUnits: 1, 17 | WriteCapacityUnits: 1, 18 | }, 19 | data: [ 20 | { 21 | url: "https://something.com/something/image.jpg", 22 | width: 100, 23 | height: 200, 24 | }, 25 | { 26 | url: "https://something.com/something/image2.jpg", 27 | width: 150, 28 | height: 250, 29 | }, 30 | ], 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /tests/jest-advanced.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import base from "../jest.base"; 3 | 4 | export default { 5 | ...base, 6 | setupFiles: [join(__dirname, "setups/setupAdvanced.ts")], 7 | setupFilesAfterEnv: [join(__dirname, "setups/setupAdvancedEnv.ts")], 8 | displayName: { 9 | name: "advanced-config", 10 | color: "green", 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /tests/jest-cjs.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import base from "../jest.base"; 3 | 4 | export default { 5 | ...base, 6 | setupFiles: [join(__dirname, "setups/setupCjs.ts")], 7 | setupFilesAfterEnv: [join(__dirname, "setups/setupAdvancedEnv.ts")], 8 | displayName: { 9 | name: "cjs", 10 | color: "yellow", 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /tests/jest-dynalite-config.ts: -------------------------------------------------------------------------------- 1 | import tables from "./configs/tables"; 2 | 3 | // This is the simple jest-dynalite config used by most tests 4 | // More advanced test configs can be found in .jest/configs 5 | export default { 6 | tables, 7 | basePort: 10500, 8 | }; 9 | -------------------------------------------------------------------------------- /tests/jest-jsdom-environment.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import base from "../jest.base"; 3 | 4 | export default { 5 | ...base, 6 | testEnvironment: "jsdom", 7 | setupFiles: [join(__dirname, "setups/setupAdvanced.ts")], 8 | setupFilesAfterEnv: [join(__dirname, "setups/setupSimple.ts")], 9 | displayName: { 10 | name: "simple", 11 | color: "magenta", 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /tests/jest-sdk-v2.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import base from "../jest.base"; 3 | 4 | export default { 5 | ...base, 6 | setupFiles: [join(__dirname, "setups/setupAdvanced.ts")], 7 | setupFilesAfterEnv: [ 8 | join(__dirname, "setups/setupDynamodbV2.ts"), 9 | join(__dirname, "setups/setupSimple.ts"), 10 | ], 11 | displayName: { 12 | name: "sdk-v2", 13 | color: "red", 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /tests/jest-simple.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import base from "../jest.base"; 3 | 4 | export default { 5 | ...base, 6 | // TODO: use testEnvironment when jest 27 arrives 7 | setupFiles: [join(__dirname, "setups/setupAdvanced.ts")], 8 | setupFilesAfterEnv: [join(__dirname, "setups/setupSimple.ts")], 9 | displayName: { 10 | name: "simple", 11 | color: "magenta", 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /tests/jest-tables-function-async.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import base from "../jest.base"; 3 | 4 | export default { 5 | ...base, 6 | setupFiles: [join(__dirname, "setups/setupTablesFunctionAsync.ts")], 7 | setupFilesAfterEnv: [join(__dirname, "setups/setupAdvancedEnv.ts")], 8 | displayName: { 9 | name: "tables-function-async", 10 | color: "cyan", 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /tests/jest-tables-function-js.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import base from "../jest.base"; 3 | 4 | export default { 5 | ...base, 6 | setupFiles: [join(__dirname, "setups/setupTablesFunctionJs.ts")], 7 | setupFilesAfterEnv: [join(__dirname, "setups/setupAdvancedEnv.ts")], 8 | displayName: { 9 | name: "tables-function-js", 10 | color: "yellow", 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /tests/jest-tables-function.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import base from "../jest.base"; 3 | 4 | export default { 5 | ...base, 6 | setupFiles: [join(__dirname, "setups/setupTablesFunction.ts")], 7 | setupFilesAfterEnv: [join(__dirname, "setups/setupAdvancedEnv.ts")], 8 | displayName: { 9 | name: "tables-function", 10 | color: "yellow", 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /tests/setups/setupAdvanced.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { setup } from "../../src"; 3 | 4 | // Setup with the root config 5 | setup(join(__dirname, "../")); 6 | 7 | jest.resetModules(); 8 | jest.mock("aws-sdk", () => { 9 | throw new Error("should not import this"); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/setups/setupAdvancedEnv.ts: -------------------------------------------------------------------------------- 1 | import { startDb, stopDb, createTables, deleteTables } from "../../src"; 2 | 3 | beforeAll(startDb); 4 | 5 | // Create tables but don't delete them after tests 6 | // beforeAll(createTables); 7 | 8 | // Optional 9 | beforeEach(createTables); 10 | afterEach(deleteTables); 11 | 12 | afterAll(stopDb); 13 | -------------------------------------------------------------------------------- /tests/setups/setupCjs.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { setup } from "../../src"; 3 | 4 | // Setup with the config dir 5 | setup(join(__dirname, "../configs/cjs")); 6 | -------------------------------------------------------------------------------- /tests/setups/setupDynamodbV2.ts: -------------------------------------------------------------------------------- 1 | jest.resetModules(); 2 | jest.mock("../../src/utils", () => ({ 3 | ...(jest.requireActual("../../src/utils") as any), 4 | hasV3: () => false, 5 | })); 6 | 7 | jest.mock("@aws-sdk/client-dynamodb", () => { 8 | throw new Error("should not import this"); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/setups/setupSimple.ts: -------------------------------------------------------------------------------- 1 | import { startDb, stopDb } from "../../src"; 2 | 3 | beforeAll(startDb); 4 | require("../../src/setupTables"); 5 | require("../../src/clearAfterEach"); 6 | 7 | afterAll(stopDb); 8 | -------------------------------------------------------------------------------- /tests/setups/setupTablesFunction.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { setup } from "../../src"; 3 | 4 | // Setup with the config dir 5 | setup(join(__dirname, "../configs/tables-function")); 6 | -------------------------------------------------------------------------------- /tests/setups/setupTablesFunctionAsync.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { setup } from "../../src"; 3 | 4 | // Setup with the config dir 5 | setup(join(__dirname, "../configs/tables-function-async")); 6 | -------------------------------------------------------------------------------- /tests/setups/setupTablesFunctionJs.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { setup } from "../../src"; 3 | 4 | // Setup with the config dir 5 | setup(join(__dirname, "../configs/javascript")); 6 | -------------------------------------------------------------------------------- /tests/suites/modern-timers.test.ts: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | jest.useRealTimers(); 3 | }); 4 | 5 | it(`handles modern fake timers`, () => { 6 | jest.useFakeTimers(`modern`); 7 | const timeout = new Promise((resolve) => setTimeout(resolve, 5000)); 8 | jest.advanceTimersByTime(5000); 9 | return timeout; 10 | }); 11 | it(`still handles legacy fake timers`, () => { 12 | jest.useFakeTimers(); 13 | const timeout = new Promise((resolve) => setTimeout(resolve, 5000)); 14 | jest.advanceTimersByTime(5000); 15 | return timeout; 16 | }); 17 | 18 | it(`handles switching back to modern timers`, () => { 19 | jest.useFakeTimers(`modern`); 20 | const timeout = new Promise((resolve) => setTimeout(resolve, 5000)); 21 | jest.advanceTimersByTime(5000); 22 | return timeout; 23 | }); 24 | -------------------------------------------------------------------------------- /tests/suites/suite1.test.ts: -------------------------------------------------------------------------------- 1 | import { DocumentClient } from "aws-sdk/clients/dynamodb"; 2 | 3 | const ddb = new DocumentClient({ 4 | convertEmptyValues: true, 5 | endpoint: process.env.MOCK_DYNAMODB_ENDPOINT, 6 | sslEnabled: false, 7 | region: "local", 8 | }); 9 | 10 | it("should not share data between test suites", async () => { 11 | const { Item } = await ddb 12 | .get({ TableName: "files", Key: { id: "1" } }) 13 | .promise(); 14 | 15 | expect(Item).not.toBeDefined(); 16 | }); 17 | 18 | it("should allow the environment variable to be deleted", () => { 19 | delete process.env.MOCK_DYNAMODB_ENDPOINT; 20 | }); 21 | -------------------------------------------------------------------------------- /tests/suites/suite2.test.ts: -------------------------------------------------------------------------------- 1 | import { DocumentClient } from "aws-sdk/clients/dynamodb"; 2 | 3 | const ddb = new DocumentClient({ 4 | convertEmptyValues: true, 5 | endpoint: process.env.MOCK_DYNAMODB_ENDPOINT, 6 | sslEnabled: false, 7 | region: "local", 8 | }); 9 | 10 | it("should insert item into table", async () => { 11 | await ddb 12 | .put({ TableName: "files", Item: { id: "1", hello: "world" } }) 13 | .promise(); 14 | 15 | const { Item } = await ddb 16 | .get({ TableName: "files", Key: { id: "1" } }) 17 | .promise(); 18 | 19 | expect(Item).toEqual({ 20 | id: "1", 21 | hello: "world", 22 | }); 23 | }); 24 | 25 | it("clears tables between tests", async () => { 26 | const { Item } = await ddb 27 | .get({ TableName: "files", Key: { id: "1" } }) 28 | .promise(); 29 | 30 | expect(Item).not.toEqual({ 31 | id: "1", 32 | hello: "world", 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/suites/table-data.test.ts: -------------------------------------------------------------------------------- 1 | import { DocumentClient } from "aws-sdk/clients/dynamodb"; 2 | 3 | const ddb = new DocumentClient({ 4 | convertEmptyValues: true, 5 | endpoint: process.env.MOCK_DYNAMODB_ENDPOINT, 6 | sslEnabled: false, 7 | region: "local", 8 | }); 9 | 10 | it("should contain table data provided in config", async () => { 11 | { 12 | const { Item } = await ddb 13 | .get({ 14 | TableName: "images", 15 | Key: { url: "https://something.com/something/image.jpg" }, 16 | }) 17 | .promise(); 18 | 19 | expect(Item).toEqual({ 20 | url: "https://something.com/something/image.jpg", 21 | 22 | width: 100, 23 | height: 200, 24 | }); 25 | } 26 | { 27 | const { Item } = await ddb 28 | .get({ 29 | TableName: "images", 30 | Key: { url: "https://something.com/something/image2.jpg" }, 31 | }) 32 | .promise(); 33 | 34 | expect(Item).toEqual({ 35 | url: "https://something.com/something/image2.jpg", 36 | width: 150, 37 | height: 250, 38 | }); 39 | } 40 | }); 41 | 42 | it("should ensure that data is recreated after each test", async () => { 43 | await ddb 44 | .delete({ 45 | TableName: "images", 46 | Key: { url: "https://something.com/something/image2.jpg" }, 47 | }) 48 | .promise(); 49 | }); 50 | 51 | // This test must follow the previous 52 | it("post should ensure that data is recreated after each test", async () => { 53 | const { Item } = await ddb 54 | .get({ 55 | TableName: "images", 56 | Key: { url: "https://something.com/something/image2.jpg" }, 57 | }) 58 | .promise(); 59 | 60 | expect(Item).toEqual({ 61 | url: "https://something.com/something/image2.jpg", 62 | width: 150, 63 | height: 250, 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/suites/timers.test.ts: -------------------------------------------------------------------------------- 1 | jest.useFakeTimers(); 2 | 3 | it("should not be affected by fake timers", () => { 4 | const timeout = new Promise((resolve) => setTimeout(resolve, 5000)); 5 | jest.advanceTimersByTime(5000); 6 | return timeout; 7 | }); 8 | 9 | it("should not turn off fake timers between tests", () => { 10 | const timeout = new Promise((resolve) => setTimeout(resolve, 5000)); 11 | jest.advanceTimersByTime(5000); 12 | return timeout; 13 | }); 14 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts", "types/**/*.d.ts"], 4 | "exclude": ["**/*.spec.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["**/*.js", "**/*.ts", "**/*.cjs", ".jest/**/*.js", ".eslintrc.js"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["esnext", "DOM"], 5 | "outDir": "./dist", 6 | "module": "commonjs", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "declaration": true 11 | }, 12 | "include": ["src/**/*.ts", "tests/**/*.ts", "types/**/*.d.ts"], 13 | "exclude": ["dist"] 14 | } 15 | -------------------------------------------------------------------------------- /types/dynalite.d.ts: -------------------------------------------------------------------------------- 1 | declare module "dynalite" { 2 | import { Server } from "http"; 3 | 4 | interface DynaliteOptions { 5 | ssl?: boolean; 6 | path?: string; 7 | createTableMs?: number; 8 | deleteTableMs?: number; 9 | updateTableMs?: number; 10 | maxItemSizeKb?: number; 11 | } 12 | 13 | export interface DynaliteServer extends Omit { 14 | close(cb?: (err?: Error) => void): void; 15 | } 16 | 17 | export class DynaliteServer { 18 | public close(cb?: (err?: Error) => void): void; 19 | } 20 | 21 | function createDynalite(options: DynaliteOptions): DynaliteServer; 22 | 23 | export default createDynalite; 24 | } 25 | -------------------------------------------------------------------------------- /withDb.js: -------------------------------------------------------------------------------- 1 | const { startDb, stopDb, createTables, deleteTables } = require("./dist"); 2 | 3 | beforeAll(startDb); 4 | beforeEach(createTables); 5 | afterEach(deleteTables); 6 | afterAll(stopDb); 7 | --------------------------------------------------------------------------------