diff --git a/.gitignore b/.gitignore index 35354b04..05192b78 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* + +# Jest +globalConfig.json \ No newline at end of file diff --git a/jest-mongodb-config.js b/jest-mongodb-config.js new file mode 100644 index 00000000..eeebbc69 --- /dev/null +++ b/jest-mongodb-config.js @@ -0,0 +1,12 @@ +module.exports = { + mongodbMemoryServerOptions: { + instance: { + dbName: 'jest', + }, + binary: { + version: '4.0.3', + skipMD5: true, + }, + autoStart: false, + }, +}; diff --git a/jest.config.js b/jest.config.js index b2daf8d5..214a6ca6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,8 +4,10 @@ module.exports = { '!**/*.d.ts', '!**/node_modules/**', ], + preset: '@shelf/jest-mongodb', setupFilesAfterEnv: ['/src/setupTests.js'], testPathIgnorePatterns: ['/node_modules/', '/.next/'], + watchPathIgnorePatterns: ['/globalConfig.json'], transform: { '^.+\\.(js|jsx|ts|tsx)$': '/node_modules/babel-jest', '^.+\\.css$': '/config/jest/cssTransform.js', diff --git a/package-lock.json b/package-lock.json index 2ed83a48..2c814589 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3248,6 +3248,25 @@ "integrity": "sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw==", "dev": true }, + "@shelf/jest-mongodb": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@shelf/jest-mongodb/-/jest-mongodb-1.2.1.tgz", + "integrity": "sha512-hvgqSBZT1F4/dCzcKEPEIgi0KY4Sa8MkOzycGIJnltztqYliszHkTcwk6K0y9aBF9uVWMlhhwwXgMS2JokfMKA==", + "dev": true, + "requires": { + "debug": "4.1.1", + "mongodb-memory-server": "6.6.1", + "uuid": "8.1.0" + }, + "dependencies": { + "uuid": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz", + "integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==", + "dev": true + } + } + }, "@sinonjs/commons": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz", @@ -3418,6 +3437,51 @@ "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, + "@types/cross-spawn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz", + "integrity": "sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/debug": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", + "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==", + "dev": true + }, + "@types/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@types/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-EGlKlgMhnLt/cM4DbUSafFdrkeJoC9Mvnj0PUCU7tFmTjMjNRT957kXCx0wYm3JuEq4o4ZsS5vG+NlkM2DMd2A==", + "dev": true + }, + "@types/find-cache-dir": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/find-cache-dir/-/find-cache-dir-3.2.0.tgz", + "integrity": "sha512-+JeT9qb2Jwzw72WdjU+TSvD5O1QRPWCeRpDJV+guiIq+2hwR0DFGw+nZNbTFjMIVe6Bf4GgAKeB/6Ytx6+MbeQ==", + "dev": true + }, + "@types/find-package-json": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/find-package-json/-/find-package-json-1.1.1.tgz", + "integrity": "sha512-XMCocYkg6VUpkbOQMKa3M5cgc3MvU/LJKQwd3VUJrWZbLr2ARUggupsCAF8DxjEEIuSO6HlnH+vl+XV4bgVeEQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/get-port": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/get-port/-/get-port-4.2.0.tgz", + "integrity": "sha512-Iv2FAb5RnIk/eFO2CTu8k+0VMmIR15pKbcqRWi+s3ydW+aKXlN2yemP92SrO++ERyJx+p6Ie1ggbLBMbU1SjiQ==", + "dev": true, + "requires": { + "get-port": "*" + } + }, "@types/graceful-fs": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz", @@ -3474,6 +3538,27 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/lockfile": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/lockfile/-/lockfile-1.0.1.tgz", + "integrity": "sha512-65WZedEm4AnOsBDdsapJJG42MhROu3n4aSSiu87JXF/pSdlubxZxp3S1yz3kTfkJ2KBPud4CpjoHVAptOm9Zmw==", + "dev": true + }, + "@types/md5-file": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/md5-file/-/md5-file-4.0.2.tgz", + "integrity": "sha512-8gacRfEqLrmZ6KofpFfxyjsm/LYepeWUWUJGaf5A9W9J5B2/dRZMdkDqFDL6YDa9IweH12IO76jO7mpsK2B3wg==", + "dev": true + }, + "@types/mkdirp": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.1.tgz", + "integrity": "sha512-HkGSK7CGAXncr8Qn/0VqNtExEE+PHMWb+qlR1faHMao7ng6P3tAaoWWBMdva0gL5h4zprjIO89GJOLXsMcDm1Q==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/mongodb": { "version": "3.5.25", "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.5.25.tgz", @@ -3624,6 +3709,18 @@ "@types/react-test-renderer": "*" } }, + "@types/tmp": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.1.0.tgz", + "integrity": "sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA==", + "dev": true + }, + "@types/uuid": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-7.0.0.tgz", + "integrity": "sha512-RiX1I0lK9WFLFqy2xOxke396f0wKIzk5sAll0tL4J4XDYJXURI7JOs96XQb3nP+2gEpQ/LutBb66jgiT5oQshQ==", + "dev": true + }, "@types/warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", @@ -3949,6 +4046,15 @@ } } }, + "agent-base": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", + "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", + "dev": true, + "requires": { + "debug": "4" + } + }, "aggregate-error": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", @@ -4732,6 +4838,12 @@ "ieee754": "^1.1.4" } }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -5738,6 +5850,12 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", + "dev": true + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -6859,6 +6977,15 @@ "bser": "2.1.1" } }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, "figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -6922,6 +7049,12 @@ "pkg-dir": "^4.1.0" } }, + "find-package-json": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/find-package-json/-/find-package-json-1.2.0.tgz", + "integrity": "sha512-+SOGcLGYDJHtyqHd87ysBhmaeQ95oWspDKnMXBrnQ9Eq4OkLNqejgoaD8xVWu6GPa0B6roa6KinCMEMcVeqONw==", + "dev": true + }, "find-replace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", @@ -7048,6 +7181,12 @@ "readable-stream": "^2.0.0" } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -7108,6 +7247,12 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, + "get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "dev": true + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -7408,6 +7553,16 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -9669,6 +9824,15 @@ "path-exists": "^3.0.0" } }, + "lockfile": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", + "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", + "dev": true, + "requires": { + "signal-exit": "^3.0.2" + } + }, "lodash": { "version": "4.17.19", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", @@ -9806,6 +9970,12 @@ "object-visit": "^1.0.0" } }, + "md5-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-4.0.0.tgz", + "integrity": "sha512-UC0qFwyAjn4YdPpKaDNw6gNxRf7Mcx7jC1UGCY4boCzgvU2Aoc1mOGzTtrjjLKhM5ivsnhoKpQVxKPp+1j1qwg==", + "dev": true + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -10129,6 +10299,105 @@ } } }, + "mongodb-memory-server": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-6.6.1.tgz", + "integrity": "sha512-qnGpGE50snhS3IgKwiPlxnL5wFWPxWutqcCqlsbb68FKohaEIsvi/gMBqJbBLF2l2sQu4a4aJofuKFudSI38vQ==", + "dev": true, + "requires": { + "mongodb-memory-server-core": "6.6.1" + } + }, + "mongodb-memory-server-core": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-6.6.1.tgz", + "integrity": "sha512-k6YowfiHxUdGwR/AhXuyv1V3VW2NkA6VuICQfUqZGeqENsExbnL05oKpavAXrmCGxcabXZ3IMvcgmUwmlJAGng==", + "dev": true, + "requires": { + "@types/cross-spawn": "^6.0.1", + "@types/debug": "^4.1.5", + "@types/dedent": "^0.7.0", + "@types/find-cache-dir": "^3.2.0", + "@types/find-package-json": "^1.1.1", + "@types/get-port": "^4.0.1", + "@types/lockfile": "^1.0.1", + "@types/md5-file": "^4.0.1", + "@types/mkdirp": "^1.0.0", + "@types/tmp": "0.1.0", + "@types/uuid": "7.0.0", + "camelcase": "^5.3.1", + "cross-spawn": "^7.0.1", + "debug": "^4.1.1", + "dedent": "^0.7.0", + "find-cache-dir": "3.3.1", + "find-package-json": "^1.2.0", + "get-port": "5.1.1", + "https-proxy-agent": "5.0.0", + "lockfile": "^1.0.4", + "md5-file": "^4.0.0", + "mkdirp": "^1.0.3", + "mongodb": "^3.5.4", + "tar-stream": "^2.1.1", + "tmp": "^0.1.0", + "uuid": "^7.0.2", + "yauzl": "^2.10.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "tmp": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", + "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", + "dev": true, + "requires": { + "rimraf": "^2.6.3" + } + }, + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "dev": true + } + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -10168,6 +10437,11 @@ "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", "optional": true }, + "nanoid": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", + "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -10861,6 +11135,12 @@ "sha.js": "^2.4.8" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -12563,6 +12843,14 @@ "dev": true, "optional": true }, + "shortid": { + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.15.tgz", + "integrity": "sha512-5EaCy2mx2Jgc/Fdn9uuDuNIIfWBpzY4XIlhoqtXF6qsf+/+SGZ+FxDdX/ZsMZiWupIWNqAEmiNY4RC+LSmCeOw==", + "requires": { + "nanoid": "^2.1.0" + } + }, "side-channel": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz", @@ -13318,6 +13606,53 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" }, + "tar-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.3.tgz", + "integrity": "sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA==", + "dev": true, + "requires": { + "bl": "^4.0.1", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "bl": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", + "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -13472,6 +13807,12 @@ "minimatch": "^3.0.4" } }, + "test-listen": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/test-listen/-/test-listen-1.1.0.tgz", + "integrity": "sha512-OyEVi981C1sb9NX1xayfgZls3p8QTDRwp06EcgxSgd1kktaENBW8dO15i8v/7Fi15j0IYQctJzk5J+hyEBId2w==", + "dev": true + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -14713,6 +15054,16 @@ "decamelize": "^1.2.0" } }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index c8f28365..c85e3a8b 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "react-bootstrap-icons": "^1.0.3-alpha5", "react-dom": "^16.13.1", "sass": "^1.26.10", + "shortid": "^2.2.15", "swr": "^0.2.3", "typeorm": "^0.2.24", "typescript": "^3.9.5", @@ -47,11 +48,13 @@ "yup": "^0.29.1" }, "devDependencies": { + "@shelf/jest-mongodb": "^1.2.1", "@testing-library/jest-dom": "^5.11.0", "@testing-library/react": "^9.4.0", "@testing-library/react-hooks": "^3.3.0", "@types/mongodb": "^3.5.25", "babel-jest": "^25.1.0", + "cookie": "^0.4.1", "eslint-config-airbnb": "^18.2.0", "eslint-plugin-import": "^2.22.0", "eslint-plugin-jest": "^23.17.1", @@ -62,7 +65,8 @@ "identity-obj-proxy": "^3.0.0", "jest": "^25.1.0", "jest-fetch-mock": "^3.0.3", - "react-test-renderer": "^16.12.0" + "react-test-renderer": "^16.12.0", + "test-listen": "^1.1.0" }, "peerOptionalDependencies": { "mongodb": "^3.6.0" diff --git a/src/__mocks__/next-auth/jwt.js b/src/__mocks__/next-auth/jwt.js new file mode 100644 index 00000000..c52a6af4 --- /dev/null +++ b/src/__mocks__/next-auth/jwt.js @@ -0,0 +1,17 @@ +const { getObjectId } = require('mongo-seeding'); + +const jwt = jest.genMockFromModule('next-auth/jwt'); + +function getToken() { + return { + exp: 1000, + user: { + id: getObjectId('test-user'), + name: 'Test User', + email: 'test@example.com', + }, + }; +} + +jwt.getToken = getToken; +module.exports = jwt; diff --git a/src/__tests__/api/document.test.ts b/src/__tests__/api/document.test.ts new file mode 100644 index 00000000..ae9d03b1 --- /dev/null +++ b/src/__tests__/api/document.test.ts @@ -0,0 +1,53 @@ +import nc from 'next-connect'; +import { testLambda } from '../../utils/lambdaTestUtil'; +import postDocument from '../../utils/dbUtil'; +import { expectJSONBodyMiddleware } from '../../middlewares/expectJSONBody'; +import { + RequestInitMethod, +} from '../../utils/requestMethods'; + +afterEach(jest.clearAllMocks); + +const url = '/api/document'; +const middleware = expectJSONBodyMiddleware; +const docRoute = nc().post(postDocument); + +test('should be a function', () => { + expect(docRoute).toBeInstanceOf(Function); +}); + +test('results in 404 on get', async () => { + const method: RequestInitMethod = 'get'; + const response = await testLambda(docRoute, { + method, + middleware, + url, + }); + + expect(response.status).toBe(404); +}); + +test('results in 400 on empty post', async () => { + const method: RequestInitMethod = 'post'; + const response = await testLambda(docRoute, { + method, + middleware, + url, + }); + + expect(response.status).toBe(400); +}); + +test('results in 400 on post with missing title', async () => { + const method: RequestInitMethod = 'post'; + const response = await testLambda(docRoute, { + method, + middleware, + url, + body: { + 'notes':'test', + }, + }); + + expect(response.status).toBe(400); +}); diff --git a/src/__tests__/index.test.js b/src/__tests__/index.test.js index 8ca39608..002ad8c3 100644 --- a/src/__tests__/index.test.js +++ b/src/__tests__/index.test.js @@ -1,3 +1,7 @@ +/** + * @jest-environment jsdom + */ + import { act } from 'react-dom/test-utils'; import { render, wait } from '@testing-library/react'; import Index from '../pages/index'; diff --git a/src/__tests__/user/editprofile.test.js b/src/__tests__/user/editprofile.test.js index 82ba3e97..03b5f3a4 100644 --- a/src/__tests__/user/editprofile.test.js +++ b/src/__tests__/user/editprofile.test.js @@ -1,3 +1,7 @@ +/** + * @jest-environment jsdom + */ + import { render, wait } from '@testing-library/react'; import EditProfile from '../../pages/user/[slug]/editprofile'; diff --git a/src/__tests__/user/newuser.test.js b/src/__tests__/user/newuser.test.js index 90206eda..dbe4c8d5 100644 --- a/src/__tests__/user/newuser.test.js +++ b/src/__tests__/user/newuser.test.js @@ -1,3 +1,7 @@ +/** + * @jest-environment jsdom + */ + import { render, wait } from '@testing-library/react'; import NewUser from '../../pages/user/newuser'; diff --git a/src/components/Alerts/Alerts.test.js b/src/components/Alerts/Alerts.test.js index b12efc7b..1e1c6ab2 100644 --- a/src/components/Alerts/Alerts.test.js +++ b/src/components/Alerts/Alerts.test.js @@ -1,3 +1,7 @@ +/** + * @jest-environment jsdom + */ + import { render } from '@testing-library/react'; import { mockNextUseRouter } from '../../utils/testUtil.ts'; import Alerts from './Alerts'; diff --git a/src/components/Footer/Footer.test.js b/src/components/Footer/Footer.test.js index 0eb0da51..69296a14 100644 --- a/src/components/Footer/Footer.test.js +++ b/src/components/Footer/Footer.test.js @@ -1,3 +1,7 @@ +/** + * @jest-environment jsdom + */ + import { render } from '@testing-library/react'; import Footer from './Footer'; diff --git a/src/components/Header/Header.test.js b/src/components/Header/Header.test.js index 837973df..dad50c33 100644 --- a/src/components/Header/Header.test.js +++ b/src/components/Header/Header.test.js @@ -1,3 +1,7 @@ +/** + * @jest-environment jsdom + */ + import { render, wait } from '@testing-library/react'; import Header from './Header'; diff --git a/src/components/Layout/Layout.test.js b/src/components/Layout/Layout.test.js index d9161f5e..416db443 100644 --- a/src/components/Layout/Layout.test.js +++ b/src/components/Layout/Layout.test.js @@ -1,3 +1,7 @@ +/** + * @jest-environment jsdom + */ + import { act } from 'react-dom/test-utils'; import { render, wait } from '@testing-library/react'; import Layout from './Layout'; diff --git a/src/middlewares/expectJSONBody.ts b/src/middlewares/expectJSONBody.ts new file mode 100644 index 00000000..e388ebdd --- /dev/null +++ b/src/middlewares/expectJSONBody.ts @@ -0,0 +1,29 @@ +import { NextApiResponse, NextApiRequest } from 'next'; +import { Middleware } from 'next-connect'; + +/** + * Middleware accepting exclusively valid JSON as req.body, if existing + * This utility courtesy of Gerrit Alex (@ljosberinn) + * From @ljosberinn/personal-react-boilerplate on GitHub + * Released under MIT License, 2020 + */ + +export const expectJSONBodyMiddleware: Middleware = (req, res, next) => { + if (req.body.length > 0) { + try { + const body = JSON.parse(req.body); + + if (!(body instanceof Object)) { + return res.status(400).end(); + } + + req.body = body; + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + return res.status(400).end(); + } + } + + next(); +}; \ No newline at end of file diff --git a/src/pages/api/document/index.js b/src/pages/api/document/index.js index 7f135205..00f99f6d 100644 --- a/src/pages/api/document/index.js +++ b/src/pages/api/document/index.js @@ -1,90 +1,9 @@ import nc from 'next-connect'; -import { ObjectID } from 'mongodb'; -import jwt from 'next-auth/jwt'; import middleware from '../../../middlewares/middleware'; - -const secret = process.env.AUTH_SECRET; +import postDocument from '../../../utils/dbUtil'; const handler = nc() .use(middleware) - .post( - async (req, res) => { - const token = await jwt.getToken({ req, secret }); - if (token && token.exp > 0) { - const dateCreated = new Date(Date.now()); - const { - title, - slug, - groups, - resourceType, - authors, - publisher, - publicationDate, - bookTitle, - edition, - url, - accessed, - rightsStatus, - location, - state, - text, - uploadContentType, - editors, - volume, - issue, - pageNumbers, - publication, - series, - sesiesNumber, - notes, - } = req.body; - const metadata = { - title, - slug, - groups, - resourceType, - authors, - publisher, - publicationDate, - bookTitle, - edition, - url, - accessed, - rightsStatus, - location, - state, - text, - uploadContentType, - editors, - volume, - issue, - pageNumbers, - publication, - series, - sesiesNumber, - notes, - }; - Object.keys(metadata).forEach((key) => { - if (metadata[key] === undefined) { - delete metadata[key]; - } - }); - await req.db - .collection('documents') - .insert( - { - owner: token.id, - createdAt: dateCreated, - updatedAt: dateCreated, - ...metadata, - }, - (err, doc) => { - if (err) throw err; - res.status(200).json(doc); - }, - ); - } else res.status(403).json({ error: '403 Invalid or expired token' }); - }, - ); + .post(postDocument); export default handler; diff --git a/src/setupTests.js b/src/setupTests.js index cb802709..281a623d 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -6,7 +6,3 @@ // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom/extend-expect'; - -import fetchMock from 'jest-fetch-mock'; - -fetchMock.enableMocks(); diff --git a/src/utils/dbUtil.js b/src/utils/dbUtil.js new file mode 100644 index 00000000..373d9681 --- /dev/null +++ b/src/utils/dbUtil.js @@ -0,0 +1,88 @@ +import jwt from 'next-auth/jwt'; + +const secret = process.env.AUTH_SECRET; + +const postDocument = async (req, res) => { + const token = await jwt.getToken({ req, secret }); + if (token && token.exp > 0) { + const dateCreated = new Date(Date.now()); + const { + title, + slug, + groups, + resourceType, + authors, + publisher, + publicationDate, + bookTitle, + edition, + url, + accessed, + rightsStatus, + location, + state, + text, + uploadContentType, + editors, + volume, + issue, + pageNumbers, + publication, + series, + sesiesNumber, + notes, + } = req.body; + const metadata = { + title, + slug, + groups, + resourceType, + authors, + publisher, + publicationDate, + bookTitle, + edition, + url, + accessed, + rightsStatus, + location, + state, + text, + uploadContentType, + editors, + volume, + issue, + pageNumbers, + publication, + series, + sesiesNumber, + notes, + }; + Object.keys(metadata).forEach((key) => { + if (metadata[key] === undefined) { + delete metadata[key]; + } + }); + if (Object.keys(metadata).length === 0) { + res.status(400).json({ error: '400 No request body' }); + } else if (!metadata.title) { + res.status(400).json({ error: '400 Missing title' }); + } else { + await req.db + .collection('documents') + .insert( + { + owner: token.user.id, + createdAt: dateCreated, + updatedAt: dateCreated, + ...metadata, + }, + (err, doc) => { + if (err) throw err; + res.status(200).json(doc); + }, + ); + } + } else res.status(403).json({ error: '403 Invalid or expired token' }); +}; +export default postDocument; diff --git a/src/utils/lambdaTestUtil.ts b/src/utils/lambdaTestUtil.ts new file mode 100644 index 00000000..afe9cde6 --- /dev/null +++ b/src/utils/lambdaTestUtil.ts @@ -0,0 +1,215 @@ +import { serialize } from 'cookie'; +import { createServer } from 'http'; +import { NextApiRequest, NextApiResponse } from 'next'; +import nextConnect, { NextConnect, Middleware as NextConnectMiddleware, } from 'next-connect'; +import { + apiResolver, + getQueryParser, +} from 'next/dist/next-server/server/api-utils'; +import { route } from 'next/dist/next-server/server/router'; +import fetch, { Response } from 'node-fetch'; +import listen from 'test-listen'; +import { RequestInitMethod } from '../utils/requestMethods'; + + +/* +* This utility courtesy of Gerrit Alex (@ljosberinn) +* From @ljosberinn/personal-react-boilerplate on GitHub +* Released under MIT License, 2020 +*/ + +interface UrlArguments { + /** + * the endpoint to test + */ + url: string; + /** + * optional search params + */ + searchParams?: Record | URLSearchParams; + /** + * HTTP request method + * @default GET + */ + method?: RequestInitMethod; + /** + * any JSON payload + */ + body?: Record | string; + /** + * required to test a catchall lambda + */ + catchAllName?: string; + /** + * next-connect compatible middleware(s) + */ + middleware?: NextConnectMiddleware | NextConnectMiddleware[]; + /** + * optional headers passed to the request + */ + headers?: + | Record + | { + cookie: Record | string; + [key: string]: any; + }; + /** + * whether the request should follow a redirect + */ + redirect?: RequestRedirect; +} + +const getUrl = ( + index: string, + maybeUrl: string | URL = '/', + maybeParams: UrlArguments['searchParams'] = {} +) => { + const url = maybeUrl instanceof URL ? maybeUrl : new URL(index + maybeUrl); + + if (url.search) { + throw new Error( + 'Use `searchParams` instead of appending `?foo=bar` to the url' + ); + } + + const params = + maybeParams instanceof URLSearchParams + ? maybeParams + : new URLSearchParams(maybeParams); + + params.forEach((value, key) => { + url.searchParams.append(key, value); + }); + + return url.toString(); +}; + +/** + * Applies middlewares to tested lambdas + * + * @param handler + * @param middleware + */ +const withMiddleware = ( + handler: NextConnect, + middleware?: UrlArguments['middleware'] +) => { + if (!middleware) { + return handler; + } + + const middlewaresToApply = Array.isArray(middleware) + ? middleware + : [middleware]; + + const connected = nextConnect(); + + middlewaresToApply.forEach((middleware) => { + connected.use(middleware); + }); + + connected.use(handler); + + return connected; +}; + +export const testLambda = async ( + handler: NextConnect, + { + url, + searchParams, + method = 'get', + body, + catchAllName, + middleware, + headers, + redirect, + }: UrlArguments +): Promise => { + const server = createServer((req, res) => { + const getQuery = getQueryParser(req); + const resolver = withMiddleware(handler, middleware); + + const apiContext = { + previewModeEncryptionKey: '', + previewModeId: '', + previewModeSigningKey: '', + }; + + return apiResolver(req, res, getQuery(), resolver, apiContext, true); + }); + + const index = await listen(server); + + const urlToFetch = (() => { + if (catchAllName) { + const matcher = route('/:path*'); + const { path }: { path: string[] } = matcher(url); + + /** + * Next doesnt allow nested catch all routes such as + * /api/[...foo]/bar/[...baz].js + * so we only have to care about + * /api/[...foo].js + */ + const lastSegment = path.pop() as string; + + // migrate all previous search params if existing + const params = + searchParams instanceof URLSearchParams + ? searchParams + : new URLSearchParams(searchParams); + + /** + * workaround for catchall routes + * appending the same key twice automatically makes it an array which is + * required for the catchall logic in the handler to work as expected + * + * downside: the empty value will show up in the lambda as 2nd argument + * e.g. [...authRouter].ts will have + * req.query.authRouter === ['login', ''] + * + * probably won't matter though as we only care about [0] + */ + const query = [ + params.toString(), + // order is important - key with value must come before key without value + `${catchAllName}=${lastSegment}`, + catchAllName, + ] + // filter in case params is empty + .filter(Boolean) + .join('&'); + + const pathname = path.join('/') + '/'; + const affix = [pathname, query].join('?'); + + return [index, affix].join('/'); + } + + return url || searchParams ? getUrl(index, url, searchParams) : index; + })(); + + if (headers?.cookie) { + headers.cookie = Object.entries(headers.cookie) + .map(([key, value]) => serialize(key, value)) + .join(';'); + } + + const response = await fetch(urlToFetch, { + body: body ? JSON.stringify(body) : undefined, + headers, + method, + redirect, + }); + + return new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + } else { + resolve(response); + } + }); + }); +}; \ No newline at end of file diff --git a/src/utils/requestMethods.ts b/src/utils/requestMethods.ts new file mode 100644 index 00000000..a736d76a --- /dev/null +++ b/src/utils/requestMethods.ts @@ -0,0 +1,12 @@ +export const RequestMethods = [ + 'put', + 'patch', + 'get', + 'delete', + 'head', + 'post', + 'options', + 'trace', +] as const; + +export type RequestInitMethod = typeof RequestMethods[number];