From ebd1b4c087948f0057ecf10953fc8cecae9c4a89 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Sun, 3 May 2026 14:21:15 -0700 Subject: [PATCH] feat: harnessed Cache to criteria evaluation request flow --- .env-cmdrc-template | 2 + .github/FUNDING.yml | 3 +- npm-shrinkwrap.json | 175 +++---- package.json | 10 +- src/aggregator/configuration-type.js | 16 +- src/app.js | 11 +- src/helpers/cache/index.js | 21 +- src/helpers/cache/query.js | 31 +- src/middleware/validators.js | 4 +- src/services/config.js | 7 + src/services/criteria.js | 31 +- src/services/snapshot-cache.js | 31 ++ tests/app.test.js | 17 + tests/client-api-cached-payload.test.js | 114 +++++ tests/client-api-cached.test.js | 607 ++++++++++++++++++++++++ tests/graphql-utils/index.js | 8 +- tests/relay-cached.test.js | 547 +++++++++++++++++++++ tests/unit-test/cache/cache.test.js | 5 +- 18 files changed, 1521 insertions(+), 119 deletions(-) create mode 100644 src/services/snapshot-cache.js create mode 100644 tests/client-api-cached-payload.test.js create mode 100644 tests/client-api-cached.test.js create mode 100644 tests/relay-cached.test.js diff --git a/.env-cmdrc-template b/.env-cmdrc-template index d21e184..c6dcb94 100644 --- a/.env-cmdrc-template +++ b/.env-cmdrc-template @@ -15,6 +15,7 @@ "REGEX_MAX_TIMEOUT": 3000, "REGEX_MAX_BLACKLIST": 50, "MAX_REQUEST_PER_MINUTE": 0, + "CACHE_SNAPSHOT_MS": 5000, "SWITCHER_API_LOGGER": true, "SWITCHER_API_LOGGER_LEVEL": "debug", @@ -43,6 +44,7 @@ "REGEX_MAX_TIMEOUT": 3000, "REGEX_MAX_BLACKLIST": 50, "MAX_REQUEST_PER_MINUTE": 0, + "CACHE_SNAPSHOT_MS": 5000, "SWITCHER_API_LOGGER": false, "SWITCHER_API_LOGGER_LEVEL": "debug", diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 890ded4..1dde32b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,3 @@ # These are supported funding model platforms - -patreon: switcherapi ko_fi: petruki +github: [petruki] \ No newline at end of file diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 59d375e..f6aa6d8 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -9,12 +9,12 @@ "version": "1.1.0", "license": "MIT", "dependencies": { - "axios": "^1.15.0", + "axios": "^1.16.0", "bcryptjs": "^3.0.3", "cors": "^2.8.6", "express": "^5.2.1", "express-basic-auth": "^1.2.1", - "express-rate-limit": "^8.3.2", + "express-rate-limit": "^8.4.1", "express-validator": "^7.3.2", "graphql": "^16.13.2", "graphql-http": "^1.22.4", @@ -22,8 +22,8 @@ "helmet": "^8.1.0", "jsonwebtoken": "^9.0.3", "moment": "^2.30.1", - "mongodb": "^7.1.1", - "mongoose": "^9.4.1", + "mongodb": "^7.2.0", + "mongoose": "^9.6.1", "pino": "^10.3.1", "pino-pretty": "^13.1.3", "swagger-ui-express": "^5.0.1", @@ -32,7 +32,7 @@ }, "devDependencies": { "env-cmd": "^11.0.0", - "eslint": "^10.2.0", + "eslint": "^10.3.0", "jest": "^30.3.0", "jest-sonar-reporter": "^2.0.0", "node-notifier": "^10.0.1", @@ -57,9 +57,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", "dev": true, "license": "MIT", "engines": { @@ -228,9 +228,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "dev": true, "license": "MIT", "dependencies": { @@ -548,9 +548,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, @@ -560,9 +560,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", "optional": true, @@ -689,29 +689,43 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -832,9 +846,9 @@ } }, "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, "license": "MIT", "engines": { @@ -1232,9 +1246,9 @@ } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", - "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.11.tgz", + "integrity": "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==", "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" @@ -1827,9 +1841,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1948,12 +1962,12 @@ } }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } @@ -2068,9 +2082,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.18", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz", - "integrity": "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==", + "version": "2.10.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz", + "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2289,9 +2303,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001787", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", - "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", "dev": true, "funding": [ { @@ -2788,9 +2802,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.335", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz", - "integrity": "sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==", + "version": "1.5.349", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz", + "integrity": "sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==", "dev": true, "license": "ISC" }, @@ -2935,18 +2949,18 @@ } }, "node_modules/eslint": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz", - "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", + "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.4", - "@eslint/config-helpers": "^0.5.4", - "@eslint/core": "^1.2.0", - "@eslint/plugin-kit": "^0.7.0", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3221,9 +3235,9 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", - "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz", + "integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==", "license": "MIT", "dependencies": { "ip-address": "10.1.0" @@ -3380,9 +3394,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -3771,9 +3785,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4906,9 +4920,9 @@ } }, "node_modules/kareem": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.2.0.tgz", - "integrity": "sha512-VS8MWZz/cT+SqBCpVfNN4zoVz5VskR3N4+sTmUXme55e9avQHntpwpNq0yjnosISXqwJ3AQVjlbI4Dyzv//JtA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.3.0.tgz", + "integrity": "sha512-kpSuLD3/7RenBnjnJdOHXCKC8dTd1JzeOiJhN0necWWci6cC+qX+VuwPnMVgb+a4+KNJSfgqahpnfWaeDXCimw==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -5214,13 +5228,13 @@ } }, "node_modules/mongodb": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.1.1.tgz", - "integrity": "sha512-067DXiMjcpYQl6bGjWQoTUEE9UoRViTtKFcoqX7z08I+iDZv/emH1g8XEFiO3qiDfXAheT5ozl1VffDTKhIW/w==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.2.0.tgz", + "integrity": "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==", "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.3.0", - "bson": "^7.1.1", + "bson": "^7.2.0", "mongodb-connection-string-url": "^7.0.0" }, "engines": { @@ -5273,13 +5287,13 @@ } }, "node_modules/mongoose": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.4.1.tgz", - "integrity": "sha512-4rFBWa+/wdBQSfvnOPJBpiSG6UCEbhSQh865dEdaH9Y8WfHBUC+I2XT28dp0IBIGrEwmh+gzrgZgea5PbmrHWA==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.6.1.tgz", + "integrity": "sha512-3T8/b0plM3ZJPW3WjlzVMIGJEYYTjgDPQ05Qzru3xu3/wOPSFKWYxdwUF2dl8h3NG5dVkzIuOkZdLacnlLf/sA==", "license": "MIT", "dependencies": { - "kareem": "3.2.0", - "mongodb": "~7.1", + "kareem": "3.3.0", + "mongodb": "~7.2", "mpath": "0.9.0", "mquery": "6.0.0", "ms": "2.1.3", @@ -5385,9 +5399,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.37", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", - "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", "dev": true, "license": "MIT" }, @@ -6715,9 +6729,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.32.2", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.2.tgz", - "integrity": "sha512-t6Ns52nS8LU2hqi0+rezMjFO1ZrCsCrnommXrU7Nfrg2va2dWahdvM6TuSwzdHpG29v6BHJyU1c/UWFhgVZzVQ==", + "version": "5.32.5", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.5.tgz", + "integrity": "sha512-7/FQfWe9A4qoyYFdAwy0chD0uDYidDp/ZT9VQ9LZlgD4AnnHJk8/+ytAA1HkJYOPySmK6helPDdJQMlcumt7HA==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -7053,6 +7067,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index a0466c0..7ade1f6 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,12 @@ ], "license": "MIT", "dependencies": { - "axios": "^1.15.0", + "axios": "^1.16.0", "bcryptjs": "^3.0.3", "cors": "^2.8.6", "express": "^5.2.1", "express-basic-auth": "^1.2.1", - "express-rate-limit": "^8.3.2", + "express-rate-limit": "^8.4.1", "express-validator": "^7.3.2", "graphql": "^16.13.2", "graphql-http": "^1.22.4", @@ -50,8 +50,8 @@ "helmet": "^8.1.0", "jsonwebtoken": "^9.0.3", "moment": "^2.30.1", - "mongodb": "^7.1.1", - "mongoose": "^9.4.1", + "mongodb": "^7.2.0", + "mongoose": "^9.6.1", "pino": "^10.3.1", "pino-pretty": "^13.1.3", "swagger-ui-express": "^5.0.1", @@ -60,7 +60,7 @@ }, "devDependencies": { "env-cmd": "^11.0.0", - "eslint": "^10.2.0", + "eslint": "^10.3.0", "jest": "^30.3.0", "jest-sonar-reporter": "^2.0.0", "node-notifier": "^10.0.1", diff --git a/src/aggregator/configuration-type.js b/src/aggregator/configuration-type.js index a65a92e..fdecb07 100644 --- a/src/aggregator/configuration-type.js +++ b/src/aggregator/configuration-type.js @@ -100,27 +100,27 @@ export const relayType = new GraphQLObjectType({ } }, verifiedByEnv: { - type: new GraphQLList(envValue), + type: new GraphQLList(envStatus), resolve: (source) => { - return resolveEnvValue(source, 'verified', Object.keys(source.verified)); + return resolveEnvValue(source, 'verified', Object.keys(source.verified || {})); } }, endpointByEnv: { type: new GraphQLList(envValue), resolve: (source) => { - return resolveEnvValue(source, 'endpoint', Object.keys(source.endpoint)); + return resolveEnvValue(source, 'endpoint', Object.keys(source.endpoint || {})); } }, statusByEnv: { type: new GraphQLList(envStatus), resolve: (source) => { - return resolveEnvValue(source, 'activated', Object.keys(source.activated)); + return resolveEnvValue(source, 'activated', Object.keys(source.activated || {})); } }, authTokenByEnv: { type: new GraphQLList(envValue), resolve: (source) => { - return resolveEnvValue(source, 'auth_token', Object.keys(source.auth_token)); + return resolveEnvValue(source, 'auth_token', Object.keys(source.auth_token || {})); } }, authPrefix: { @@ -173,6 +173,12 @@ export const configType = new GraphQLObjectType({ resolve: (source, _args, context) => { return resolveRelay(source, context); } + }, + disableMetricsByEnv: { + type: new GraphQLList(envStatus), + resolve: (source) => { + return resolveEnvValue(source, 'disable_metrics', Object.keys(source.disable_metrics)); + } } } }); diff --git a/src/app.js b/src/app.js index 08de14d..59babd8 100644 --- a/src/app.js +++ b/src/app.js @@ -10,6 +10,7 @@ import mongoose from 'mongoose'; import swaggerDocument from './api-docs/swagger-document.js'; import clientApiRouter from './routers/client-api.js'; import TimedMatch from './helpers/timed-match/index.js'; +import Cache from './helpers/cache/index.js'; import schema from './aggregator/schema.js'; import { appAuth, resourcesAuth } from './middleware/auth.js'; import { clientLimiter, defaultLimiter } from './middleware/limiter.js'; @@ -20,6 +21,13 @@ import { createServer } from './app-server.js'; */ TimedMatch.initializeWorker(); +/** + * Initialize cache + */ +const cache = Cache.getInstance(); +await cache.initializeCache(); +cache.startScheduledUpdates({ interval: Number.parseInt(process.env.CACHE_SNAPSHOT_MS) }); + /** * Express app instance */ @@ -77,7 +85,8 @@ app.get('/check', defaultLimiter, (req, res) => { metrics: process.env.METRICS_ACTIVATED, max_rpm: process.env.MAX_REQUEST_PER_MINUTE, regex_max_timeout: process.env.REGEX_MAX_TIMEOUT, - regex_max_blacklist: process.env.REGEX_MAX_BLACKLIST + regex_max_blacklist: process.env.REGEX_MAX_BLACKLIST, + cache_snapshot_ms: process.env.CACHE_SNAPSHOT_MS, }; } diff --git a/src/helpers/cache/index.js b/src/helpers/cache/index.js index fae71a7..e4b0cc5 100644 --- a/src/helpers/cache/index.js +++ b/src/helpers/cache/index.js @@ -22,7 +22,17 @@ export default class Cache { return Cache.instance; } + isEnabled() { + return process.env.CACHE_SNAPSHOT_MS !== undefined; + } + async initializeCache() { + if (!this.isEnabled()) { + Logger.info('Cache is disabled. Skipping cache initialization.'); + return; + } + + Logger.info('Cache is enabled. Initializing cache...'); const domains = await getAllDomains(); for (const domain of domains) { @@ -30,7 +40,16 @@ export default class Cache { } } + async refreshDomain(domainId) { + await this.#updateCache({ _id: domainId }); + } + async startScheduledUpdates(options = {}) { + if (!this.isEnabled()) { + Logger.info('Cache is disabled. Skipping scheduled updates.'); + return; + } + this.#workerManager = new CacheWorkerManager({ onCacheUpdates: (updates) => this.#handleCacheUpdates(updates), onCacheDeletions: (deletions) => this.#handleCacheDeletions(deletions), @@ -90,7 +109,7 @@ export default class Cache { } status() { - return this.#workerManager.getStatus(); + return this.#workerManager?.getStatus(); } get(key) { diff --git a/src/helpers/cache/query.js b/src/helpers/cache/query.js index bbc2e47..76c2eeb 100644 --- a/src/helpers/cache/query.js +++ b/src/helpers/cache/query.js @@ -1,15 +1,24 @@ export const domainQuery = (id) => ` query { domain(_id: "${id}") { + _id version + name + description statusByEnv { env value } group { + _id + name + description statusByEnv { env value } config { + _id key components statusByEnv { env value } + disableMetricsByEnv { env value } strategies { + description strategy operation values @@ -33,7 +42,7 @@ export const domainQuery = (id) => ` export function reduceSnapshot(snapshot) { const reduced = { ...snapshot }; - reduced.activated = reduced.statusByEnv.reduce((acc, { env, value }) => { + reduced.activated = reduced.statusByEnv?.reduce((acc, { env, value }) => { acc[env] = value; return acc; }, {}); @@ -48,7 +57,7 @@ export function reduceSnapshot(snapshot) { return config; }); - group.activated = group.statusByEnv.reduce((acc, { env, value }) => { + group.activated = group.statusByEnv?.reduce((acc, { env, value }) => { acc[env] = value; return acc; }, {}); @@ -61,16 +70,22 @@ export function reduceSnapshot(snapshot) { } function reduceConfig(config) { - config.activated = config.statusByEnv.reduce((acc, { env, value }) => { + config.activated = config.statusByEnv?.reduce((acc, { env, value }) => { acc[env] = value; return acc; }, {}); delete config.statusByEnv; + + config.disable_metrics = config.disableMetricsByEnv?.reduce((acc, { env, value }) => { + acc[env] = value; + return acc; + }, {}); + delete config.disableMetricsByEnv; } function reduceConfigStrategy(config) { config.strategies = config.strategies?.map(strategy => { - strategy.activated = strategy.statusByEnv.reduce((acc, { env, value }) => { + strategy.activated = strategy.statusByEnv?.reduce((acc, { env, value }) => { acc[env] = value; return acc; }, {}); @@ -88,25 +103,25 @@ function reduceRelay(config) { config.relay.auth_prefix = config.relay.authPrefix; delete config.relay.authPrefix; - config.relay.activated = config.relay.statusByEnv.reduce((acc, { env, value }) => { + config.relay.activated = config.relay.statusByEnv?.reduce((acc, { env, value }) => { acc[env] = value; return acc; }, {}); delete config.relay.statusByEnv; - config.relay.endpoint = config.relay.endpointByEnv.reduce((acc, { env, value }) => { + config.relay.endpoint = config.relay.endpointByEnv?.reduce((acc, { env, value }) => { acc[env] = value; return acc; }, {}); delete config.relay.endpointByEnv; - config.relay.auth_token = config.relay.authTokenByEnv.reduce((acc, { env, value }) => { + config.relay.auth_token = config.relay.authTokenByEnv?.reduce((acc, { env, value }) => { acc[env] = value; return acc; }, {}); delete config.relay.authTokenByEnv; - config.relay.verified = config.relay.verifiedByEnv.reduce((acc, { env, value }) => { + config.relay.verified = config.relay.verifiedByEnv?.reduce((acc, { env, value }) => { acc[env] = value; return acc; }, {}); diff --git a/src/middleware/validators.js b/src/middleware/validators.js index d59f366..db3f312 100644 --- a/src/middleware/validators.js +++ b/src/middleware/validators.js @@ -1,5 +1,6 @@ import { validationResult } from 'express-validator'; import { getConfig } from '../services/config.js'; +import Cache from '../helpers/cache/index.js'; export async function checkConfig(req, res, next) { const config = await getConfig({ domain: req.domain, key: String(req.query.key) }, true); @@ -14,8 +15,9 @@ export async function checkConfig(req, res, next) { } export async function checkConfigComponent(req, res, next) { + const cache = Cache.getInstance(); const hasComponent = req.config.components.some((c) => - c.toString() === req.componentId.toString()); + c.toString() === (cache.isEnabled() ? req.component.toString() : req.componentId.toString())); if (!hasComponent) { return res.status(401).send({ diff --git a/src/services/config.js b/src/services/config.js index 36fec1a..dbe79d8 100644 --- a/src/services/config.js +++ b/src/services/config.js @@ -1,7 +1,14 @@ +import Cache from '../helpers/cache/index.js'; import { Config } from '../models/config.js'; import { BadRequestError } from '../exceptions/index.js'; +import { getConfigFromCache } from './snapshot-cache.js'; export async function getConfig(where, lean = false) { + const cache = Cache.getInstance(); + if (cache.isEnabled()) { + return getConfigFromCache(cache, where.domain, where.key); + } + const query = Config.findOne(); query.where('domain', where.domain); diff --git a/src/services/criteria.js b/src/services/criteria.js index b3a4199..5f662e3 100644 --- a/src/services/criteria.js +++ b/src/services/criteria.js @@ -1,3 +1,4 @@ +import Cache from '../helpers/cache/index.js'; import Logger from '../helpers/logger.js'; import { ConfigStrategy, processOperation } from '../models/config-strategy.js'; import { RelayTypes } from '../models/config.js'; @@ -7,22 +8,30 @@ import GroupConfig from '../models/group-config.js'; import { addMetrics } from '../models/metric.js'; import { isRelayValid, isRelayVerified } from './config.js'; import { resolveNotification, resolveValidation } from './relay.js'; +import { findConfigStrategiesInCache, findDomainInCache, findGroupInCache } from './snapshot-cache.js'; export async function evaluateCriteria(config, context, strategyFilter) { context.config_id = config._id; const environment = context.environment; + const cache = Cache.getInstance(); let domain, group, strategies; - // Fetch domain, group and strategies in parallel - await Promise.all([ - findDomain(context.domain), - findGroup(config), - findConfigStrategies(config._id, context.domain, strategyFilter) - ]).then(result => { - domain = result[0]; - group = result[1]; - strategies = result[2]; - }); + if (cache.isEnabled()) { + domain = findDomainInCache(cache, context.domain); + group = findGroupInCache(cache, context.domain, config.group); + strategies = findConfigStrategiesInCache(cache, config.key, context.domain, group?._id); + } else { + // Fetch domain, group and strategies in parallel + await Promise.all([ + findDomain(context.domain), + findGroup(config), + findConfigStrategies(config._id, context.domain, strategyFilter) + ]).then(result => { + domain = result[0]; + group = result[1]; + strategies = result[2]; + }); + } // Prepare response object const response = { @@ -117,7 +126,7 @@ function checkStrategyInput(entry, { strategy, operation, values }, response) { async function checkRelay(config, environment, entry, response) { try { - if (config.relay?.activated[environment]) { + if (config.relay?.activated?.[environment]) { isRelayValid(config.relay); isRelayVerified(config.relay, environment); diff --git a/src/services/snapshot-cache.js b/src/services/snapshot-cache.js new file mode 100644 index 0000000..b2ba449 --- /dev/null +++ b/src/services/snapshot-cache.js @@ -0,0 +1,31 @@ +export function findDomainInCache(cache, domainId) { + const domainCache = cache.get(domainId.toString()); + return domainCache?.data; +} + +export function findGroupInCache(cache, domainId, groupId) { + const domainCache = cache.get(domainId.toString()); + return domainCache?.data.group.find(group => String(group._id) === String(groupId?.toString())); +} + +export function findConfigStrategiesInCache(cache, configKey, domainId, groupId) { + const domainCache = cache.get(domainId.toString()); + const groupCache = domainCache?.data.group.find(group => String(group._id) === String(groupId?.toString())); + const configCache = groupCache?.config.find(config => config.key === configKey); + return configCache?.strategies; +} + +export function getConfigFromCache(cache, domainId, key) { + const domainCache = cache.get(domainId.toString()); + const groups = domainCache?.data.group || []; + + for (const group of groups) { + const configFound = group.config.filter(c => c.key === key); + if (configFound.length) { + configFound[0].group = group._id; + return configFound[0]; + } + } + + return null; +} \ No newline at end of file diff --git a/tests/app.test.js b/tests/app.test.js index 9e38269..d4c110c 100644 --- a/tests/app.test.js +++ b/tests/app.test.js @@ -35,3 +35,20 @@ describe('Testing app [REST] ', () => { expect(req.statusCode).toBe(404); }); }); + +describe('Testing app cache startup', () => { + beforeAll(() => { + process.env.CACHE_SNAPSHOT_MS = '1000'; + }); + + test('APP_SUITE - Should return cache snapshot in health check details', async () => { + const req = await request(app) + .get('/check?details=1') + .expect(200); + + expect(req.statusCode).toBe(200); + expect(req.body.status).toEqual('UP'); + expect(req.body.attributes).toBeDefined(); + expect(req.body.attributes.cache_snapshot_ms).toEqual('1000'); + }); +}); \ No newline at end of file diff --git a/tests/client-api-cached-payload.test.js b/tests/client-api-cached-payload.test.js new file mode 100644 index 0000000..1fde211 --- /dev/null +++ b/tests/client-api-cached-payload.test.js @@ -0,0 +1,114 @@ +import mongoose from 'mongoose'; +import request from 'supertest'; +import app from '../src/app'; +import { StrategiesType } from '../src/models/config-strategy'; +import { EnvType } from '../src/models/environment'; +import { + setupDatabase, + apiKey, + domainDocument, + component1, + keyConfigPayload +} from './fixtures/db_client_payload'; +import Cache from '../src/helpers/cache'; + +const createRequestAuth = async () => { + return request(app) + .post('/criteria/auth') + .set('switcher-api-key', `${apiKey}`) + .send({ + domain: domainDocument.name, + component: component1.name, + environment: EnvType.DEFAULT + }); +}; + +beforeAll(async () => { + await setupDatabase(); + process.env.CACHE_SNAPSHOT_MS = 5000; +}); + +afterAll(async () => { + Cache.getInstance().stopScheduledUpdates(); + await new Promise(resolve => setTimeout(resolve, 1000)); + await mongoose.disconnect(); +}); + +describe('Testing criteria [REST] ', () => { + let token; + + beforeAll(async () => { + await Cache.getInstance().initializeCache(); + const response = await createRequestAuth(); + token = response.body.token; + }); + + test('CLIENT_CACHED_SUITE - Should return success on a payload-entry-based CRITERIA request', async () => { + const req = await request(app) + .post(`/criteria?key=${keyConfigPayload}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.PAYLOAD, + input: '{ "username": "USER_1" }' + } + ]}) + .expect(200); + + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual('Success'); + expect(req.body.result).toBe(true); + }); + + test('CLIENT_CACHED_SUITE - Should return success on a nested payload-entry-based CRITERIA request', async () => { + const req = await request(app) + .post(`/criteria?key=${keyConfigPayload}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.PAYLOAD, + input: '{ "username": "USER_1", "login": { "status": "activated" } }' + } + ]}) + .expect(200); + + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual('Success'); + expect(req.body.result).toBe(true); + }); + + test('CLIENT_CACHED_SUITE - Should return error on a payload-entry-based CRITERIA request - object input', async () => { + await request(app) + .post(`/criteria?key=${keyConfigPayload}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.PAYLOAD, + input: { username: 'USER_1' } + } + ]}) + .expect(422); + }); + + test('CLIENT_CACHED_SUITE - Should return false on an invalid payload-entry-based CRITERIA request', async () => { + const req = await request(app) + .post(`/criteria?key=${keyConfigPayload}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.PAYLOAD, + input: '{ "user": "USER_1" }' + } + ]}) + .expect(200); + + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual(`Strategy '${StrategiesType.PAYLOAD}' does not agree`); + expect(req.body.result).toBe(false); + }); + +}); \ No newline at end of file diff --git a/tests/client-api-cached.test.js b/tests/client-api-cached.test.js new file mode 100644 index 0000000..e44470b --- /dev/null +++ b/tests/client-api-cached.test.js @@ -0,0 +1,607 @@ +import mongoose from 'mongoose'; +import request from 'supertest'; +import app from '../src/app'; +import Domain from '../src/models/domain'; +import GroupConfig from '../src/models/group-config'; +import { Config } from '../src/models/config'; +import Component from '../src/models/component'; +import { ConfigStrategy, StrategiesType, OperationsType } from '../src/models/config-strategy'; +import { EnvType } from '../src/models/environment'; +import { adminMasterAccountId, groupConfigDocument } from './fixtures/db_api'; +import { Metric } from '../src/models/metric'; +import * as graphqlUtils from './graphql-utils'; +import { + setupDatabase, + apiKey, + keyConfig, + configId, + groupConfigId, + domainId, + domainDocument, + configStrategyUSERId, + component1, + configStrategyUSERDocument, + configStrategyCIDRDocument, + configStrategyTIME_GREATDocument, + configStrategyTIME_BETWEENDocument +} from './fixtures/db_client'; +import Cache from '../src/helpers/cache'; + +const changeStrategy = async (strategyId, newOperation, status, environment) => { + const strategy = await ConfigStrategy.findById(strategyId).exec(); + strategy.operation = newOperation || strategy.operation; + strategy.activated.set(environment, status === undefined ? strategy.activated.get(environment) : status); + strategy.updatedBy = adminMasterAccountId; + await strategy.save(); +}; + +const changeConfigStatus = async (configid, status, environment) => { + const config = await Config.findById(configid).exec(); + config.activated.set(environment, status === undefined ? config.activated.get(environment) : status); + config.updatedBy = adminMasterAccountId; + await config.save(); +}; + +const changeConfigDisableMetricFlag = async (configid, status, environment) => { + const config = await Config.findById(configid).exec(); + if (!config.disable_metrics) + config.disable_metrics = new Map; + + config.disable_metrics.set(environment, status); + config.updatedBy = adminMasterAccountId; + await config.save(); +}; + +const changeGroupConfigStatus = async (groupconfigid, status, environment) => { + const groupConfig = await GroupConfig.findById(groupconfigid).exec(); + groupConfig.activated.set(environment, status === undefined ? groupConfig.activated.get(environment) : status); + groupConfig.updatedBy = adminMasterAccountId; + await groupConfig.save(); +}; + +const changeDomainStatus = async (domainid, status, environment) => { + const domain = await Domain.findById(domainid).exec(); + domain.activated.set(environment, status === undefined ? domain.activated.get(environment) : status); + domain.updatedBy = adminMasterAccountId; + await domain.save(); +}; + +const createRequestAuth = async () => { + return request(app) + .post('/criteria/auth') + .set('switcher-api-key', `${apiKey}`) + .send({ + domain: domainDocument.name, + component: component1.name, + environment: EnvType.DEFAULT + }); +}; + +beforeAll(async () => { + await setupDatabase(); + process.env.CACHE_SNAPSHOT_MS = 5000; +}); + +afterAll(async () => { + Cache.getInstance().stopScheduledUpdates(); + await new Promise(resolve => setTimeout(resolve, 1000)); + await mongoose.disconnect(); +}); + +describe('Testing criteria [GraphQL]', () => { + let token; + + beforeAll(async () => { + await Cache.getInstance().initializeCache(); + const response = await createRequestAuth(); + token = response.body.token; + }); + + afterAll(setupDatabase); + + test('CLIENT_CACHED_SUITE - Should return success on a simple CRITERIA response', async () => { + const req = await request(app) + .post('/graphql') + .set('Authorization', `Bearer ${token}`) + .send(graphqlUtils.criteriaQuery(keyConfig, graphqlUtils.buildEntries([ + [StrategiesType.VALUE, 'USER_1'], + [StrategiesType.NETWORK, '10.0.0.3']])) + ); + + const expected = graphqlUtils.criteriaResult('true', 'Success'); + expect(req.statusCode).toBe(200); + expect(JSON.parse(req.text)).toMatchObject(JSON.parse(expected)); + }); + + test('CLIENT_CACHED_SUITE - Should return success on a detailed CRITERIA response', async () => { + const req = await request(app) + .post('/graphql') + .set('Authorization', `Bearer ${token}`) + .send(graphqlUtils.criteriaDetailedQuery(keyConfig, graphqlUtils.buildEntries([ + [StrategiesType.VALUE, 'USER_1'], + [StrategiesType.NETWORK, '10.0.0.3']])) + ); + + const expected = graphqlUtils.criteriaDetailedResult( + 'true', + 'Success', + EnvType.DEFAULT, + domainDocument, + groupConfigDocument, [ + configStrategyUSERDocument, + configStrategyCIDRDocument, + configStrategyTIME_BETWEENDocument, + configStrategyTIME_GREATDocument + ] + ); + + expect(req.statusCode).toBe(200); + expect(JSON.parse(req.text)).toMatchObject(JSON.parse(expected)); + }); + + test('CLIENT_CACHED_SUITE - Should NOT return success on a simple CRITERIA response - Bad login input', async () => { + const req = await request(app) + .post('/graphql') + .set('Authorization', `Bearer ${token}`) + .send(graphqlUtils.criteriaQuery(keyConfig, graphqlUtils.buildEntries([ + [StrategiesType.VALUE, 'USER_4'], + [StrategiesType.NETWORK, '10.0.0.3']])) + ); + + const expected = graphqlUtils.criteriaResult('false', `Strategy '${StrategiesType.VALUE}' does not agree`); + expect(req.statusCode).toBe(200); + expect(JSON.parse(req.text)).toMatchObject(JSON.parse(expected)); + }); + + test('CLIENT_CACHED_SUITE - Should NOT return success on a simple CRITERIA response - Missing input', async () => { + const req = await request(app) + .post('/graphql') + .set('Authorization', `Bearer ${token}`) + .send(graphqlUtils.criteriaQuery(keyConfig, graphqlUtils.buildEntries([ + [StrategiesType.VALUE, 'USER_2']])) + ); + + const expected = graphqlUtils.criteriaResult('false', `Strategy '${StrategiesType.NETWORK}' does not agree`); + expect(req.statusCode).toBe(200); + expect(JSON.parse(req.text)).toMatchObject(JSON.parse(expected)); + }); + + test('CLIENT_CACHED_SUITE - Should NOT return success on a simple CRITERIA response - Invalid KEY', async () => { + const req = await request(app) + .post('/graphql') + .set('Authorization', `Bearer ${token}`) + .send(graphqlUtils.criteriaQuery('INVALID_KEY', graphqlUtils.buildEntries([ + [StrategiesType.VALUE, 'USER_1'], + [StrategiesType.NETWORK, '10.0.0.3']])) + ); + + expect(req.statusCode).toBe(200); + expect(JSON.parse(req.text).data.criteria).toEqual(null); + }); + + test('CLIENT_CACHED_SUITE - Should return config disabled for PRD environment while activated in QA', async () => { + // Config enabled + const response = await request(app) + .post('/graphql') + .set('Authorization', `Bearer ${token}`) + .send(graphqlUtils.criteriaQuery(keyConfig, graphqlUtils.buildEntries([ + [StrategiesType.VALUE, 'USER_1'], + [StrategiesType.NETWORK, '10.0.0.3']])) + ) + .expect(200); + + const expected = graphqlUtils.criteriaResult('true', 'Success'); + expect(JSON.parse(response.text)).toMatchObject(JSON.parse(expected)); + }); + + test('CLIENT_CACHED_SUITE - It will be deactivated on default environment', async () => { + await changeConfigStatus(configId, false, EnvType.DEFAULT); + const response = await request(app) + .post('/graphql') + .set('Authorization', `Bearer ${token}`) + .send(graphqlUtils.criteriaQuery(keyConfig, graphqlUtils.buildEntries([ + [StrategiesType.VALUE, 'USER_1'], + [StrategiesType.NETWORK, '10.0.0.3']])) + ) + .expect(200); + + const expected = graphqlUtils.criteriaResult('false', 'Config disabled'); + expect(JSON.parse(response.text)).toMatchObject(JSON.parse(expected)); + }); + + test('CLIENT_CACHED_SUITE - It will be activated on QA environment', async () => { + let qaToken; + const responseToken = await request(app) + .post('/criteria/auth') + .set('switcher-api-key', `${apiKey}`) + .send({ + domain: domainDocument.name, + component: component1.name, + environment: 'QA' + }).expect(200); + qaToken = responseToken.body.token; + + await changeConfigStatus(configId, true, 'QA'); + const response = await request(app) + .post('/graphql') + .set('Authorization', `Bearer ${qaToken}`) + .send(graphqlUtils.criteriaQuery(keyConfig, graphqlUtils.buildEntries([ + [StrategiesType.VALUE, 'USER_1'], + [StrategiesType.NETWORK, '10.0.0.3']])) + ) + .expect(200); + + const expected = graphqlUtils.criteriaResult('true', 'Success'); + expect(JSON.parse(response.text)).toMatchObject(JSON.parse(expected)); + }); + + test('CLIENT_CACHED_SUITE - Should return false after changing strategy operation', async () => { + let qaToken; + const responseToken = await request(app) + .post('/criteria/auth') + .set('switcher-api-key', `${apiKey}`) + .send({ + domain: domainDocument.name, + component: component1.name, + environment: 'QA' + }).expect(200); + qaToken = responseToken.body.token; + + await changeStrategy(configStrategyUSERId, OperationsType.NOT_EXIST, true, 'QA'); + await changeStrategy(configStrategyUSERId, undefined, false, EnvType.DEFAULT); + await Cache.getInstance().refreshDomain(domainId); + + const response = await request(app) + .post('/graphql') + .set('Authorization', `Bearer ${qaToken}`) + .send(graphqlUtils.criteriaQuery(keyConfig, graphqlUtils.buildEntries([ + [StrategiesType.VALUE, 'USER_1'], + [StrategiesType.NETWORK, '10.0.0.3']])) + ) + .expect(200); + + const expected = graphqlUtils.criteriaResult('false', `Strategy '${StrategiesType.VALUE}' does not agree`); + expect(JSON.parse(response.text)).toMatchObject(JSON.parse(expected)); + }); + + test('CLIENT_CACHED_SUITE - Should return success for default environment now, since the strategy has started being specific for QA environment', async () => { + await changeConfigStatus(configId, true, EnvType.DEFAULT); + await Cache.getInstance().refreshDomain(domainId); + + const response = await request(app) + .post('/graphql') + .set('Authorization', `Bearer ${token}`) + .send(graphqlUtils.criteriaQuery(keyConfig, graphqlUtils.buildEntries([ + [StrategiesType.VALUE, 'USER_1'], + [StrategiesType.NETWORK, '10.0.0.3']])) + ) + .expect(200); + + const expected = graphqlUtils.criteriaResult('true', 'Success'); + expect(JSON.parse(response.text)).toMatchObject(JSON.parse(expected)); + }); + + test('CLIENT_CACHED_SUITE - Should return false due to Group deactivation', async () => { + await changeGroupConfigStatus(groupConfigId, false, EnvType.DEFAULT); + await Cache.getInstance().refreshDomain(domainId); + + const response = await request(app) + .post('/graphql') + .set('Authorization', `Bearer ${token}`) + .send(graphqlUtils.criteriaQuery(keyConfig, graphqlUtils.buildEntries([ + [StrategiesType.VALUE, 'USER_1'], + [StrategiesType.NETWORK, '10.0.0.3']])) + ) + .expect(200); + + const expected = graphqlUtils.criteriaResult('false', 'Group disabled'); + expect(JSON.parse(response.text)).toMatchObject(JSON.parse(expected)); + }); + + test('CLIENT_CACHED_SUITE - Should return false due to Domain deactivation', async () => { + await changeGroupConfigStatus(groupConfigId, true, EnvType.DEFAULT); + await changeDomainStatus(domainId, false, EnvType.DEFAULT); + await Cache.getInstance().refreshDomain(domainId); + + const response = await request(app) + .post('/graphql') + .set('Authorization', `Bearer ${token}`) + .send(graphqlUtils.criteriaQuery(keyConfig, graphqlUtils.buildEntries([ + [StrategiesType.VALUE, 'USER_1'], + [StrategiesType.NETWORK, '10.0.0.3']])) + ) + .expect(200); + + const expected = graphqlUtils.criteriaResult('false', 'Domain disabled'); + expect(JSON.parse(response.text)).toMatchObject(JSON.parse(expected)); + }); + + test('CLIENT_CACHED_SUITE - Should not add to metrics when Config has disabled metric flag = true', async () => { + // given + await changeConfigStatus(configId, true, EnvType.DEFAULT); + await Cache.getInstance().refreshDomain(domainId); + + //add one metric data + await request(app) + .post('/graphql') + .set('Authorization', `Bearer ${token}`) + .send(graphqlUtils.criteriaQuery(keyConfig, graphqlUtils.buildEntries([ + [StrategiesType.VALUE, 'USER_1'], + [StrategiesType.NETWORK, '10.0.0.3']])) + ) + .expect(200); + + //get total of metric data + const numMetricData = await Metric.find({ config: configId }).countDocuments().exec(); + + //disable metrics + await changeConfigDisableMetricFlag(configId, true, EnvType.DEFAULT); + await Cache.getInstance().refreshDomain(domainId); + + //call again + await request(app) + .post('/graphql') + .set('Authorization', `Bearer ${token}`) + .send(graphqlUtils.criteriaQuery(keyConfig, graphqlUtils.buildEntries([ + [StrategiesType.VALUE, 'USER_1'], + [StrategiesType.NETWORK, '10.0.0.3']])) + ) + .expect(200); + + // test + const afterNumMetricData = await Metric.find({ config: configId }).countDocuments().exec(); + expect(numMetricData === afterNumMetricData).toBe(true); + }); +}); + +describe('Testing criteria [REST] ', () => { + let token; + + beforeAll(async () => { + await Cache.getInstance().initializeCache(); + const response = await createRequestAuth(); + token = response.body.token; + }); + + test('CLIENT_CACHED_SUITE - Should return success on a entry-based CRITERIA response', async () => { + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + }]}) + .expect(200); + + expect(req.statusCode).toBe(200); + expect(req.body.strategies.length).toEqual(4); + expect(req.body.reason).toEqual('Success'); + expect(req.body.result).toBe(true); + }); + + test('CLIENT_CACHED_SUITE - Should NOT return success on a simple CRITERIA response - Missing input', async () => { + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }]}) + .expect(200); + + expect(req.statusCode).toBe(200); + expect(req.body.strategies.length).toEqual(4); + expect(req.body.reason).toEqual(`Strategy '${StrategiesType.NETWORK}' does not agree`); + expect(req.body.result).toBe(false); + }); + + test('CLIENT_CACHED_SUITE - Should NOT return success on a entry-based CRITERIA response - Missing entry', async () => { + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({}) + .expect(200); + + expect(req.statusCode).toBe(200); + expect(req.body.strategies.length).toEqual(4); + expect(req.body.reason).toEqual(`Strategy '${StrategiesType.VALUE}' did not receive any input`); + expect(req.body.result).toBe(false); + }); + + test('CLIENT_CACHED_SUITE - Should NOT return success on a entry-based CRITERIA response - Entry not an array', async () => { + await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }}) + .expect(422); + }); + + test('CLIENT_CACHED_SUITE - Should NOT return success on a entry-based CRITERIA response - Invalid Strategy', async () => { + await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: 'INVALID_STRATEGY', + input: 'USER_1' + } + ]}) + .expect(422); + }); + + test('CLIENT_CACHED_SUITE - Should NOT return success on a entry-based CRITERIA response - Missing key', async () => { + await request(app) + .post('/criteria?showReason=true&showStrategy=true') + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + } + ]}) + .expect(422); + }); + + test('CLIENT_CACHED_SUITE - Should NOT return success on a entry-based CRITERIA response - Component not registered', async () => { + // given + const component = new Component({ + _id: new mongoose.Types.ObjectId(), + name: 'Temp Component', + description: 'Temporary component', + domain: domainId, + owner: adminMasterAccountId + }); + + const generatedApiKey = await component.generateApiKey(); + const response = await request(app) + .post('/criteria/auth') + .set('switcher-api-key', `${generatedApiKey}`) + .send({ + domain: domainDocument.name, + component: component.name, + environment: EnvType.DEFAULT + }).expect(200); + + const tempToken = response.body.token; + + // test + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${tempToken}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + }]}); + + expect(req.statusCode).toBe(401); + expect(req.body.error).toEqual(`Component ${component.name} is not registered to ${keyConfig}`); + }); + + test('CLIENT_CACHED_SUITE - Should NOT return success on a simple CRITERIA response - Bad login input', async () => { + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_4' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + }]}); + + expect(req.statusCode).toBe(200); + expect(req.body.strategies).toBe(undefined); + expect(req.body.reason).toEqual(`Strategy '${StrategiesType.VALUE}' does not agree`); + expect(req.body.result).toBe(false); + }); + + test('CLIENT_CACHED_SUITE - Should NOT return success on a simple CRITERIA response - Invalid KEY', async () => { + const req = await request(app) + .post('/criteria?key=INVALID_KEY&showReason=true') + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + }]}); + + expect(req.statusCode).toBe(404); + }); + + test('CLIENT_CACHED_SUITE - Should NOT return due to a API Key change, then it should return after renewing the token', async () => { + const firstResponse = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + }]}) + .expect(200); + + expect(firstResponse.body.strategies.length).toEqual(4); + expect(firstResponse.body.reason).toEqual('Success'); + expect(firstResponse.body.result).toBe(true); + + // Change API Key + const component = await Component.findById(component1._id); + const newApiKey = await component.generateApiKey(); + + const secondResponse = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + }]}) + .expect(401); + + expect(secondResponse.body.error).toEqual('Invalid API token.'); + + const responseNewToken = await request(app) + .post('/criteria/auth') + .set('switcher-api-key', `${newApiKey}`) + .send({ + domain: domainDocument.name, + component: component1.name, + environment: EnvType.DEFAULT + }).expect(200); + + token = responseNewToken.body.token; + + await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + }]}) + .expect(200); + + }); +}); \ No newline at end of file diff --git a/tests/graphql-utils/index.js b/tests/graphql-utils/index.js index 78a293f..2ba544d 100644 --- a/tests/graphql-utils/index.js +++ b/tests/graphql-utils/index.js @@ -5,18 +5,18 @@ const isActivated = (element) => element ? 'true' : 'false'; export const domainQuery = (where, group, config, strategy) => { const query = `${where.map(createInput)}`; const elementQuery = (element) => - element != undefined ? `(activated: ${isActivated(element)})` : ''; + element == undefined ? '' : `(activated: ${isActivated(element)})`; return { query: ` { domain(${query}) { name version description activated statusByEnv { env value } - group${group !== undefined ? elementQuery(group) : ''} { + group${group == undefined ? '' : elementQuery(group)} { name description activated statusByEnv { env value } - config${config !== undefined ? elementQuery(config) : ''} { + config${config == undefined ? '' : elementQuery(config)} { key description activated statusByEnv { env value } - strategies${strategy !== undefined ? elementQuery(strategy): ''} { + strategies${strategy == undefined ? '' : elementQuery(strategy)} { strategy activated operation values } relay { diff --git a/tests/relay-cached.test.js b/tests/relay-cached.test.js new file mode 100644 index 0000000..0fa24f9 --- /dev/null +++ b/tests/relay-cached.test.js @@ -0,0 +1,547 @@ +import mongoose from 'mongoose'; +import request from 'supertest'; +import sinon from 'sinon'; +import axios from 'axios'; +import app from '../src/app'; +import { Config, RelayMethods, RelayTypes } from '../src/models/config'; +import { + setupDatabase, + apiKey, + keyConfig, + configId, + domainDocument, + component1, + configStrategyUSERId, + configStrategyCIDRId, + domainId +} from './fixtures/db_client'; +import { EnvType } from '../src/models/environment'; +import { adminMasterAccountId } from './fixtures/db_api'; +import { StrategiesType, ConfigStrategy } from '../src/models/config-strategy'; +import Cache from '../src/helpers/cache'; + +const changeStrategy = async (strategyId, newOperation, status, environment) => { + const strategy = await ConfigStrategy.findById(strategyId).exec(); + strategy.operation = newOperation || strategy.operation; + strategy.activated.set(environment, status ?? strategy.activated.get(environment)); + strategy.updatedBy = adminMasterAccountId; + await strategy.save(); +}; + +const bodyRelay = (endpoint) => { + return { + type: RelayTypes.VALIDATION, + activated: { + default: true + }, + endpoint: { + default: endpoint + }, + method: RelayMethods.GET, + verified: { + default: false + } + }; +}; + +beforeAll(async () => { + await setupDatabase(); + process.env.CACHE_SNAPSHOT_MS = 5000; +}); + +afterAll(async () => { + Cache.getInstance().stopScheduledUpdates(); + await new Promise(resolve => setTimeout(resolve, 1000)); + await mongoose.disconnect(); +}); + +describe('Testing Switcher Relay', () => { + + const bodyRelay = (method, type) => { + return { + type, + description: 'Validate input via external API', + activated: { + default: true + }, + endpoint: { + default: 'http://localhost:3001' + }, + method, + auth_prefix: 'Bearer', + auth_token: { + default: '123' + }, + verified: { + default: false + } + }; + }; + + let token; + let axiosStub; + + beforeAll(async () => { + await Cache.getInstance().initializeCache(); + const response = await request(app) + .post('/criteria/auth') + .set('switcher-api-key', `${apiKey}`) + .send({ + domain: domainDocument.name, + component: component1.name, + environment: EnvType.DEFAULT + }).expect(200); + + token = response.body.token; + }); + + afterAll(setupDatabase); + + test('RELAY_CACHED_SUITE - Should return success when validating relay using GET method', async () => { + // mock + axiosStub = sinon.stub(axios, 'get'); + + // given + const mockRelayService = { data: { result: true, message: 'A message', metadata: { custom: 'VALUE' } } }; + axiosStub.returns(Promise.resolve(mockRelayService)); + + // Setup Switcher + const config = await Config.findById(configId).exec(); + config.relay = bodyRelay(RelayMethods.GET, RelayTypes.VALIDATION); + await config.save(); + await Cache.getInstance().refreshDomain(domainId); + + // test + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + } + ]}); + + axiosStub.restore(); + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual('Success'); + expect(req.body.message).toBe('A message'); + expect(req.body.result).toBe(true); + expect(req.body.metadata).toEqual({ custom: 'VALUE' }); + }); + + test('RELAY_CACHED_SUITE - Should return failure when validating relay using GET method', async () => { + // mock + axiosStub = sinon.stub(axios, 'get'); + + // given + const mockRelayService = { data: { result: false, reason: 'Failed' } }; + axiosStub.returns(Promise.resolve(mockRelayService)); + + // Setup Switcher + const config = await Config.findById(configId).exec(); + config.relay = bodyRelay(RelayMethods.GET, RelayTypes.VALIDATION); + await config.save(); + await Cache.getInstance().refreshDomain(domainId); + + // test + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_2' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + } + ]}); + + axiosStub.restore(); + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual('Relay does not agree'); + expect(req.body.result).toBe(false); + }); + + + test('RELAY_CACHED_SUITE - Should return success when validating relay using POST method', async () => { + // mock + axiosStub = sinon.stub(axios, 'post'); + + // given + const mockRelayService = { data: { result: true, reason: 'Success' } }; + axiosStub.returns(Promise.resolve(mockRelayService)); + + // Setup Switcher + const config = await Config.findById(configId).exec(); + config.relay = bodyRelay(RelayMethods.POST, RelayTypes.VALIDATION); + await config.save(); + await Cache.getInstance().refreshDomain(domainId); + + // test + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + } + ]}); + + axiosStub.restore(); + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual('Success'); + expect(req.body.result).toBe(true); + }); + + test('RELAY_CACHED_SUITE - Should return success when notifying relay using GET method', async () => { + // mock + axiosStub = sinon.stub(axios, 'get'); + + // given - altough it's not considered after invoking the relay + const mockRelayService = { data: { result: true, reason: 'Success' } }; + axiosStub.returns(Promise.resolve(mockRelayService)); + + // Setup Switcher + const config = await Config.findById(configId).exec(); + config.relay = bodyRelay(RelayMethods.GET, RelayTypes.NOTIFICATION); + await config.save(); + await Cache.getInstance().refreshDomain(domainId); + + // test + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + } + ]}); + + axiosStub.restore(); + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual('Success'); + expect(req.body.result).toBe(true); + }); + + test('RELAY_CACHED_SUITE - Should return success when notifying relay using POST method', async () => { + // mock + axiosStub = sinon.stub(axios, 'post'); + + // given - altough it's not considered after invoking the relay + const mockRelayService = { data: { result: true, reason: 'Success' } }; + axiosStub.returns(Promise.resolve(mockRelayService)); + + // Setup Switcher + const config = await Config.findById(configId).exec(); + config.relay = bodyRelay(RelayMethods.POST, RelayTypes.NOTIFICATION); + await config.save(); + await Cache.getInstance().refreshDomain(domainId); + + // test + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + } + ]}); + + axiosStub.restore(); + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual('Success'); + expect(req.body.result).toBe(true); + }); + + test('RELAY_CACHED_SUITE - Should return success when validating relay using GET method - no input', async () => { + // mock + axiosStub = sinon.stub(axios, 'get'); + + // given + const mockRelayService = { data: { result: true, reason: 'Success' } }; + axiosStub.returns(Promise.resolve(mockRelayService)); + + // Setup Switcher + const config = await Config.findById(configId).exec(); + config.relay = bodyRelay(RelayMethods.GET, RelayTypes.NOTIFICATION); + await config.save(); + + //disabling strategy to proceed calling with no input + await changeStrategy(configStrategyUSERId, undefined, false, EnvType.DEFAULT); + await changeStrategy(configStrategyCIDRId, undefined, false, EnvType.DEFAULT); + await Cache.getInstance().refreshDomain(domainId); + + // test + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send(); + + axiosStub.restore(); + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual('Success'); + expect(req.body.result).toBe(true); + }); + + test('RELAY_CACHED_SUITE - Should return success when validating relay using POST method - no input', async () => { + // mock + axiosStub = sinon.stub(axios, 'post'); + + // given + const mockRelayService = { data: { result: true, reason: 'Success' } }; + axiosStub.returns(Promise.resolve(mockRelayService)); + + // Setup Switcher + const config = await Config.findById(configId).exec(); + config.relay = bodyRelay(RelayMethods.POST, RelayTypes.NOTIFICATION); + await config.save(); + + //disabling strategy to proceed calling with no input + await changeStrategy(configStrategyUSERId, undefined, false, EnvType.DEFAULT); + await changeStrategy(configStrategyCIDRId, undefined, false, EnvType.DEFAULT); + await Cache.getInstance().refreshDomain(domainId); + + // test + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send(); + + axiosStub.restore(); + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual('Success'); + expect(req.body.result).toBe(true); + }); + + test('RELAY_CACHED_SUITE - Should NOT return success when validating relay using GET method - Service exception', async () => { + // mock + axiosStub = sinon.stub(axios, 'get'); + axiosStub.throwsException(); + + // Setup Switcher + const config = await Config.findById(configId).exec(); + config.relay = bodyRelay(RelayMethods.GET, RelayTypes.VALIDATION); + await config.save(); + await Cache.getInstance().refreshDomain(domainId); + + // test + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send(); + + axiosStub.restore(); + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual('Relay service could not be reached: Failed to reach http://localhost:3001 via GET'); + expect(req.body.result).toBe(false); + }); + + test('RELAY_CACHED_SUITE - Should NOT return success when validating relay using POST method - Service exception', async () => { + // mock + axiosStub = sinon.stub(axios, 'post'); + axiosStub.throwsException(); + + // Setup Switcher + const config = await Config.findById(configId).exec(); + config.relay = bodyRelay(RelayMethods.POST, RelayTypes.VALIDATION); + await config.save(); + await Cache.getInstance().refreshDomain(domainId); + + // test + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send(); + + axiosStub.restore(); + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual('Relay service could not be reached: Failed to reach http://localhost:3001 via POST'); + expect(req.body.result).toBe(false); + }); + +}); + +describe('Testing Switcher Relay Validation', () => { + + let token; + + beforeAll(async () => { + await Cache.getInstance().initializeCache(); + const response = await request(app) + .post('/criteria/auth') + .set('switcher-api-key', `${apiKey}`) + .send({ + domain: domainDocument.name, + component: component1.name, + environment: EnvType.DEFAULT + }).expect(200); + + token = response.body.token; + }); + + afterAll(() => { + process.env.RELAY_BYPASS_HTTPS = true; + }); + + test('RELAY_CACHED_SUITE - Should return Relay could not be reached - Relay HTTPS required', async () => { + // given + // HTTPS not required + process.env.RELAY_BYPASS_HTTPS = true; + + // Setup Switcher + const config = await Config.findById(configId).exec(); + config.relay = bodyRelay('http://localhost:3001'); + await config.save(); + await Cache.getInstance().refreshDomain(domainId); + + // HTTPS required + process.env.RELAY_BYPASS_HTTPS = false; + + // test + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + } + ]}); + + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual('Relay service could not be reached: HTTPS required'); + }); + +}); + +describe('Testing Switcher Relay Verification', () => { + + let token; + let axiosStub; + + beforeAll(async () => { + await Cache.getInstance().initializeCache(); + const response = await request(app) + .post('/criteria/auth') + .set('switcher-api-key', `${apiKey}`) + .send({ + domain: domainDocument.name, + component: component1.name, + environment: EnvType.DEFAULT + }).expect(200); + + token = response.body.token; + }); + + afterAll(() => { + process.env.RELAY_BYPASS_VERIFICATION = true; + }); + + test('RELAY_CACHED_SUITE - Should return Relay could not be reached - Not verified', async () => { + // given + // Verification required + process.env.RELAY_BYPASS_VERIFICATION = false; + + // Setup Switcher + const config = await Config.findById(configId).exec(); + config.relay = bodyRelay('https://localhost:3001'); + await config.save(); + await Cache.getInstance().refreshDomain(domainId); + + // test + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + } + ]}); + + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual('Relay service could not be reached: Relay not verified'); + }); + + test('RELAY_CACHED_SUITE - Should return success when validating verified relay', async () => { + // mock + axiosStub = sinon.stub(axios, 'get'); + + // given + const mockRelayService = { data: { result: true, reason: 'Success' } }; + axiosStub.returns(Promise.resolve(mockRelayService)); + + // Verification required + process.env.RELAY_BYPASS_VERIFICATION = false; + + // Setup Switcher + let config = await Config.findById(configId).exec(); + config.relay = bodyRelay('https://localhost:3001'); + await config.save(); + + // Config has a verified Relay + config = await Config.findById(configId).exec(); + config.relay.verified.set(EnvType.DEFAULT, true); + await config.save(); + expect(config.relay.verified.get(EnvType.DEFAULT)).toBe(true); + await Cache.getInstance().refreshDomain(domainId); + + // test + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + } + ]}); + + axiosStub.restore(); + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual('Success'); + }); + +}); \ No newline at end of file diff --git a/tests/unit-test/cache/cache.test.js b/tests/unit-test/cache/cache.test.js index dab4ce3..d739a7a 100644 --- a/tests/unit-test/cache/cache.test.js +++ b/tests/unit-test/cache/cache.test.js @@ -14,7 +14,10 @@ afterAll(async () => { describe('Test cache', () => { - beforeAll(setupDatabase); + beforeAll(async () => { + process.env.CACHE_SNAPSHOT_MS = '1000'; + await setupDatabase(); + }); beforeEach(() => { cache = Cache.getInstance();