From fce7bb8071db10230ac9ab58fce7b68822c5bd96 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Sat, 29 Jun 2024 19:28:31 -0700 Subject: [PATCH 1/2] Fixes token expire error handler --- README.md | 18 +++++++++--------- package.json | 10 +++++----- src/exceptions/index.js | 2 +- src/middleware/auth.js | 6 +++++- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 5f0ad65..ab31a6b 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,15 @@ Switching fast. Adapt everywhere. **Switcher API** is a *Feature Flag* API with the main focus on decreasing the friction caused by changes while keeping control of what really matters. Main features: -- Control & track more using little effort by sharing switchers among application components. -- Cross environment. Generate zero impact when manipulating your project features. -- Customizable environment strategies. Setup switchers using variables per environment. -- Delegate Switcher analysis to external services with secured Switcher Relay. -- Create manageable teams to collaborate. -- Keep track of every modification and features usage. -- Detailed metrics. -- SDKs support zero-latency mode for performance improvement. -- Exclusive Slack App to control and test changes. +- Easy to setup and seemless integration with your application using our lightweight Client SDKs. +- Shareable Switchers can be used across multiple applications with high support to observability. +- Multi-environment support. Create and manage features across different environments. +- Add extra layer of verification with custom conditions using Strategies. +- Delegate Switcher criteria decision to external services with Switcher Relay. +- Support to multiple teams and granular access control. +- Integrate with Slack usign Switcher Slack App to enable release flow requests. +- Detailed metrics and logs to help you understand how your features are being used. +- Open Source and free to use. - **JS Client SDK**: (https://github.com/switcherapi/switcher-client-js) diff --git a/package.json b/package.json index e05e0f3..aabfc97 100644 --- a/package.json +++ b/package.json @@ -43,14 +43,14 @@ "express-basic-auth": "^1.2.1", "express-rate-limit": "^7.3.1", "express-validator": "^7.1.0", - "graphql": "^16.8.2", + "graphql": "^16.9.0", "graphql-http": "^1.22.1", "graphql-tag": "^2.12.6", "helmet": "^7.1.0", "jsonwebtoken": "^9.0.2", "moment": "^2.30.1", - "mongodb": "^6.7.0", - "mongoose": "^8.4.3", + "mongodb": "^6.8.0", + "mongoose": "^8.4.4", "pino": "^9.2.0", "pino-pretty": "^11.2.1", "swagger-ui-express": "^5.0.1", @@ -59,11 +59,11 @@ }, "devDependencies": { "env-cmd": "^10.1.0", - "eslint": "^9.5.0", + "eslint": "^9.6.0", "jest": "^29.7.0", "jest-sonar-reporter": "^2.0.0", "node-notifier": "^10.0.1", - "nodemon": "^3.1.3", + "nodemon": "^3.1.4", "sinon": "^18.0.0", "supertest": "^7.0.0" }, diff --git a/src/exceptions/index.js b/src/exceptions/index.js index a7ab6c9..257c0dd 100644 --- a/src/exceptions/index.js +++ b/src/exceptions/index.js @@ -45,7 +45,7 @@ export function responseException(res, err, code, feature = undefined) { } export function responseExceptionSilent(res, err, code, message) { - Logger.httpError(err.constructor.name, err.code, err.message, err); + Logger.httpError(err.constructor.name, err.code || code, message, err); if (err.code) { return res.status(err.code).send({ error: message }); diff --git a/src/middleware/auth.js b/src/middleware/auth.js index b52d1fc..895e97d 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -27,13 +27,17 @@ export async function auth(req, res, next) { req.admin = admin; next(); } catch (err) { + if (err.name === 'TokenExpiredError') { + return res.status(401).send({ error: 'Token expired.' }); + } + responseExceptionSilent(res, err, 401, 'Please authenticate.'); } } export async function authRefreshToken(req, res, next) { try { - const token = req.header('Authorization').replace('Bearer ', ''); + const token = req.header('Authorization')?.replace('Bearer ', ''); const refreshToken = req.body.refreshToken; const decodedRefreshToken = jwt.verify(refreshToken, process.env.JWT_SECRET); From 976f53157024ab1843146ad62e23c6db87c72d00 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Sun, 30 Jun 2024 16:03:47 -0700 Subject: [PATCH 2/2] Removed deprecated Client APIs (moved to Resolver Node) --- .env-cmdrc-template | 5 +- .github/workflows/master.yml | 2 - .github/workflows/re-release.yml | 2 - .github/workflows/release.yml | 2 - config/.env.dev | 5 +- docker-compose.yml | 3 - ...witcher API (dev).postman_environment.json | 14 +- requests/Switcher API.postman_collection.json | 850 +++++------------- src/api-docs/paths/path-client.js | 205 ----- src/api-docs/schemas/config-strategy.js | 30 - src/api-docs/swagger-document.js | 11 - src/app.js | 13 +- src/client/criteria-type.js | 24 - src/client/permission-resolvers.js | 4 +- src/client/relay/index.js | 94 +- src/client/resolvers.js | 139 +-- src/client/schema.js | 24 +- src/helpers/index.js | 29 - src/helpers/ipcidr.js | 15 - src/helpers/timed-match/index.js | 111 --- src/helpers/timed-match/match-proc.js | 15 - src/middleware/auth.js | 49 - src/middleware/limiter.js | 21 +- src/middleware/validators.js | 25 - src/models/component.js | 46 - src/models/config-strategy.js | 146 --- src/models/metric.js | 16 - src/routers/client-api.js | 93 -- src/services/config.js | 21 +- tests/client-api-payload.test.js | 108 --- tests/client-api.test.js | 824 ++--------------- tests/config-relay.test.js | 30 +- tests/graphql-utils/index.js | 29 +- tests/model/component.test.js | 75 -- tests/relay.test.js | 495 ---------- tests/unit-test/client/relay.test.js | 46 - tests/unit-test/config-strategy.test.js | 661 -------------- tests/unit-test/switcher-api-facade.test.js | 3 +- tests/unit-test/try-match.test.js | 90 -- 39 files changed, 347 insertions(+), 4028 deletions(-) delete mode 100644 src/api-docs/paths/path-client.js delete mode 100644 src/helpers/ipcidr.js delete mode 100644 src/helpers/timed-match/index.js delete mode 100644 src/helpers/timed-match/match-proc.js delete mode 100644 src/routers/client-api.js delete mode 100644 tests/client-api-payload.test.js delete mode 100644 tests/model/component.test.js delete mode 100644 tests/relay.test.js delete mode 100644 tests/unit-test/client/relay.test.js delete mode 100644 tests/unit-test/config-strategy.test.js delete mode 100644 tests/unit-test/try-match.test.js diff --git a/.env-cmdrc-template b/.env-cmdrc-template index b839bb4..8b68d9d 100644 --- a/.env-cmdrc-template +++ b/.env-cmdrc-template @@ -19,11 +19,8 @@ "RELAY_BYPASS_VERIFICATION": true, "PERMISSION_CACHE_ACTIVATED": true, "HISTORY_ACTIVATED": true, - "METRICS_ACTIVATED": true, "METRICS_MAX_PAGE": 50, - "REGEX_MAX_TIMEOUT": 3000, - "REGEX_MAX_BLACKLIST": 50, - "MAX_REQUEST_PER_MINUTE": 0, + "MAX_REQUEST_PER_MINUTE": 1000, "GIT_OAUTH_CLIENT_ID": "MOCK_GIT_OAUTH_CLIENT_ID", "GIT_OAUTH_SECRET": "MOCK_GIT_OAUTH_SECRET", "BITBUCKET_OAUTH_CLIENT_ID": "MOCK_BITBUCKET_OAUTH_CLIENT_ID", diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 89b73cd..f2663eb 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -52,9 +52,7 @@ jobs: RELAY_BYPASS_HTTPS: true RELAY_BYPASS_VERIFICATION: true PERMISSION_CACHE_ACTIVATED: true - METRICS_ACTIVATED: true METRICS_MAX_PAGE: 50 - MAX_REQUEST_PER_MINUTE: 0 SWITCHER_API_ENABLE: false SWITCHER_API_LOGGER: false diff --git a/.github/workflows/re-release.yml b/.github/workflows/re-release.yml index 5c736ae..2c98596 100644 --- a/.github/workflows/re-release.yml +++ b/.github/workflows/re-release.yml @@ -53,9 +53,7 @@ jobs: RELAY_BYPASS_HTTPS: true RELAY_BYPASS_VERIFICATION: true PERMISSION_CACHE_ACTIVATED: true - METRICS_ACTIVATED: true METRICS_MAX_PAGE: 50 - MAX_REQUEST_PER_MINUTE: 0 SWITCHER_API_ENABLE: false SWITCHER_API_LOGGER: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 196e367..95a6f11 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,9 +48,7 @@ jobs: RELAY_BYPASS_HTTPS: true RELAY_BYPASS_VERIFICATION: true PERMISSION_CACHE_ACTIVATED: true - METRICS_ACTIVATED: true METRICS_MAX_PAGE: 50 - MAX_REQUEST_PER_MINUTE: 0 SWITCHER_API_ENABLE: false SWITCHER_API_LOGGER: false diff --git a/config/.env.dev b/config/.env.dev index f419f57..181886f 100644 --- a/config/.env.dev +++ b/config/.env.dev @@ -12,11 +12,8 @@ MAX_STRATEGY_OPERATION=100 RELAY_BYPASS_HTTPS=true RELAY_BYPASS_VERIFICATION=true PERMISSION_CACHE_ACTIVATED=true -REGEX_MAX_TIMEOUT=3000 -REGEX_MAX_BLACKLIST=50 -MAX_REQUEST_PER_MINUTE=0 +MAX_REQUEST_PER_MINUTE=1000 HISTORY_ACTIVATED=true -METRICS_ACTIVATED=true METRICS_MAX_PAGE=50 GOOGLE_SKIP_AUTH=true SWITCHER_API_LOGGER=true diff --git a/docker-compose.yml b/docker-compose.yml index 72da5fc..d0df0cf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,10 +56,7 @@ services: - RELAY_BYPASS_VERIFICATION=${RELAY_BYPASS_VERIFICATION} - PERMISSION_CACHE_ACTIVATED=${PERMISSION_CACHE_ACTIVATED} - HISTORY_ACTIVATED=${HISTORY_ACTIVATED} - - METRICS_ACTIVATED=${METRICS_ACTIVATED} - METRICS_MAX_PAGE=${METRICS_MAX_PAGE} - - REGEX_MAX_TIMEOUT=${REGEX_MAX_TIMEOUT} - - REGEX_MAX_BLACKLIST=${REGEX_MAX_BLACKLIST} - MAX_REQUEST_PER_MINUTE=${MAX_REQUEST_PER_MINUTE} - GIT_OAUTH_CLIENT_ID=${GIT_OAUTH_CLIENT_ID} - GIT_OAUTH_SECRET=${GIT_OAUTH_SECRET} diff --git a/requests/Switcher API (dev).postman_environment.json b/requests/Switcher API (dev).postman_environment.json index 593ae83..ef98761 100644 --- a/requests/Switcher API (dev).postman_environment.json +++ b/requests/Switcher API (dev).postman_environment.json @@ -12,16 +12,6 @@ "value": "", "enabled": true }, - { - "key": "authClientToken", - "value": "", - "enabled": true - }, - { - "key": "apiKey", - "value": "", - "enabled": true - }, { "key": "refreshToken", "value": "", @@ -35,6 +25,6 @@ } ], "_postman_variable_scope": "environment", - "_postman_exported_at": "2024-06-01T05:19:53.623Z", - "_postman_exported_using": "Postman/11.1.14" + "_postman_exported_at": "2024-06-30T22:51:27.484Z", + "_postman_exported_using": "Postman/11.2.26" } \ No newline at end of file diff --git a/requests/Switcher API.postman_collection.json b/requests/Switcher API.postman_collection.json index e2fa1aa..d6c4a76 100644 --- a/requests/Switcher API.postman_collection.json +++ b/requests/Switcher API.postman_collection.json @@ -5173,659 +5173,257 @@ ] }, { - "name": "Client", + "name": "GraphQL", "item": [ { - "name": "Client - Criteria/Auth (REST)", - "event": [ + "name": "Admin", + "item": [ { - "listen": "test", - "script": { - "exec": [ - "if (pm.response.code === 200) {", - " pm.environment.set('authClientToken', pm.response.json().token)", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{apiKey}}", - "type": "string" + "name": "Domain Tree - By Id", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query Domain(\r\n $_id: String, \r\n $name: String, \r\n $activated: Boolean, \r\n $environment: String, \r\n $_component: String) {\r\n domain(\r\n _id: $_id, \r\n name: $name, \r\n activated: $activated, \r\n environment: $environment,\r\n _component: $_component\r\n ) {\r\n _id\r\n name\r\n version\r\n description\r\n statusByEnv {\r\n env\r\n value\r\n }\r\n activated\r\n group {\r\n _id\r\n name\r\n description\r\n activated\r\n statusByEnv {\r\n env\r\n value\r\n }\r\n config {\r\n _id\r\n key\r\n description\r\n activated\r\n statusByEnv {\r\n env\r\n value\r\n }\r\n strategies {\r\n _id\r\n strategy\r\n activated\r\n statusByEnv {\r\n env\r\n value\r\n }\r\n operation\r\n values\r\n }\r\n components\r\n }\r\n }\r\n }\r\n}", + "variables": "{\r\n \"_id\": \"\",\r\n \"activated\": true,\r\n \"environment\": \"\",\r\n \"_component\": \"\"\r\n}" + } }, - { - "key": "key", - "value": "switcher-api-key", - "type": "string" + "url": { + "raw": "{{url}}/adm-graphql", + "host": [ + "{{url}}" + ], + "path": [ + "adm-graphql" + ] } - ] + }, + "response": [] }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json", - "name": "Content-Type", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"domain\": \"Playground\",\n\t\"component\": \"switcher-playground\",\n\t\"environment\": \"default\"\n}", - "options": { - "raw": { - "language": "json" + { + "name": "Domain Tree - By Name", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query Domain(\r\n $_id: String, \r\n $name: String, \r\n $activated: Boolean, \r\n $environment: String, \r\n $_component: String) {\r\n domain(\r\n _id: $_id, \r\n name: $name, \r\n activated: $activated, \r\n environment: $environment,\r\n _component: $_component\r\n ) {\r\n _id\r\n name\r\n version\r\n description\r\n statusByEnv {\r\n env\r\n value\r\n }\r\n activated\r\n group {\r\n _id\r\n name\r\n description\r\n activated\r\n statusByEnv {\r\n env\r\n value\r\n }\r\n config {\r\n _id\r\n key\r\n description\r\n activated\r\n statusByEnv {\r\n env\r\n value\r\n }\r\n strategies {\r\n _id\r\n strategy\r\n activated\r\n statusByEnv {\r\n env\r\n value\r\n }\r\n operation\r\n values\r\n }\r\n components\r\n }\r\n }\r\n }\r\n}", + "variables": "{\r\n \"name\": \"GitOps\",\r\n \"activated\": true,\r\n \"environment\": \"\",\r\n \"_component\": \"\"\r\n}" + } + }, + "url": { + "raw": "{{url}}/adm-graphql", + "host": [ + "{{url}}" + ], + "path": [ + "adm-graphql" + ] } - } + }, + "response": [] }, - "url": { - "raw": "{{url}}/criteria/auth", - "host": [ - "{{url}}" - ], - "path": [ - "criteria", - "auth" - ], - "query": [ - { - "key": "showReason", - "value": "true", - "disabled": true - }, - { - "key": "showStrategy", - "value": "true", - "disabled": true + { + "name": "Configuration - By Switcher Key", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query Configuration(\r\n $domain: String!, \r\n $group: String,\r\n $group_id: String,\r\n $key: String,\r\n $config_id: String,\r\n $slack_team_id: String,\r\n $environment: String) {\r\n configuration(domain: $domain, group: $group, group_id: $group_id, key: $key, config_id: $config_id, slack_team_id: $slack_team_id, environment: $environment) {\r\n domain {\r\n id: _id\r\n name\r\n owner\r\n transfer\r\n activated\r\n integrations {\r\n slack\r\n }\r\n }\r\n group {\r\n _id\r\n name\r\n activated\r\n statusByEnv {\r\n env\r\n value\r\n }\r\n }\r\n config {\r\n _id\r\n key\r\n activated\r\n }\r\n }\r\n}", + "variables": "{\r\n \"domain\": \"\",\r\n \"group\": \"\",\r\n \"group_id\": \"\",\r\n \"key\": \"FEATURE_1\",\r\n \"config_id\": \"\",\r\n \"slack_team_id\": \"\",\r\n \"environment\": \"default\"\r\n}" + } }, - { - "key": "bypassMetric", - "value": "true", - "disabled": true - } - ] - } - }, - "response": [] - }, - { - "name": "Client - Criteria (REST) - Input", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{authClientToken}}", - "type": "string" + "url": { + "raw": "{{url}}/adm-graphql", + "host": [ + "{{url}}" + ], + "path": [ + "adm-graphql" + ] } - ] + }, + "response": [] }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json", - "name": "Content-Type", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"entry\": [\n\t\t{\n\t\t\t\"strategy\": \"PAYLOAD_VALIDATION\",\n\t\t\t\"input\": \"{ \\\"status\\\": \\\"ready\\\" }\"\n\t\t}]\n}", - "options": { - "raw": { - "language": "json" + { + "name": "Configuration - By Group Name", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query Configuration(\r\n $domain: String!, \r\n $group: String,\r\n $group_id: String,\r\n $key: String,\r\n $config_id: String,\r\n $slack_team_id: String,\r\n $environment: String) {\r\n configuration(domain: $domain, group: $group, group_id: $group_id, key: $key, config_id: $config_id, slack_team_id: $slack_team_id, environment: $environment) {\r\n domain {\r\n id: _id\r\n name\r\n owner\r\n transfer\r\n activated\r\n integrations {\r\n slack\r\n }\r\n }\r\n group {\r\n _id\r\n name\r\n activated\r\n statusByEnv {\r\n env\r\n value\r\n }\r\n }\r\n config {\r\n _id\r\n key\r\n activated\r\n }\r\n }\r\n}", + "variables": "{\r\n \"domain\": \"\",\r\n \"group\": \"Group 1\",\r\n \"group_id\": \"\",\r\n \"key\": \"\",\r\n \"config_id\": \"\",\r\n \"slack_team_id\": \"\",\r\n \"environment\": \"default\"\r\n}" + } + }, + "url": { + "raw": "{{url}}/adm-graphql", + "host": [ + "{{url}}" + ], + "path": [ + "adm-graphql" + ] } - } + }, + "response": [] }, - "url": { - "raw": "{{url}}/criteria?showReason=true&showStrategy=true&key=MY_SWITCHER", - "host": [ - "{{url}}" - ], - "path": [ - "criteria" - ], - "query": [ - { - "key": "showReason", - "value": "true" + { + "name": "Configuration - By Slack Team Id", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query Configuration(\r\n $domain: String!, \r\n $group: String,\r\n $group_id: String,\r\n $key: String,\r\n $config_id: String,\r\n $slack_team_id: String,\r\n $environment: String) {\r\n configuration(domain: $domain, group: $group, group_id: $group_id, key: $key, config_id: $config_id, slack_team_id: $slack_team_id, environment: $environment) {\r\n domain {\r\n id: _id\r\n name\r\n owner\r\n transfer\r\n activated\r\n integrations {\r\n slack\r\n }\r\n }\r\n group {\r\n _id\r\n name\r\n activated\r\n statusByEnv {\r\n env\r\n value\r\n }\r\n }\r\n config {\r\n _id\r\n key\r\n activated\r\n }\r\n }\r\n}", + "variables": "{\r\n \"domain\": \"\",\r\n \"group\": \"\",\r\n \"group_id\": \"\",\r\n \"key\": \"\",\r\n \"config_id\": \"\",\r\n \"slack_team_id\": \"T123456789\",\r\n \"environment\": \"default\"\r\n}" + } }, - { - "key": "showStrategy", - "value": "true" + "url": { + "raw": "{{url}}/adm-graphql", + "host": [ + "{{url}}" + ], + "path": [ + "adm-graphql" + ] + } + }, + "response": [] + }, + { + "name": "Permission - By Router Switcher", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query Permission(\r\n $domain: String!, $actions: [String]!, $router: String!, \r\n $parent: String, $environment: String) {\r\n permission(\r\n domain: $domain, \r\n parent: $parent, \r\n actions: $actions,\r\n router: $router,\r\n environment: $environment\r\n ) {\r\n id\r\n name\r\n permissions {\r\n action\r\n result\r\n }\r\n }\r\n}", + "variables": "{\r\n \"domain\": \"\",\r\n \"parent\": \"\",\r\n \"actions\": [\"READ\", \"UPDATE\", \"DELETE\"],\r\n \"router\": \"SWITCHER\",\r\n \"environment\": \"\"\r\n}" + } }, - { - "key": "bypassMetric", - "value": "true", - "disabled": true + "url": { + "raw": "{{url}}/adm-graphql", + "host": [ + "{{url}}" + ], + "path": [ + "adm-graphql" + ] + } + }, + "response": [] + }, + { + "name": "Permission - By Router Grroup", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query Permission(\r\n $domain: String!, $actions: [String]!, $router: String!, \r\n $parent: String, $environment: String) {\r\n permission(\r\n domain: $domain, \r\n parent: $parent, \r\n actions: $actions,\r\n router: $router,\r\n environment: $environment\r\n ) {\r\n id\r\n name\r\n permissions {\r\n action\r\n result\r\n }\r\n }\r\n}", + "variables": "{\r\n \"domain\": \"\",\r\n \"actions\": [\"READ\", \"UPDATE\", \"DELETE\"],\r\n \"router\": \"GROUP\",\r\n \"environment\": \"\"\r\n}" + } }, - { - "key": "key", - "value": "MY_SWITCHER" + "url": { + "raw": "{{url}}/adm-graphql", + "host": [ + "{{url}}" + ], + "path": [ + "adm-graphql" + ] } - ] + }, + "response": [] } - }, - "response": [] + ] }, { - "name": "Client - Criteria (REST)", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{authClientToken}}", - "type": "string" + "name": "Slack", + "item": [ + { + "name": "Configuration - Env", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query Configuration($slack_team_id: String!) {\r\n configuration(slack_team_id: $slack_team_id) {\r\n environments\r\n }\r\n}", + "variables": "{\r\n \"slack_team_id\": \"TEAM_ID\"\r\n}" + } + }, + "url": { + "raw": "{{url}}/adm-graphql", + "host": [ + "{{url}}" + ], + "path": [ + "adm-graphql" + ] } - ] + }, + "response": [] }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json", - "name": "Content-Type", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" + { + "name": "Configuration - Groups", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query Configuration($slack_team_id: String!, $environment: String) {\r\n configuration(slack_team_id: $slack_team_id, environment: $environment) {\r\n group {\r\n name\r\n activated\r\n }\r\n }\r\n}", + "variables": "{\r\n \"slack_team_id\": \"TEAM_ID\",\r\n \"environment\": \"development\"\r\n}" + } + }, + "url": { + "raw": "{{url}}/adm-graphql", + "host": [ + "{{url}}" + ], + "path": [ + "adm-graphql" + ] } - } + }, + "response": [] }, - "url": { - "raw": "{{url}}/criteria?key=MY_SWITCHER", - "host": [ - "{{url}}" - ], - "path": [ - "criteria" - ], - "query": [ - { - "key": "showReason", - "value": "true", - "disabled": true - }, - { - "key": "showStrategy", - "value": "true", - "disabled": true - }, - { - "key": "bypassMetric", - "value": "true", - "disabled": true + { + "name": "Configuration - Configs", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query Configuration($slack_team_id: String!, $environment: String, $group: String) {\r\n configuration(slack_team_id: $slack_team_id, environment: $environment, group: $group) {\r\n config {\r\n key\r\n activated\r\n }\r\n }\r\n}", + "variables": "{\r\n \"slack_team_id\": \"TEAM_ID\",\r\n \"group\": \"Group Name\",\r\n \"environment\": \"default\"\r\n}" + } }, - { - "key": "key", - "value": "MY_SWITCHER" + "url": { + "raw": "{{url}}/adm-graphql", + "host": [ + "{{url}}" + ], + "path": [ + "adm-graphql" + ] } - ] + }, + "response": [] } - }, - "response": [] - }, - { - "name": "Client - Check Snapshot", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{authClientToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json", - "name": "Content-Type", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{url}}/criteria/snapshot_check/:version", - "host": [ - "{{url}}" - ], - "path": [ - "criteria", - "snapshot_check", - ":version" - ], - "variable": [ - { - "key": "version", - "value": "1588557288037" - } - ] - } - }, - "response": [] - }, - { - "name": "Client - Check Switcher/Component", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{authClientToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json", - "name": "Content-Type", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"switchers\": [\r\n \"FEATURE2020\",\r\n \"RELAY_1\"\r\n ]\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{url}}/criteria/switchers_check", - "host": [ - "{{url}}" - ], - "path": [ - "criteria", - "switchers_check" - ] - } - }, - "response": [] - }, - { - "name": "Client - Criteria (GraphQL)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{authClientToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "graphql", - "graphql": { - "query": "{\r\n criteria(\r\n key: \"MY_SWITCHER\",\r\n # bypassMetric: false,\r\n entry: [\r\n {\r\n strategy: \"VALUE_VALIDATION\", \r\n input: \"Roger\"\r\n }\r\n ]\r\n ) {\r\n key\r\n activated\r\n response {\r\n result\r\n reason\r\n domain {\r\n name\r\n activated\r\n description\r\n group {\r\n name\r\n activated\r\n description\r\n config {\r\n key\r\n activated\r\n description\r\n strategies {\r\n strategy\r\n activated\r\n operation\r\n values\r\n }\r\n }\r\n }\r\n }\r\n group {\r\n name\r\n activated\r\n description\r\n }\r\n strategies {\r\n strategy\r\n activated\r\n operation\r\n values\r\n }\r\n }\r\n }\r\n}", - "variables": "" - } - }, - "url": { - "raw": "{{url}}/graphql", - "host": [ - "{{url}}" - ], - "path": [ - "graphql" - ] - } - }, - "response": [] - }, - { - "name": "Client - Simple Criteria (GraphQL)", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{authClientToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "graphql", - "graphql": { - "query": "{\r\n criteria(\r\n key: \"MY_SWITCHER\", \r\n entry: [\r\n {\r\n strategy: \"VALUE_VALIDATION\", \r\n input: \"Roger\"\r\n },\r\n {\r\n strategy: \"NETWORK_VALIDATION\", \r\n input: \"192.168.0.2\"\r\n }\r\n ]\r\n ) {\r\n response {\r\n result\r\n reason\r\n }\r\n }\r\n}", - "variables": "" - } - }, - "url": { - "raw": "{{url}}/graphql", - "host": [ - "{{url}}" - ], - "path": [ - "graphql" - ] - } - }, - "response": [] - }, - { - "name": "Client - Domain Tree (GraphQL)", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{authClientToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "graphql", - "graphql": { - "query": "{\r\n domain(name: \"My Domain\", activated: true) {\r\n name\r\n version\r\n description\r\n activated\r\n group(activated: true) {\r\n name\r\n description\r\n activated\r\n config(activated: true) {\r\n key\r\n description\r\n activated\r\n strategies(activated: true) {\r\n strategy\r\n activated\r\n operation\r\n values\r\n }\r\n components\r\n }\r\n }\r\n }\r\n}", - "variables": "" - } - }, - "url": { - "raw": "{{url}}/graphql", - "host": [ - "{{url}}" - ], - "path": [ - "graphql" - ] - } - }, - "response": [] - }, - { - "name": "Client - Domain Tree (Adm-GraphQL)", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "graphql", - "graphql": { - "query": "{\r\n # domain(name: \"Switcher API\") {\r\n # domain(name: \"My Domain\", environment: \"default\", component: \"InventoryWS\") {\r\n domain(_id: \"5e44eb76916dd10048d72542\", environment: \"default\", _component: \"switcherapi\") {\r\n _id\r\n name\r\n version\r\n description\r\n statusByEnv {\r\n env\r\n value\r\n }\r\n activated\r\n group {\r\n _id\r\n name\r\n description\r\n activated\r\n statusByEnv {\r\n env\r\n value\r\n }\r\n config {\r\n _id\r\n key\r\n description\r\n activated\r\n statusByEnv {\r\n env\r\n value\r\n }\r\n strategies {\r\n _id\r\n strategy\r\n activated\r\n statusByEnv {\r\n env\r\n value\r\n }\r\n operation\r\n values\r\n }\r\n components\r\n }\r\n }\r\n }\r\n}", - "variables": "" - } - }, - "url": { - "raw": "{{url}}/adm-graphql", - "host": [ - "{{url}}" - ], - "path": [ - "adm-graphql" - ] - } - }, - "response": [] - }, - { - "name": "Client - Config Tree (Adm-GraphQL)", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "graphql", - "graphql": { - "query": "{\r\n # domain(name: \"Switcher API\") {\r\n domain(_id: \"5e0ece606f4f994eac9007ae\", environment: \"default\") {\r\n group {\r\n config {\r\n key\r\n }\r\n }\r\n }\r\n}", - "variables": "" - } - }, - "url": { - "raw": "{{url}}/adm-graphql", - "host": [ - "{{url}}" - ], - "path": [ - "adm-graphql" - ] - } - }, - "response": [] - }, - { - "name": "Client - Configuration (GraphQL)", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{authClientToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "graphql", - "graphql": { - "query": "{\r\n # configuration(key: \"FEATURE01\") { #FF3FOR2020\r\n configuration(group: \"Release 1\") {\r\n domain {\r\n name\r\n description\r\n activated\r\n }\r\n group {\r\n name\r\n description\r\n activated\r\n }\r\n config {\r\n key\r\n description\r\n activated\r\n }\r\n strategies {\r\n strategy\r\n activated\r\n operation\r\n values\r\n }\r\n }\r\n}", - "variables": "" - } - }, - "url": { - "raw": "{{url}}/graphql", - "host": [ - "{{url}}" - ], - "path": [ - "graphql" - ] - } - }, - "response": [] - }, - { - "name": "Client - Configuration (Adm-GraphQL)", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "graphql", - "graphql": { - "query": "{\r\n # configuration(domain: \"62d47c7555cb4611d17b2b55\") {\r\n # configuration(domain: \"5e4f9d8966e06b59c8254500\") {\r\n # configuration(domain: \"5e4f9d8966e06b59c8254500\", config_id: \"6270b2fe68bceea783c450b4\") {\r\n # configuration(domain: \"5e4f9d8966e06b59c8254500\", group_id: \"6270b2f568bceea783c45081\") {\r\n # configuration(domain: \"5e4f9d8966e06b59c8254500\", group: \"Test group\") {\r\n configuration(domain: \"5e0ece606f4f994eac9007ae\", key: \"FEATURE01\", environment: \"default\") {\r\n domain {\r\n id: _id\r\n name\r\n owner\r\n transfer\r\n activated\r\n integrations {\r\n slack\r\n }\r\n }\r\n group {\r\n _id\r\n name\r\n activated\r\n statusByEnv {\r\n env\r\n value\r\n }\r\n }\r\n config {\r\n _id\r\n key\r\n activated\r\n }\r\n }\r\n}", - "variables": "" - } - }, - "url": { - "raw": "{{url}}/adm-graphql", - "host": [ - "{{url}}" - ], - "path": [ - "adm-graphql" - ] - } - }, - "response": [] - }, - { - "name": "Client - Configuration (Slack-GraphQL) - Env", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "graphql", - "graphql": { - "query": "{\r\n configuration(slack_team_id: \"TEAM_ID\") {\r\n environments\r\n }\r\n}", - "variables": "" - } - }, - "url": { - "raw": "{{url}}/adm-graphql", - "host": [ - "{{url}}" - ], - "path": [ - "adm-graphql" - ] - } - }, - "response": [] - }, - { - "name": "Client - Configuration (Slack-GraphQL) - Groups", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "graphql", - "graphql": { - "query": "{\r\n configuration(slack_team_id: \"TEAM_ID\", environment: \"development\") {\r\n group {\r\n name\r\n activated\r\n }\r\n }\r\n}", - "variables": "" - } - }, - "url": { - "raw": "{{url}}/adm-graphql", - "host": [ - "{{url}}" - ], - "path": [ - "adm-graphql" - ] - } - }, - "response": [] - }, - { - "name": "Client - Configuration (Slack-GraphQL) - Configs", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "graphql", - "graphql": { - "query": "{\r\n configuration(slack_team_id: \"TEAM_ID\", environment: \"default\", group: \"Release 1\") {\r\n config {\r\n key\r\n activated\r\n }\r\n }\r\n}", - "variables": "" - } - }, - "url": { - "raw": "{{url}}/adm-graphql", - "host": [ - "{{url}}" - ], - "path": [ - "adm-graphql" - ] - } - }, - "response": [] - }, - { - "name": "Client - Configuration by Group (GraphQL)", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{authClientToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "graphql", - "graphql": { - "query": "{\r\n #configuration(group: \"Rollout 2020\") {\r\n configuration(key: \"FF2FOR2030\") { #FF3FOR2020\r\n domain {\r\n name\r\n description\r\n activated\r\n }\r\n group {\r\n name\r\n description\r\n activated\r\n }\r\n config {\r\n key\r\n description\r\n activated\r\n strategies {\r\n strategy\r\n values\r\n }\r\n }\r\n strategies {\r\n strategy\r\n activated\r\n operation\r\n values\r\n }\r\n }\r\n}", - "variables": "" - } - }, - "url": { - "raw": "{{url}}/graphql", - "host": [ - "{{url}}" - ], - "path": [ - "graphql" - ] - } - }, - "response": [] - }, - { - "name": "Client - Permission (Adm-GraphQL)", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "graphql", - "graphql": { - "query": "{\r\n # permission(domain: \"5e0ece606f4f994eac9007ae\", actions: [\"UPDATE\", \"DELETE\"], router: \"GROUP\") {\r\n permission(\r\n domain: \"601de9281e38730048dd0988\", \r\n parent: \"601dea391e38730048dd098a\", \r\n actions: [\"READ\", \"UPDATE\", \"DELETE\"],\r\n router: \"SWITCHER\") {\r\n id\r\n name\r\n permissions {\r\n action\r\n result\r\n }\r\n }\r\n}", - "variables": "" - } - }, - "url": { - "raw": "{{url}}/adm-graphql", - "host": [ - "{{url}}" - ], - "path": [ - "adm-graphql" - ] - } - }, - "response": [] + ] } ] }, diff --git a/src/api-docs/paths/path-client.js b/src/api-docs/paths/path-client.js deleted file mode 100644 index 2ed6748..0000000 --- a/src/api-docs/paths/path-client.js +++ /dev/null @@ -1,205 +0,0 @@ -import { StrategiesType } from '../../models/config-strategy.js'; -import { pathParameter, queryParameter } from '../schemas/common.js'; - -export default { - '/criteria': { - post: { - tags: ['Client API'], - description: 'Execute criteria query against the API settings', - security: [{ componentAuth: [] }], - parameters: [ - queryParameter('key', 'Switcher Key', true, 'string'), - queryParameter('showReason', 'Show criteria execution reason (default: true)', false, 'boolean'), - queryParameter('showStrategy', 'Show criteria execution strategy (default: true)', false, 'boolean'), - queryParameter('bypassMetric', 'Bypass metric check (default: true)', false, 'boolean') - ], - requestBody: { - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - entry: { - type: 'array', - items: { - type: 'object', - properties: { - strategy: { - type: 'string', - enum: Object.values(StrategiesType) - }, - input: { - type: 'string' - } - } - } - } - } - } - } - } - }, - responses: { - 200: { - description: 'Success', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - result: { - type: 'boolean' - }, - reason: { - type: 'string' - }, - metadata: { - type: 'object' - }, - strategies: { - type: 'array', - items: { - $ref: '#/components/schemas/ConfigStrategyCriteriaResponse' - } - } - } - } - } - } - } - } - } - }, - '/criteria/snapshot_check/{version}': { - get: { - tags: ['Client API'], - description: 'Check if snapshot version is up to date', - security: [{ componentAuth: [] }], - parameters: [ - pathParameter('version', 'Snapshot version', true) - ], - responses: { - 200: { - description: 'Success', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - status: { - type: 'boolean', - description: 'true if snapshot version is up to date' - } - } - } - } - } - } - } - } - }, - '/criteria/switchers_check': { - post: { - tags: ['Client API'], - description: 'Check if switcher keys are valid', - security: [{ componentAuth: [] }], - requestBody: { - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - switchers: { - type: 'array', - items: { - type: 'string' - } - } - } - } - } - } - }, - responses: { - 200: { - description: 'Success', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - not_found: { - type: 'array', - items: { - type: 'string' - } - } - } - } - } - } - } - } - } - }, - '/criteria/auth': { - post: { - tags: ['Client API'], - description: 'Authenticate component', - security: [{ apiKey: [] }], - requestBody: { - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - domain: { - type: 'string', - description: 'Domain name' - }, - component: { - type: 'string', - description: 'Component name' - }, - enviroment: { - type: 'string', - description: 'Enviroment name' - } - } - } - } - } - }, - responses: { - 200: { - description: 'Success', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - token: { - type: 'string', - description: 'Authentication token' - }, - exp: { - type: 'number', - description: 'Expiration time' - } - } - } - } - } - } - } - } - } -}; - - - - - - - - diff --git a/src/api-docs/schemas/config-strategy.js b/src/api-docs/schemas/config-strategy.js index e13c28d..1b4a6f4 100644 --- a/src/api-docs/schemas/config-strategy.js +++ b/src/api-docs/schemas/config-strategy.js @@ -72,36 +72,6 @@ const configStrategy = { export default { ConfigStrategy: configStrategy, - ConfigStrategyCriteriaResponse: { - type: 'object', - properties: { - description: { - type: 'string', - description: 'The description of the config strategy' - }, - activated: { - type: 'object', - additionalProperties: { - type: 'boolean', - description: 'The environment status' - } - }, - strategy: { - type: 'string', - enum: Object.values(StrategiesType) - }, - values: { - type: 'array', - items: { - type: 'string' - } - }, - operation: { - type: 'string', - enum: Object.values(OperationsType) - } - } - }, ConfigStrategyCreateRequest: { type: 'object', properties: { diff --git a/src/api-docs/swagger-document.js b/src/api-docs/swagger-document.js index ecad578..5a2821d 100644 --- a/src/api-docs/swagger-document.js +++ b/src/api-docs/swagger-document.js @@ -9,7 +9,6 @@ import pathTeam from './paths/path-team.js'; import pathPermission from './paths/path-permission.js'; import pathMetric from './paths/path-metric.js'; import pathSlack from './paths/path-slack.js'; -import pathClient from './paths/path-client.js'; import { commonSchema } from './schemas/common.js'; import adminSchema from './schemas/admin.js'; @@ -49,15 +48,6 @@ export default { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' - }, - componentAuth: { - type: 'http', - scheme: 'bearer' - }, - apiKey: { - type: 'apiKey', - in: 'header', - name: 'switcher-api-key' } }, schemas: { @@ -86,7 +76,6 @@ export default { ...pathTeam, ...pathPermission, ...pathMetric, - ...pathClient, ...pathSlack } }; \ No newline at end of file diff --git a/src/app.js b/src/app.js index e7c367a..2a06f25 100644 --- a/src/app.js +++ b/src/app.js @@ -8,7 +8,6 @@ import './db/mongoose.js'; import mongoose from 'mongoose'; import swaggerDocument from './api-docs/swagger-document.js'; -import clientApiRouter from './routers/client-api.js'; import adminRouter from './routers/admin.js'; import environment from './routers/environment.js'; import component from './routers/component.js'; @@ -21,8 +20,8 @@ import teamRouter from './routers/team.js'; import permissionRouter from './routers/permission.js'; import slackRouter from './routers/slack.js'; import schema from './client/schema.js'; -import { componentAuth, auth, resourcesAuth, slackAuth, gitopsAuth } from './middleware/auth.js'; -import { clientLimiter, defaultLimiter } from './middleware/limiter.js'; +import { auth, resourcesAuth, slackAuth, gitopsAuth } from './middleware/auth.js'; +import { DEFAULT_RATE_LIMIT, defaultLimiter } from './middleware/limiter.js'; import { createServer } from './app-server.js'; const app = express(); @@ -38,7 +37,6 @@ app.disable('x-powered-by'); /** * API Routes */ -app.use(clientApiRouter); app.use(adminRouter); app.use(component); app.use(environment); @@ -58,8 +56,6 @@ app.use(slackRouter); const handler = (req, res, next) => createHandler({ schema, context: req })(req, res, next); -// Component: Client API -app.use('/graphql', componentAuth, clientLimiter, handler); // Admin: Client API app.use('/adm-graphql', auth, defaultLimiter, handler); // Slack: Client API @@ -98,12 +94,9 @@ app.get('/check', defaultLimiter, (req, res) => { relay_bypass_verification: process.env.RELAY_BYPASS_VERIFICATION, permission_cache: process.env.PERMISSION_CACHE_ACTIVATED, history: process.env.HISTORY_ACTIVATED, - metrics: process.env.METRICS_ACTIVATED, max_metrics_pages: process.env.METRICS_MAX_PAGE, max_stretegy_op: process.env.MAX_STRATEGY_OPERATION, - max_rpm: process.env.MAX_REQUEST_PER_MINUTE, - regex_max_timeout: process.env.REGEX_MAX_TIMEOUT, - regex_max_blacklist: process.env.REGEX_MAX_BLACKLIST + max_rpm: process.env.MAX_REQUEST_PER_MINUTE || DEFAULT_RATE_LIMIT }; } diff --git a/src/client/criteria-type.js b/src/client/criteria-type.js index 3a31cdf..d1cb446 100644 --- a/src/client/criteria-type.js +++ b/src/client/criteria-type.js @@ -1,7 +1,5 @@ import { GraphQLObjectType, GraphQLString, GraphQLList, GraphQLBoolean, GraphQLNonNull, GraphQLInputObjectType } from 'graphql'; import { domainType, groupConfigType, strategyType } from './configuration-type.js'; -import { EnvType } from '../models/environment.js'; -import { resolveCriteria } from './resolvers.js'; export const strategyInputType = new GraphQLInputObjectType({ name: 'StrategyInput', @@ -34,26 +32,4 @@ export const responseType = new GraphQLObjectType({ type: new GraphQLList(strategyType) } } -}); - -export const criteriaType = new GraphQLObjectType({ - name: 'Criteria', - fields: { - key: { - type: GraphQLString - }, - activated: { - type: GraphQLBoolean, - resolve: (source, _args, { environment }) => { - return source.activated[`${environment}`] === undefined ? - source.activated[`${EnvType.DEFAULT}`] : source.activated[`${environment}`]; - } - }, - response: { - type: responseType, - resolve: (source, _params, context) => { - return resolveCriteria(source, context); - } - } - } }); \ No newline at end of file diff --git a/src/client/permission-resolvers.js b/src/client/permission-resolvers.js index c2c768f..aff6e52 100644 --- a/src/client/permission-resolvers.js +++ b/src/client/permission-resolvers.js @@ -42,11 +42,11 @@ export async function resolvePermission(args, admin) { } const getElements = async (domain, parent, router) => { - if (router === RouterTypes.GROUP) { + if (domain && router === RouterTypes.GROUP) { return getGroupConfigs({ domain }, true); } - if (router === RouterTypes.CONFIG) { + if (domain && parent && router === RouterTypes.CONFIG) { return getConfigs({ domain, group: parent }, true); } diff --git a/src/client/relay/index.js b/src/client/relay/index.js index 1542b0d..9c858d0 100644 --- a/src/client/relay/index.js +++ b/src/client/relay/index.js @@ -1,6 +1,5 @@ import axios from 'axios'; import https from 'https'; -import { StrategiesToRelayDataType, RelayMethods } from '../../models/config.js'; import { checkHttpsAgent } from '../../external/switcher-api-facade.js'; import Logger from '../../helpers/logger.js'; @@ -9,94 +8,23 @@ const agent = async (url) => { return new https.Agent({ rejectUnauthorized: !(response?.result) }); }; -export async function resolveNotification(relay, entry, environment) { - const env = Object.keys(relay.endpoint).find(e => e === environment); - const url = relay.endpoint[env]; - const header = createHeader(relay.auth_prefix, relay.auth_token, env); - - if (relay.method === RelayMethods.GET) { - get(url, createParams(entry), header); - } else { - post(url, createBody(entry), header); - } -} - -export async function resolveValidation(relay, entry, environment) { - let response; - - const env = Object.keys(relay.endpoint).find(e => e === environment); - const url = relay.endpoint[env]; - const header = createHeader(relay.auth_prefix, relay.auth_token, env); - - if (relay.method === RelayMethods.GET) { - response = await get(url, createParams(entry), header); - } else { - response = await post(url, createBody(entry), header); - } - - return { - result: response.data.result, - message: response.data.message, - metadata: response.data.metadata - }; -} - export async function resolveVerification(relay, environment) { - const endpoint = relay.endpoint.get(environment)?.replace(/\/$/, ''); - const url = `${endpoint?.substring(0, endpoint.lastIndexOf('/'))}/verify`; - const header = createHeader(relay.auth_prefix, relay.auth_token.get(environment)); - const response = await get(url, '', header); - - return response.data?.code; -} - -async function post(url, data, headers) { try { - return await axios.post(url, data, { httpsAgent: await agent(url), headers }); - } catch (error) { - Logger.debug('post', error); - throw new Error(`Failed to reach ${url} via POST`); - } -} + const endpoint = relay.endpoint.get(environment)?.replace(/\/$/, ''); + const url = `${endpoint?.substring(0, endpoint.lastIndexOf('/'))}/verify`; + const headers = createHeader(relay.auth_prefix, relay.auth_token.get(environment)); + const response = await axios.get(url, { httpsAgent: await agent(url), headers }); -async function get(url, data, headers) { - try { - return await axios.get(`${url}${data}`, { httpsAgent: await agent(url), headers }); + return response.data?.code; } catch (error) { - Logger.debug('get', error); - throw new Error(`Failed to reach ${url} via GET`); - } -} - -function createBody(entry) { - if (entry) { - let body = {}; - entry.forEach(e => body[StrategiesToRelayDataType[e.strategy]] = e.input); - return body; + Logger.debug('resolveVerification', error); + return undefined; } - return null; } -function createParams(entry) { - if (entry) { - const params = entry.map(e => `${StrategiesToRelayDataType[e.strategy]}=${e.input}`); - return `?${encodeURI(params.join('&'))}`; - } - return ''; -} - -function createHeader(auth_prefix, auth_token, environment) { - let headers = { - ['Content-Type']: 'application/json' +function createHeader(auth_prefix, auth_token) { + return { + ['Content-Type']: 'application/json', + ['Authorization']: `${auth_prefix} ${auth_token}` }; - - if (environment) { - if (auth_token && environment in auth_token && auth_prefix) { - headers['Authorization'] = `${auth_prefix} ${auth_token[environment]}`; - } - } else if (auth_token && auth_prefix) { - headers['Authorization'] = `${auth_prefix} ${auth_token}`; - } - - return headers; } \ No newline at end of file diff --git a/src/client/resolvers.js b/src/client/resolvers.js index f1cfb02..462999a 100644 --- a/src/client/resolvers.js +++ b/src/client/resolvers.js @@ -1,15 +1,12 @@ import { EnvType } from '../models/environment.js'; import Domain from '../models/domain.js'; import GroupConfig from '../models/group-config.js'; -import { Config, RelayTypes} from '../models/config.js'; -import { addMetrics } from '../models/metric.js'; -import { ConfigStrategy, processOperation } from '../models/config-strategy.js'; +import { Config } from '../models/config.js'; +import { ConfigStrategy } from '../models/config-strategy.js'; import { ActionTypes, RouterTypes } from '../models/permission.js'; import { verifyOwnership } from '../helpers/index.js'; -import { resolveNotification, resolveValidation } from './relay/index.js'; import Component from '../models/component.js'; import Logger from '../helpers/logger.js'; -import { isRelayVerified, isRelayValid } from '../services/config.js'; export const resolveConfigByKey = async (domain, key) => Config.findOne({ domain, key }, null, { lean: true }); @@ -65,7 +62,7 @@ export async function resolveConfig(source, _id, key, activated, context) { let configs = await Config.find({ group: source._id, ...args }).lean().exec(); if (activated !== undefined) { - configs = configs.filter(config => config.activated[context.environment] === activated); + configs = configs.filter(config => config.activated[context.environment || EnvType.DEFAULT] === activated); } try { @@ -88,9 +85,9 @@ export async function resolveGroupConfig(source, _id, name, activated, context) if (name) { args.name = name; } let groups = await GroupConfig.find({ domain: source._id, ...args }).lean().exec(); - + if (activated !== undefined) { - groups = groups.filter(group => group.activated[context.environment] === activated); + groups = groups.filter(group => group.activated[context.environment || EnvType.DEFAULT] === activated); } try { @@ -122,7 +119,7 @@ export async function resolveDomain(_id, name, activated, context) { } let domain = await Domain.findOne({ ...args }).lean().exec(); - if (activated !== undefined && domain.activated[context.environment] !== activated) { + if (activated !== undefined && domain?.activated[context.environment || EnvType.DEFAULT] !== activated) { return null; } @@ -138,10 +135,6 @@ export async function resolveDomain(_id, name, activated, context) { return domain; } -export async function checkDomain(domainId) { - return Domain.findOne({ _id: domainId }, null, { lean: true }); -} - /** * Resolve components first is used by SDKs to filter only configurations in which the component * exists resulting in a snapshot size reduction. @@ -164,124 +157,4 @@ async function resolveComponentsFirst(source, context, groups) { return validGroups; } return groups; -} - -async function checkGroup(configId) { - const config = await Config.findOne({ _id: configId }, null, { lean: true }).exec(); - return GroupConfig.findOne({ _id: config.group }, null, { lean: true }); -} - -async function checkConfigStrategies(configId, strategyFilter) { - return ConfigStrategy.find({ config: configId }, strategyFilter).lean(); -} - -async function resolveRelay(config, environment, entry, response) { - try { - if (config.relay?.activated[environment]) { - isRelayValid(config.relay); - isRelayVerified(config.relay, environment); - - if (config.relay.type === RelayTypes.NOTIFICATION) { - resolveNotification(config.relay, entry, environment); - } else { - const relayResponse = await resolveValidation(config.relay, entry, environment); - - response.result = relayResponse.result; - response.reason = relayResponse.result ? 'Success' : 'Relay does not agree'; - response.message = relayResponse.message; - response.metadata = relayResponse.metadata; - } - } - } catch (e) { - if (config.relay.type === RelayTypes.VALIDATION) { - response.result = false; - response.reason = `Relay service could not be reached: ${e.message}`; - Logger.error(response.reason, e); - } - } -} - -function isMetricDisabled(config, environment) { - if (config.disable_metrics[environment] === undefined) { - return true; - } - - return config.disable_metrics[environment]; -} - -function checkFlags(config, group, domain, environment) { - if (config.activated[environment] === undefined ? - !config.activated[EnvType.DEFAULT] : !config.activated[environment]) { - throw new Error('Config disabled'); - } else if (group.activated[environment] === undefined ? - !group.activated[EnvType.DEFAULT] : !group.activated[environment]) { - throw new Error('Group disabled'); - } else if (domain.activated[environment] === undefined ? - !domain.activated[EnvType.DEFAULT] : !domain.activated[environment]) { - throw new Error('Domain disabled'); - } -} - -async function checkStrategy(entry, strategies, environment) { - if (strategies) { - for (const strategy of strategies) { - if (!strategy.activated[environment]) { - continue; - } - - await checkStrategyInput(entry, strategy); - } - } -} - -async function checkStrategyInput(entry, { strategy, operation, values }) { - if (entry?.length) { - const strategyEntry = entry.filter(e => e.strategy === strategy); - if (strategyEntry.length == 0 || !(await processOperation(strategy, operation, strategyEntry[0].input, values))) { - throw new Error(`Strategy '${strategy}' does not agree`); - } - } else { - throw new Error(`Strategy '${strategy}' did not receive any input`); - } -} - -export async function resolveCriteria(config, context, strategyFilter) { - context.config_id = config._id; - const environment = context.environment; - let domain, group, strategies; - - await Promise.all([ - checkDomain(context.domain), - checkGroup(config._id), - checkConfigStrategies(config._id, strategyFilter) - ]).then(result => { - domain = result[0]; - group = result[1]; - strategies = result[2]; - }); - - let response = { - domain, - group, - strategies, - result: true, - reason: 'Success' - }; - - try { - checkFlags(config, group, domain, environment); - await checkStrategy(context.entry, strategies, environment); - await resolveRelay(config, environment, context.entry, response); - } catch (e) { - response.result = false; - response.reason = e.message; - } finally { - const bypassMetric = context.bypassMetric ? context.bypassMetric === 'true' : false; - if (!bypassMetric && process.env.METRICS_ACTIVATED === 'true' && - !isMetricDisabled(config, environment)) { - addMetrics(context, response); - } - } - - return response; } \ No newline at end of file diff --git a/src/client/schema.js b/src/client/schema.js index 2d6fb98..0d5441b 100644 --- a/src/client/schema.js +++ b/src/client/schema.js @@ -1,7 +1,6 @@ -import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLList, GraphQLBoolean, GraphQLNonNull } from 'graphql'; +import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLList, GraphQLBoolean } from 'graphql'; import { domainType, flatConfigurationType } from './configuration-type.js'; -import { strategyInputType, criteriaType } from './criteria-type.js'; -import { resolveConfigByKey, resolveDomain } from './resolvers.js'; +import { resolveDomain } from './resolvers.js'; import { resolveConfiguration } from './configuration-resolvers.js'; import { permissionType } from './permission-type.js'; import { resolvePermission } from './permission-resolvers.js'; @@ -9,25 +8,6 @@ import { resolvePermission } from './permission-resolvers.js'; const queryType = new GraphQLObjectType({ name: 'Query', fields: { - criteria: { - type: criteriaType, - args: { - key: { - type: new GraphQLNonNull(GraphQLString) - }, - entry: { - type: new GraphQLList(strategyInputType) - }, - bypassMetric: { - type: GraphQLBoolean - } - }, - resolve: async (_source, { key, entry, bypassMetric }, context) => { - context.entry = entry; - context.bypassMetric = bypassMetric; - return resolveConfigByKey(context.domain, key); - } - }, domain: { type: domainType, args: { diff --git a/src/helpers/index.js b/src/helpers/index.js index eab10c1..7541a20 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -3,7 +3,6 @@ import { EnvType } from '../models/environment.js'; import { getDomainById } from '../services/domain.js'; import { getEnvironments } from '../services/environment.js'; import { getTeams } from '../services/team.js'; -import Logger from './logger.js'; import { verifyPermissions, verifyPermissionsCascade } from './permission.js'; const PATTERN_ALPHANUMERIC_SPACE = /^[a-zA-Z0-9_\- ]*$/; @@ -20,34 +19,6 @@ export async function checkEnvironmentStatusRemoval(domainId, environmentName, s } } -export function payloadReader(payload) { - let payloadRead = payload + '' === payload || payload || 0; - if (Array.isArray(payloadRead)) { - return payloadRead.flatMap(p => payloadReader(p)); - } - - return Object.keys(payloadRead) - .flatMap(field => [field, ...payloadReader(payload[field]) - .map(nestedField => `${field}.${nestedField}`)]) - .filter(field => isNaN(Number(field))) - .reduce((acc, curr) => { - if (!acc.includes(curr)) { - acc.push(curr); - } - - return acc; - }, []); -} - -export function parseJSON(str) { - try { - return JSON.parse(str); - } catch (e) { - Logger.debug('parseJSON', e); - return undefined; - } -} - export function containsValue(arr, value) { if (!arr?.length) { return false; diff --git a/src/helpers/ipcidr.js b/src/helpers/ipcidr.js deleted file mode 100644 index 0d392c4..0000000 --- a/src/helpers/ipcidr.js +++ /dev/null @@ -1,15 +0,0 @@ -export default class IPCIDR { - constructor(cidr) { - this.cidr = cidr; - } - - ip4ToInt(ip) { - return ip.split('.').reduce((int, oct) => (int << 8) + parseInt(oct, 10), 0) >>> 0; - } - - isIp4InCidr(ip) { - const [range, bits = 32] = this.cidr.split('/'); - const mask = ~(2 ** (32 - Number(bits)) - 1); - return (this.ip4ToInt(ip) & mask) === (this.ip4ToInt(range) & mask); - } -} \ No newline at end of file diff --git a/src/helpers/timed-match/index.js b/src/helpers/timed-match/index.js deleted file mode 100644 index c903936..0000000 --- a/src/helpers/timed-match/index.js +++ /dev/null @@ -1,111 +0,0 @@ -import cp from 'child_process'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -/** - * This class will run a match operation using a child process. - * Workers should be killed given a specified (3000 ms default) time limit. - * Blacklist caching is available to prevent sequence of matching failures and resource usage. - */ -export default class TimedMatch { - static _worker = this._createChildProcess(); - static _blacklisted = []; - static _maxBlackListed = process.env.REGEX_MAX_BLACKLIST || 50; - static _maxTimeLimit = process.env.REGEX_MAX_TIMEOUT || 3000; - - /** - * Run match using child process - * - * @param {*} values array of regular expression to be evaluated - * @param {*} input to be matched - * @returns match result - */ - static async tryMatch(values, input) { - let result = false; - let timer, resolveListener; - - if (this._isBlackListed({ values, input })) { - return false; - } - - const matchPromise = new Promise((resolve) => { - resolveListener = resolve; - this._worker.on('message', resolveListener); - this._worker.send({ values, input }); - }); - - const matchTimer = new Promise((resolve) => { - timer = setTimeout(() => { - this._resetWorker({ values, input }); - resolve(false); - }, this._maxTimeLimit); - }); - - await Promise.race([matchPromise, matchTimer]).then((value) => { - this._worker.off('message', resolveListener); - clearTimeout(timer); - result = value; - }); - - return result; - } - - /** - * Clear entries from failed matching operations - */ - static clearBlackList() { - this._blacklisted = []; - } - - static setMaxBlackListed(value) { - this._maxBlackListed = value; - } - - static setMaxTimeLimit(value) { - this._maxTimeLimit = value; - } - - static _isBlackListed({ values, input }) { - const bls = this._blacklisted.filter(bl => - // input can contain same segment that could fail matching operation - (bl.input.includes(input) || input.includes(bl.input)) && - // regex order should not affect - bl.res.filter(value => values.includes(value)).length); - return bls.length; - } - - /** - * Called when match worker fails to finish in time by; - * - Killing worker - * - Restarting new worker - * - Caching entry to the blacklist - * - * @param {*} param0 list of regex and input - */ - static _resetWorker({ values, input }) { - this._worker.kill(); - this._worker = this._createChildProcess(); - - if (this._blacklisted.length == this._maxBlackListed) { - this._blacklisted.splice(0, 1); - } - - this._blacklisted.push({ - res: values, - input - }); - } - - static _createChildProcess() { - const match_proc = cp.fork(`${__dirname}/match-proc.js`, { - stdio: 'ignore' - }); - - match_proc.unref(); - match_proc.channel.unref(); - return match_proc; - } -} \ No newline at end of file diff --git a/src/helpers/timed-match/match-proc.js b/src/helpers/timed-match/match-proc.js deleted file mode 100644 index dfc641e..0000000 --- a/src/helpers/timed-match/match-proc.js +++ /dev/null @@ -1,15 +0,0 @@ -function tryMatch(values, input) { - let result = false; - for (const value of values) { - if (input.match(value)) { - result = true; - break; - } - } - - return result; -} - -process.on('message', ({ values, input }) => { - process.send(tryMatch(values, input)); -}); \ No newline at end of file diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 895e97d..2006318 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -1,11 +1,7 @@ import basicAuth from 'express-basic-auth'; import jwt from 'jsonwebtoken'; import { getAdmin, getAdminById } from '../services/admin.js'; -import { getComponentById } from '../services/component.js'; -import { getEnvironmentByName } from '../services/environment.js'; import Admin from '../models/admin.js'; -import Component from '../models/component.js'; -import { getRateLimit } from '../external/switcher-api-facade.js'; import { responseExceptionSilent } from '../exceptions/index.js'; import { EnvType } from '../models/environment.js'; @@ -60,28 +56,6 @@ export async function authRefreshToken(req, res, next) { } } -export async function componentAuth(req, res, next) { - try { - const token = req.header('Authorization').replace('Bearer ', ''); - const decoded = jwt.verify(token, process.env.JWT_SECRET); - const component = await getComponentById(decoded.component); - - if (component?.apihash.substring(50, component.apihash.length - 1) !== decoded.vc) { - throw new Error('Invalid API token'); - } - - req.token = token; - req.domain = component.domain; - req.component = component.name; - req.componentId = component._id; - req.environment = decoded.environment; - req.rate_limit = decoded.rate_limit; - next(); - } catch (err) { - responseExceptionSilent(res, err, 401, 'Invalid API token.'); - } -} - export async function slackAuth(req, res, next) { try { const token = req.header('Authorization').replace('Bearer ', ''); @@ -113,27 +87,4 @@ export function resourcesAuth() { }, challenge: true, }); -} - -export async function appGenerateCredentials(req, res, next) { - try { - const key = req.header('switcher-api-key'); - const { component, domain } = await Component.findByCredentials(req.body.domain, req.body.component, key); - const environment = await getEnvironmentByName(component.domain, req.body.environment); - - if (!environment) { - throw new Error('Invalid environment'); - } - - const rate_limit = await getRateLimit(key, component); - const token = await component.generateAuthToken(req.body.environment, rate_limit); - - req.token = token; - req.domain = domain; - req.environment = req.body.environment; - req.rate_limit = rate_limit; - next(); - } catch (err) { - responseExceptionSilent(res, err, 401, 'Invalid token request.'); - } } \ No newline at end of file diff --git a/src/middleware/limiter.js b/src/middleware/limiter.js index 723ab35..b94e4b8 100644 --- a/src/middleware/limiter.js +++ b/src/middleware/limiter.js @@ -5,30 +5,11 @@ const ERROR_MESSAGE = { error: 'API request per minute quota exceeded' }; -const getMaxRate = (rate_limit) => { - if (rate_limit === 0) { - return parseInt(DEFAULT_RATE_LIMIT); - } - - return rate_limit; -}; - export const DEFAULT_RATE_LIMIT = 1000; export const defaultLimiter = rateLimit({ windowMs: DEFAULT_WINDOWMS, - limit: getMaxRate(parseInt(process.env.MAX_REQUEST_PER_MINUTE)), - standardHeaders: 'draft-7', - legacyHeaders: false, - message: ERROR_MESSAGE, - store: new MemoryStore() -}); - -export const clientLimiter = rateLimit({ - windowMs: DEFAULT_WINDOWMS, - keyGenerator: (request) => request.domain.toString(), - limit: (request) => getMaxRate(request.rate_limit), - skip: (request) => request.rate_limit === 0, + limit: parseInt(process.env.MAX_REQUEST_PER_MINUTE || DEFAULT_RATE_LIMIT), standardHeaders: 'draft-7', legacyHeaders: false, message: ERROR_MESSAGE, diff --git a/src/middleware/validators.js b/src/middleware/validators.js index 54ed5ca..36938ec 100644 --- a/src/middleware/validators.js +++ b/src/middleware/validators.js @@ -1,32 +1,7 @@ import { validationResult } from 'express-validator'; import { BadRequestError, responseException } from '../exceptions/index.js'; -import { getConfig } from '../services/config.js'; import { getEnvironments } from '../services/environment.js'; -export async function checkConfig(req, res, next) { - const config = await getConfig({ domain: req.domain, key: String(req.query.key) }, true); - - if (!config) { - return res.status(404).send({ - error: `Unable to load a key ${String(req.query.key)}` }); - } - - req.config = config; - next(); -} - -export async function checkConfigComponent(req, res, next) { - const hasComponent = req.config.components.filter((c) => - c.toString() === req.componentId.toString()).length > 0; - - if (!hasComponent) { - return res.status(401).send({ - error: `Component ${req.component} is not registered to ${req.config.key}` }); - } - - next(); -} - export async function checkEnvironmentStatusChange(args, domain, field) { const environment = await getEnvironments({ domain }, ['_id', 'name']); const updates = Object.keys(field || args); diff --git a/src/models/component.js b/src/models/component.js index aa48d93..87b9fc1 100644 --- a/src/models/component.js +++ b/src/models/component.js @@ -2,9 +2,7 @@ import mongoose from 'mongoose'; import moment from 'moment'; import bcryptjs from 'bcryptjs'; import { randomUUID } from 'crypto'; -import jwt from 'jsonwebtoken'; import { Config } from './config.js'; -import Domain from './domain.js'; import { EncryptionSalts } from './common/index.js'; const componentSchema = new mongoose.Schema({ @@ -64,50 +62,6 @@ componentSchema.methods.generateApiKey = async function () { return apiKey; }; -componentSchema.methods.generateAuthToken = async function (environment, rate_limit) { - const component = this; - - const options = { - expiresIn: process.env.JWT_CLIENT_TOKEN_EXP_TIME - }; - - return jwt.sign(({ - component: component._id, - environment, - rate_limit, - vc: component.apihash.substring(50, component.apihash.length - 1) - }), process.env.JWT_SECRET, options); -}; - -componentSchema.statics.findByCredentials = async (domainName, componentName, apiKey) => { - const domain = await Domain.findOne({ name: domainName }).exec(); - const component = await Component.findOne({ name: componentName, domain: domain._id || '' }).exec(); - - if (!component) { - throw new Error('Unable to find this Component'); - } - - let isMatch = false; - - // Validate API Key type - if (apiKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)) { - isMatch = await bcryptjs.compare(apiKey, component.apihash); - } else { - // Must be deprecated by date - let decoded = Buffer.from(apiKey, 'base64').toString('ascii'); - isMatch = await bcryptjs.compare(decoded, component.apihash); - } - - if (!isMatch) { - throw new Error('Unable to find this Component'); - } - - return { - component, - domain - }; -}; - const existComponent = async ({ domain, name, __v }) => { if (__v === undefined) { const foundComponent = await Component.find({ domain, name }).exec(); diff --git a/src/models/config-strategy.js b/src/models/config-strategy.js index 2b4b99f..bb88760 100644 --- a/src/models/config-strategy.js +++ b/src/models/config-strategy.js @@ -3,9 +3,6 @@ import moment from 'moment'; import History from './history.js'; import { recordHistory } from './common/index.js'; import { NotFoundError } from '../exceptions/index.js'; -import { parseJSON, payloadReader } from '../helpers/index.js'; -import IPCIDR from '../helpers/ipcidr.js'; -import TimedMatch from '../helpers/timed-match/index.js'; export const StrategiesType = Object.freeze({ NETWORK: 'NETWORK_VALIDATION', @@ -148,149 +145,6 @@ export function strategyRequirements(strategy) { }; } -export async function processOperation(strategy, operation, input, values) { - switch(strategy) { - case StrategiesType.NETWORK: - return processNETWORK(operation, input, values); - case StrategiesType.VALUE: - return processVALUE(operation, input, values); - case StrategiesType.NUMERIC: - return processNUMERIC(operation, input, values); - case StrategiesType.TIME: - return processTIME(operation, input, values); - case StrategiesType.DATE: - return processDATE(operation, input, values); - case StrategiesType.REGEX: - return processREGEX(operation, input, values); - case StrategiesType.PAYLOAD: - return processPAYLOAD(operation, input, values); - } -} - -function processNETWORK(operation, input, values) { - const cidrRegex = /^(\d{1,3}\.){3}\d{1,3}(\/(\d|[1-2]\d|3[0-2]))$/; - switch(operation) { - case OperationsType.EXIST: - return processNETWORK_Exist(input, values, cidrRegex); - case OperationsType.NOT_EXIST: - return processNETWORK_NotExist(input, values, cidrRegex); - } - return false; -} - -function processNETWORK_Exist(input, values, cidrRegex) { - for (const value of values) { - if (value.match(cidrRegex)) { - const cidr = new IPCIDR(value); - if (cidr.isIp4InCidr(input)) { - return true; - } - } else { - return values.includes(input); - } - } - return false; -} - -function processNETWORK_NotExist(input, values, cidrRegex) { - const result = values.filter((element) => { - if (element.match(cidrRegex)) { - const cidr = new IPCIDR(element); - if (cidr.isIp4InCidr(input)) { - return true; - } - } else { - return values.includes(input); - } - }); - return result.length === 0; -} - -function processVALUE(operation, input, values) { - switch(operation) { - case OperationsType.EXIST: - return values.includes(input); - case OperationsType.NOT_EXIST: - return !values.includes(input); - case OperationsType.EQUAL: - return input === values[0]; - case OperationsType.NOT_EQUAL: - return values.filter(element => element === input).length === 0; - } -} - -function processNUMERIC(operation, input, values) { - const inputStr = String(input); - switch(operation) { - case OperationsType.EXIST: - return processVALUE(operation, inputStr, values); - case OperationsType.NOT_EXIST: - return processVALUE(operation, inputStr, values); - case OperationsType.EQUAL: - return processVALUE(operation, inputStr, values); - case OperationsType.NOT_EQUAL: - return processVALUE(operation, inputStr, values); - case OperationsType.LOWER: - return inputStr < values[0]; - case OperationsType.GREATER: - return inputStr > values[0]; - case OperationsType.BETWEEN: - return inputStr >= values[0] && inputStr <= values[1]; - } -} - -function processTIME(operation, input, values) { - const today = moment().format('YYYY-MM-DD'); - - switch(operation) { - case OperationsType.LOWER: - return moment(`${today}T${input}`).isSameOrBefore(`${today}T${values[0]}`); - case OperationsType.GREATER: - return moment(`${today}T${input}`).isSameOrAfter(`${today}T${values[0]}`); - case OperationsType.BETWEEN: - return moment(`${today}T${input}`).isBetween(`${today}T${values[0]}`, `${today}T${values[1]}`); - } -} - -function processDATE(operation, input, values) { - switch(operation) { - case OperationsType.LOWER: - return moment(input).isSameOrBefore(values[0]); - case OperationsType.GREATER: - return moment(input).isSameOrAfter(values[0]); - case OperationsType.BETWEEN: - return moment(input).isBetween(values[0], values[1]); - } -} - -async function processREGEX(operation, input, values) { - switch(operation) { - case OperationsType.EXIST: - return await TimedMatch.tryMatch(values, input); - case OperationsType.NOT_EXIST: - return !(await processREGEX(OperationsType.EXIST, input, values)); - case OperationsType.EQUAL: - return await TimedMatch.tryMatch([`\\b${values[0]}\\b`], input); - case OperationsType.NOT_EQUAL: - return !(await TimedMatch.tryMatch([`\\b${values[0]}\\b`], input)); - } -} - -function processPAYLOAD(operation, input, values) { - const inputJson = parseJSON(input); - if (!inputJson) { - return false; - } - - const keys = payloadReader(inputJson); - switch(operation) { - case OperationsType.HAS_ONE: - return keys.filter(key => values.includes(key)).length > 0; - case OperationsType.HAS_ALL: - return values.every(element => keys.includes(element)); - } -} - async function recordStrategyHistory(strategyConfig, modifiedField) { if (strategyConfig.__v !== undefined && modifiedField.length) { const oldStrategy = await ConfigStrategy.findById(strategyConfig._id).exec(); diff --git a/src/models/metric.js b/src/models/metric.js index ba49eaa..9be3637 100644 --- a/src/models/metric.js +++ b/src/models/metric.js @@ -58,20 +58,4 @@ metricSchema.options.toJSON = { } }; -export function addMetrics(context, response) { - const metric = new Metric({ - config: context.config_id, - component: context.component, - entry: context.entry, - result: response.result, - reason: response.reason, - message: response.message, - group: response.group.name, - environment: context.environment, - domain: response.domain._id, - date: Date.now() - }); - metric.save(); -} - export const Metric = mongoose.model('Metric', metricSchema); diff --git a/src/routers/client-api.js b/src/routers/client-api.js deleted file mode 100644 index e3d1c15..0000000 --- a/src/routers/client-api.js +++ /dev/null @@ -1,93 +0,0 @@ -import express from 'express'; -import { body, check, query, header } from 'express-validator'; -import jwt from 'jsonwebtoken'; -import { checkConfig, checkConfigComponent, validate } from '../middleware/validators.js'; -import { componentAuth, appGenerateCredentials } from '../middleware/auth.js'; -import { resolveCriteria, checkDomain } from '../client/resolvers.js'; -import { getConfigs } from '../services/config.js'; -import { clientLimiter } from '../middleware/limiter.js'; -import { StrategiesType } from '../models/config-strategy.js'; - -const router = new express.Router(); - -// GET /check?key=KEY -// GET /check?key=KEY&showReason=true -// GET /check?key=KEY&showStrategy=true -// GET /check?key=KEY&bypassMetric=true -router.post('/criteria', componentAuth, clientLimiter, [ - query('key').isLength({ min: 1 }).withMessage('Key is required'), - body('entry').isArray().optional().withMessage('Entry must be an array'), - body('entry.*.input').isString(), - body('entry.*.strategy').isString() - .custom(value => Object.values(StrategiesType).includes(value)).withMessage('Invalid strategy type') -], validate, checkConfig, checkConfigComponent, async (req, res) => { - try { - const environment = req.environment; - const domain = req.domain; - const entry = req.body.entry; - - const context = { domain, entry, environment, bypassMetric: req.query.bypassMetric, component: req.component }; - - const response = await resolveCriteria(req.config, context, 'values description strategy operation activated -_id'); - - delete response.domain; - delete response.group; - - if ((req.query.showReason || 'false') === 'false') { - delete response.reason; - } - - if ((req.query.showStrategy || 'false') === 'false') { - delete response.strategies; - } - - res.send(response); - } catch (e) { - res.status(500).send({ error: e.message }); - } -}); - -router.get('/criteria/snapshot_check/:version', componentAuth, clientLimiter, [ - check('version', 'Wrong value for domain version').isNumeric() -], validate, async (req, res) => { - try { - const domain = await checkDomain(req.domain); - const version = req.params.version; - - if (domain.lastUpdate > version) { - res.send({ status: false }); - } else { - res.send({ status: true }); - } - } catch (e) { - res.status(500).send({ error: e.message }); - } -}); - -router.post('/criteria/switchers_check', componentAuth, clientLimiter, [ - check('switchers', 'Switcher Key is required').isArray().isLength({ min: 1 }) -], validate, async (req, res) => { - try { - const configsFound = await getConfigs({ domain: req.domain, components: req.componentId }); - const configs = configsFound.map(config => config.key); - res.send({ not_found: req.body.switchers.filter(switcher => !configs.includes(switcher)) }); - } catch (e) { - res.status(500).send({ error: e.message }); - } -}); - -router.post('/criteria/auth', [ - header('switcher-api-key').isString().withMessage('API Key header is required'), - body('domain').isString().withMessage('Domain is required'), - body('component').isString().withMessage('Component is required'), - body('environment').isString().withMessage('Environment is required') -], validate, appGenerateCredentials, clientLimiter, async (req, res) => { - try { - const { exp } = jwt.decode(req.token); - res.send({ token: req.token, exp }); - } catch (e) { - res.status(400).send({ error: e.message }); - } -}); - -export default router; \ No newline at end of file diff --git a/src/services/config.js b/src/services/config.js index 5df9acf..a574f85 100644 --- a/src/services/config.js +++ b/src/services/config.js @@ -26,13 +26,12 @@ export async function getConfigById(id, populateAdmin = false) { return response(config, 'Config not found'); } -export async function getConfig(where, lean = false) { +export async function getConfig(where) { const query = Config.findOne(); if (where.domain) query.where('domain', where.domain); if (where.key) query.where('key', where.key); if (where.group) query.where('group', where.group); - if (lean) query.lean(); return query.exec(); } @@ -44,7 +43,6 @@ export async function getConfigs(where, lean = false) { if (where.key) query.where('key', where.key); if (where.domain) query.where('domain', where.domain); if (where.group) query.where('group', where.group); - if (where.components) query.where('components', where.components); if (lean) query.lean(); return query.exec(); @@ -301,7 +299,10 @@ export async function verifyRelay(id, env, admin) { RouterTypes.CONFIG, false, env); const code = await resolveVerification(config.relay, env); - if (!config.relay.verified?.get(env) && Object.is(domain.integrations.relay.verification_code, code)) { + if (!config.relay.verified?.get(env) && + domain.integrations.relay.verification_code && code && + Object.is(domain.integrations.relay.verification_code, code) + ) { config.relay.verified.set(env, true); await config.save(); return 'verified'; @@ -323,16 +324,4 @@ export function isRelayValid(relay) { if (foundNotHttps.length) { throw new BadRequestError('HTTPS required'); } -} - -export function isRelayVerified(relay, environment) { - const bypass = process.env.RELAY_BYPASS_VERIFICATION === 'true' || false; - - if (bypass) { - return; - } - - if (!relay.verified[environment]) { - throw new BadRequestError('Relay not verified'); - } } \ No newline at end of file diff --git a/tests/client-api-payload.test.js b/tests/client-api-payload.test.js deleted file mode 100644 index 54dc07a..0000000 --- a/tests/client-api-payload.test.js +++ /dev/null @@ -1,108 +0,0 @@ -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'; - -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(setupDatabase); - -afterAll(async () => { - await new Promise(resolve => setTimeout(resolve, 1000)); - await mongoose.disconnect(); -}); - -describe('Testing criteria [REST] ', () => { - let token; - - beforeAll(async () => { - const response = await createRequestAuth(); - token = response.body.token; - }); - - test('CLIENT_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_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_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_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.test.js b/tests/client-api.test.js index 77f5944..865204b 100644 --- a/tests/client-api.test.js +++ b/tests/client-api.test.js @@ -4,84 +4,23 @@ import sinon from 'sinon'; import app from '../src/app'; import { ActionTypes, Permission, RouterTypes } from '../src/models/permission'; import { permissionCache } from '../src/helpers/cache'; -import Domain from '../src/models/domain'; -import GroupConfig from '../src/models/group-config'; -import { Config } from '../src/models/config'; import { Team } from '../src/models/team'; -import Component from '../src/models/component'; -import { ConfigStrategy, StrategiesType, OperationsType } from '../src/models/config-strategy'; import { EnvType } from '../src/models/environment'; -import { adminMasterAccountId } from './fixtures/db_api'; import Admin from '../src/models/admin'; -import { Metric } from '../src/models/metric'; import * as graphqlUtils from './graphql-utils'; import { setupDatabase, adminMasterAccountToken, adminAccountToken, - apiKey, keyConfig, - keyConfigPrdQA, configId, groupConfigId, domainId, - domainDocument, - configStrategyUSERId, - component1, adminAccountId, - slack + slack, + keyConfigPrdQA } from './fixtures/db_client'; -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 ? status : strategy.activated.get(environment)); - 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 ? status : config.activated.get(environment)); - 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 ? status : groupConfig.activated.get(environment)); - 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 ? status : domain.activated.get(environment)); - 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 - }); -}; - const setPermissionsToTeam = async (teamId, permission, reset) => { const permissionId = new mongoose.Types.ObjectId(); permission._id = permissionId; @@ -104,330 +43,26 @@ afterAll(async () => { await mongoose.disconnect(); }); -describe('Testing criteria [GraphQL] ', () => { - let token; - - beforeAll(async () => { - const response = await createRequestAuth(); - token = response.body.token; - }); - - afterAll(setupDatabase); - - test('CLIENT_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_SUITE - Should return success on Flat view resolved by Group name', async () => { - const req = await request(app) - .post('/graphql') - .set('Authorization', `Bearer ${token}`) - .send(graphqlUtils.configurationQuery([['group', 'Group Test']])); - - expect(req.statusCode).toBe(200); - expect(req.body).toMatchObject(JSON.parse(graphqlUtils.expected100)); - }); - - test('CLIENT_SUITE - Should return on Flat view resolved without an unknown Group name', async () => { - const req = await request(app) - .post('/graphql') - .set('Authorization', `Bearer ${token}`) - .send(graphqlUtils.configurationQuery([['group', 'UNKNOWN GROUP NAME']])); - - expect(req.statusCode).toBe(200); - expect(JSON.parse(req.text)).toMatchObject(JSON.parse(graphqlUtils.expected112)); - }); - - test('CLIENT_SUITE - Should return success on Flat view resolved by Config Key', async () => { - const req = await request(app) - .post('/graphql') - .set('Authorization', `Bearer ${token}`) - .send(graphqlUtils.configurationQuery([['key', keyConfig]])); - - expect(req.statusCode).toBe(200); - expect(JSON.parse(req.text)).toMatchObject(JSON.parse(graphqlUtils.expected101)); - }); - - test('CLIENT_SUITE - Should NOT authenticate invalid component', async () => { - await request(app) - .post('/criteria/auth') - .set('switcher-api-key', `${apiKey}`) - .send({ - domain: domainDocument.name, - component: 'UNKNOWN COMPONENT', - environment: EnvType.DEFAULT - }).expect(401); - }); - - test('CLIENT_SUITE - Should NOT authenticate invalid environment', async () => { - await request(app) - .post('/criteria/auth') - .set('switcher-api-key', `${apiKey}`) - .send({ - domain: domainDocument.name, - component: component1.name, - environment: 'UNKNOWN ENVIRONMENT' - }).expect(401); - }); - - test('CLIENT_SUITE - Should return on Flat view without unknown Config Key', async () => { - const req = await request(app) - .post('/graphql') - .set('Authorization', `Bearer ${token}`) - .send(graphqlUtils.configurationQuery([['key', 'UNKNOWN_CONFIG_KEY']])); - - expect(req.statusCode).toBe(200); - expect(JSON.parse(req.text)).toMatchObject(JSON.parse(graphqlUtils.expected112)); - }); - - test('CLIENT_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_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_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_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_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_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_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); - 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_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); - 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_SUITE - Should return false due to Group deactivation', async () => { - await changeGroupConfigStatus(groupConfigId, 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', 'Group disabled'); - expect(JSON.parse(response.text)).toMatchObject(JSON.parse(expected)); - }); - - test('CLIENT_SUITE - Should return false due to Domain deactivation', async () => { - await changeGroupConfigStatus(groupConfigId, true, EnvType.DEFAULT); - await changeDomainStatus(domainId, 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', 'Domain disabled'); - expect(JSON.parse(response.text)).toMatchObject(JSON.parse(expected)); - }); - - test('CLIENT_SUITE - Should not add to metrics when Config has disabled metric flag = true', async () => { - // Given - await changeConfigStatus(configId, true, EnvType.DEFAULT); - - //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); - - //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 domain', () => { - let token; - - beforeAll(async () => { - 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('CLIENT_SUITE - Should return the Domain structure', async () => { const req = await request(app) - .post('/graphql') - .set('Authorization', `Bearer ${token}`) + .post('/adm-graphql') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) .send(graphqlUtils.domainQuery([['_id', domainId]], true, true, true)); - + expect(req.statusCode).toBe(200); expect(JSON.parse(req.text)).toMatchObject(JSON.parse(graphqlUtils.expected102)); }); test('CLIENT_SUITE - Should return 2 switchers when NOT filtered by Component', async () => { const req = await request(app) - .post('/graphql') - .set('Authorization', `Bearer ${token}`) + .post('/adm-graphql') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) .send(graphqlUtils.domainQuery([ + ['_id', domainId], ['environment', EnvType.DEFAULT]]) ); @@ -438,9 +73,10 @@ describe('Testing domain', () => { test('CLIENT_SUITE - Should return 1 switcher when filtered by Component', async () => { const req = await request(app) - .post('/graphql') - .set('Authorization', `Bearer ${token}`) + .post('/adm-graphql') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) .send(graphqlUtils.domainQuery([ + ['_id', domainId], ['environment', EnvType.DEFAULT], ['_component', 'TestApp']]) ); @@ -450,23 +86,10 @@ describe('Testing domain', () => { expect(result.data.domain.group[0].config.length).toBe(1); }); - test('CLIENT_SUITE - Should return the Domain structure - Just using environment', async () => { - // Domain will be resolved while identifying the component - const req = await request(app) - .post('/graphql') - .set('Authorization', `Bearer ${token}`) - .send(graphqlUtils.domainQuery([ - ['environment', EnvType.DEFAULT]], true, true, true) - ); - - expect(req.statusCode).toBe(200); - expect(JSON.parse(req.text)).toMatchObject(JSON.parse(graphqlUtils.expected102)); - }); - test('CLIENT_SUITE - Should return the Domain structure - Disabling strategies (resolver test)', async () => { const req = await request(app) - .post('/graphql') - .set('Authorization', `Bearer ${token}`) + .post('/adm-graphql') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) .send(graphqlUtils.domainQuery([['_id', domainId]], true, true, false)); expect(req.statusCode).toBe(200); @@ -475,8 +98,8 @@ describe('Testing domain', () => { test('CLIENT_SUITE - Should return the Domain structure - Disabling group config (resolver test)', async () => { const req = await request(app) - .post('/graphql') - .set('Authorization', `Bearer ${token}`) + .post('/adm-graphql') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) .send(graphqlUtils.domainQuery([['_id', domainId]], false, false, false)); expect(req.statusCode).toBe(200); @@ -485,8 +108,8 @@ describe('Testing domain', () => { test('CLIENT_SUITE - Should return the Domain structure - Disabling config (resolver test)', async () => { const req = await request(app) - .post('/graphql') - .set('Authorization', `Bearer ${token}`) + .post('/adm-graphql') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) .send(graphqlUtils.domainQuery([['_id', domainId]], true, false, false)); expect(req.statusCode).toBe(200); @@ -494,398 +117,31 @@ describe('Testing domain', () => { }); }); -describe('Testing criteria [REST] ', () => { - let token; - - beforeAll(async () => { - const response = await createRequestAuth(); - token = response.body.token; - }); - - test('CLIENT_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_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_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_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_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_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_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_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' - }]}); +describe('Testing domain [Adm-GraphQL] ', () => { - 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); - }); + afterAll(setupDatabase); - test('CLIENT_SUITE - Should NOT return success on a simple CRITERIA response - Invalid KEY', async () => { + test('CLIENT_SUITE - Should return domain structure', 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_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); - - const responseNewApiKey = await request(app) - .get('/component/generateApiKey/' + component1._id) + .post('/adm-graphql') .set('Authorization', `Bearer ${adminMasterAccountToken}`) - .send().expect(201); - - 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', `${responseNewApiKey.body.apiKey}`) - .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); - - }); - - test('CLIENT_SUITE - Should NOT return due to invalid API key provided', async () => { - await request(app) - .post('/criteria/auth') - .set('switcher-api-key', 'INVALID_API_KEY') - .send({ - domain: domainDocument.name, - component: component1.name, - environment: EnvType.DEFAULT - }).expect(401); - }); - - test('CLIENT_SUITE - Should return that snapshot version is outdated - status = false', async () => { - const req = await request(app) - .get('/criteria/snapshot_check/1') - .set('Authorization', `Bearer ${token}`) - .send(); - - expect(req.statusCode).toBe(200); - expect(req.body.status).toEqual(false); - }); - - test('CLIENT_SUITE - Should return that snapshot version is updated - status = true', async () => { - const req = await request(app) - .get('/criteria/snapshot_check/5') - .set('Authorization', `Bearer ${token}`) - .send(); - - expect(req.statusCode).toBe(200); - expect(req.body.status).toEqual(true); - }); - - test('CLIENT_SUITE - Should return error when validating snapshot version - Version is not a number', async () => { - const req = await request(app) - .get('/criteria/snapshot_check/ONLY_NUMBER_ALLOWED') - .set('Authorization', `Bearer ${token}`) - .send(); - - expect(req.statusCode).toBe(422); - expect(req.body.errors[0].msg).toEqual('Wrong value for domain version'); - }); - - test('CLIENT_SUITE - Should return error when validating snapshot version - Invalid token', async () => { - const req = await request(app) - .get('/criteria/snapshot_check/5') - .set('Authorization', 'Bearer INVALID_TOKEN') - .send(); - - expect(req.statusCode).toBe(401); - expect(req.body.error).toEqual('Invalid API token.'); - }); - - test('CLIENT_SUITE - Should return an empty list of switchers - all switchers queried found', async () => { - const req = await request(app) - .post('/criteria/switchers_check') - .set('Authorization', `Bearer ${token}`) - .send({ - switchers: [ - 'TEST_CONFIG_KEY' - ] - }); - - expect(req.statusCode).toBe(200); - expect(req.body.not_found).toEqual([]); - }); - - test('CLIENT_SUITE - Should return the switcher queried - not found', async () => { - const req = await request(app) - .post('/criteria/switchers_check') - .set('Authorization', `Bearer ${token}`) - .send({ - switchers: [ - 'TEST_CONFIG_KEY', - 'I_DO_NOT_EXIST' - ] - }); + .send(graphqlUtils.domainQuery([['name', 'Domain']])); expect(req.statusCode).toBe(200); - expect(req.body.not_found).toEqual(['I_DO_NOT_EXIST']); - }); - - test('CLIENT_SUITE - Should NOT return list of switchers - Invalid body attribute', async () => { - await request(app) - .post('/criteria/switchers_check') - .set('Authorization', `Bearer ${token}`) - .send({ - switchers: 'TEST_CONFIG_KEY' - }) - .expect(422); - - await request(app) - .post('/criteria/switchers_check') - .set('Authorization', `Bearer ${token}`) - .send() - .expect(422); - }); -}); - -describe('Testing criteria [REST] Rate Limit ', () => { - let token; - - beforeAll(async () => { - process.env.MAX_REQUEST_PER_MINUTE = 1; - - await setupDatabase(); - const response = await createRequestAuth(); - token = response.body.token; - }); - - afterAll(() => { - process.env.MAX_REQUEST_PER_MINUTE = 0; - }); - - test('CLIENT_SUITE - Should limit run to 1 execution', async () => { - await request(app) - .post(`/criteria?key=${keyConfig}`) - .set('Authorization', `Bearer ${token}`) - .send() - .expect(200); - - const req = await request(app) - .post(`/criteria?key=${keyConfig}`) - .set('Authorization', `Bearer ${token}`) - .send() - .expect(429); - - expect(req.body.error).toBe('API request per minute quota exceeded'); + expect(JSON.parse(req.text)).toMatchObject(JSON.parse(graphqlUtils.expected106)); }); -}); - -describe('Testing domain [Adm-GraphQL] ', () => { - - afterAll(setupDatabase); - test('CLIENT_SUITE - Should return domain structure', async () => { + test('CLIENT_SUITE - Should NOT return domain structure - Filtered by disabled Domain', async () => { const req = await request(app) .post('/adm-graphql') .set('Authorization', `Bearer ${adminMasterAccountToken}`) - .send(graphqlUtils.domainQuery([['name', 'Domain']])); + .send(graphqlUtils.domainQuery([ + ['name', 'Domain'], + ['activated', false]])); + const expected = '{"data":{"domain":null}}'; expect(req.statusCode).toBe(200); - expect(JSON.parse(req.text)).toMatchObject(JSON.parse(graphqlUtils.expected106)); + expect(JSON.parse(req.text)).toMatchObject(JSON.parse(expected)); }); test('CLIENT_SUITE - Should return domain structure for a team member', async () => { @@ -929,6 +185,16 @@ describe('Testing domain [Adm-GraphQL] ', () => { expect(JSON.parse(req.text)).toMatchObject(JSON.parse(graphqlUtils.expected108)); }); + test('CLIENT_SUITE - Should return partial domain Flat-structure - By NOT_FOUND Switcher Key', async () => { + const req = await request(app) + .post('/adm-graphql') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send(graphqlUtils.configurationQuery([['domain', domainId], ['key', 'NOT_FAUND']])); + + expect(req.statusCode).toBe(200); + expect(JSON.parse(req.text)).toMatchObject(JSON.parse(graphqlUtils.expected112)); + }); + test('CLIENT_SUITE - Should return domain Flat-structure - By Group', async () => { const req = await request(app) .post('/adm-graphql') @@ -951,6 +217,16 @@ describe('Testing domain [Adm-GraphQL] ', () => { expect(result.data.configuration.group[0].name).toEqual('Group Test'); }); + test('CLIENT_SUITE - Should return partial domain Flat-structure - By NOT_FOUND Group', async () => { + const req = await request(app) + .post('/adm-graphql') + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send(graphqlUtils.configurationQuery([['domain', domainId], ['group', 'NOT_FOUND']])); + + expect(req.statusCode).toBe(200); + expect(JSON.parse(req.text)).toMatchObject(JSON.parse(graphqlUtils.expected112)); + }); + test('CLIENT_SUITE - Should return domain Flat-structure for a team member', async () => { const req = await request(app) .post('/adm-graphql') @@ -1176,7 +452,7 @@ describe('Testing domain/configuration [Adm-GraphQL] - Excluded team member ', ( expect(JSON.parse(req.text)).toMatchObject(JSON.parse(expected)); }); - test('CLIENT_SUITE - Should NOT return domain Flat-structure for am excluded team member', async () => { + test('CLIENT_SUITE - Should NOT return domain Flat-structure for an excluded team member', async () => { const req = await request(app) .post('/adm-graphql') .set('Authorization', `Bearer ${adminAccountToken}`) diff --git a/tests/config-relay.test.js b/tests/config-relay.test.js index 5a87e50..acc176f 100644 --- a/tests/config-relay.test.js +++ b/tests/config-relay.test.js @@ -411,7 +411,7 @@ describe('Testing relay association', () => { // Test response = await request(app) - .patch(`/config/relay/verify/${configId1}/${EnvType.DEFAULT}?code=${response.body.code}`) + .patch(`/config/relay/verify/${configId1}/${EnvType.DEFAULT}`) .set('Authorization', `Bearer ${adminMasterAccountToken}`) .send().expect(200); @@ -458,6 +458,34 @@ describe('Testing relay association', () => { .set('Authorization', `Bearer ${adminMasterAccountToken}`) .send().expect(200); + axiosStub.restore(); + expect(response.body.status).toBe('failed'); + }); + + test('CONFIG_RELAY_SUITE - Should NOT verify code - Endpoint not reachable', async () => { + // Given + // Adding relay + await request(app) + .patch(`/config/updateRelay/${configId1}`) + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send({ + activated: { default: true }, + endpoint: { default: 'http://localhost:7000/relay' }, + auth_token: { default: 'abcd' }, + auth_prefix: 'Bearer' + }).expect(200); + + // Relay verification + const axiosStub = sinon.stub(axios, 'get'); + axiosStub.throws(new Error('Endpoint not reachable')); + + // Test + const response = await request(app) + .patch(`/config/relay/verify/${configId1}/${EnvType.DEFAULT}`) + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send().expect(200); + + axiosStub.restore(); expect(response.body.status).toBe('failed'); }); diff --git a/tests/graphql-utils/index.js b/tests/graphql-utils/index.js index 1d5fcde..38f8602 100644 --- a/tests/graphql-utils/index.js +++ b/tests/graphql-utils/index.js @@ -1,5 +1,11 @@ -const createInput = input => `${input[0]}: "${input[1]}"`; -const createStrategyInput = input => `{ strategy: "${input[0]}", input: "${input[1]}" }`; +const createInput = input => { + if (typeof input[1] === 'boolean' || typeof input[1] === 'number') { + return `${input[0]}: ${input[1]}`; + } + + return `${input[0]}: "${input[1]}"`; +}; + const isActivated = (element) => element ? 'true' : 'false'; export const configurationQuery = ( @@ -44,25 +50,6 @@ export const domainQuery = (where, group, config, strategy) => { `}; }; -export const criteriaQuery = (key, entries) => { - return { - query: ` - { - criteria( - key: "${key}", - entry: [${entries}] - ) { response { result reason } } - } - `}; -}; - -export const criteriaResult = (result, reason) => ` - { "data": { "criteria": { "response": { "result": ${result}, "reason": "${reason}" } } } }`; - -export const buildEntries = (entries) => { - return `${entries.map(createStrategyInput)}`; -}; - export const permissionsQuery = (domainId, parentId, actions, router, environment) => { return { query: ` diff --git a/tests/model/component.test.js b/tests/model/component.test.js deleted file mode 100644 index fff535f..0000000 --- a/tests/model/component.test.js +++ /dev/null @@ -1,75 +0,0 @@ -import '../../src/db/mongoose'; - -import { randomBytes } from 'crypto'; -import bcryptjs from 'bcryptjs'; -import mongoose from 'mongoose'; -import { - setupDatabase, - adminMasterAccountId, - domainId, - domainDocument - } from '../fixtures/db_api'; -import Component from '../../src/models/component'; -import { EncryptionSalts } from '../../src/models/common'; - -afterAll(async () => { - await new Promise(resolve => setTimeout(resolve, 1000)); - await mongoose.disconnect(); -}); - -describe('(Deprecated) Testing component authentication', () => { - beforeAll(async () => await setupDatabase()); - - /** - * Generates API key using old method - */ - const generateApiKeyDeprecated = async (component) => { - const buffer = randomBytes(32); - const apiKey = Buffer.from(buffer).toString('base64'); - const hash = await bcryptjs.hash(apiKey, EncryptionSalts.COMPONENT); - component.apihash = hash; - await component.save(); - - const generatedApiKey = Buffer.from(apiKey).toString('base64'); - return generatedApiKey; - }; - - test('COMPONENT_MODEL - Should authenticate component using old API key format', async () => { - // Given - const componentId = new mongoose.Types.ObjectId(); - const component = new Component({ - _id: componentId, - name: 'TestDeprecatedAPIKey', - description: 'Test app with depracated API key', - domain: domainId, - owner: adminMasterAccountId - }); - - // That - const generatedApiKey = await generateApiKeyDeprecated(component); - - // Test - const result = await Component.findByCredentials(domainDocument.name, component.name, generatedApiKey); - expect(result.component).not.toBe(undefined); - }); - - test('COMPONENT_MODEL - Should authenticate component using new API key format', async () => { - // Given - const componentId = new mongoose.Types.ObjectId(); - const component = new Component({ - _id: componentId, - name: 'TestNewAPIKey', - description: 'Test app with New API key', - domain: domainId, - owner: adminMasterAccountId - }); - - // That - const generatedApiKey = await component.generateApiKey(); - - // Test - const result = await Component.findByCredentials(domainDocument.name, component.name, generatedApiKey); - expect(result.component).not.toBe(undefined); - }); - -}); diff --git a/tests/relay.test.js b/tests/relay.test.js deleted file mode 100644 index 31cbe63..0000000 --- a/tests/relay.test.js +++ /dev/null @@ -1,495 +0,0 @@ -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, - adminMasterAccountToken, - apiKey, - keyConfig, - configId, - domainDocument, - component1, - configStrategyUSERId, - configStrategyCIDRId -} from './fixtures/db_client'; -import { EnvType } from '../src/models/environment'; -import { adminMasterAccountId } from './fixtures/db_api'; -import { StrategiesType, ConfigStrategy } from '../src/models/config-strategy'; - -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 ? 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 - }; -}; - -beforeAll(setupDatabase); - -afterAll(async () => { - 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' - } - }; - }; - - let token; - let axiosStub; - - beforeAll(async () => { - 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_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 - await request(app) - .patch(`/config/updateRelay/${configId}`) - .set('Authorization', `Bearer ${adminMasterAccountToken}`) - .send(bodyRelay(RelayMethods.GET, RelayTypes.VALIDATION)).expect(200); - - // 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.message).toBe('A message'); - expect(req.body.result).toBe(true); - expect(req.body.metadata).toEqual({ custom: 'VALUE' }); - }); - - test('RELAY_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 - await request(app) - .patch(`/config/updateRelay/${configId}`) - .set('Authorization', `Bearer ${adminMasterAccountToken}`) - .send(bodyRelay(RelayMethods.POST, RelayTypes.VALIDATION)).expect(200); - - // 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_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 - await request(app) - .patch(`/config/updateRelay/${configId}`) - .set('Authorization', `Bearer ${adminMasterAccountToken}`) - .send(bodyRelay(RelayMethods.GET, RelayTypes.NOTIFICATION)).expect(200); - - // 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_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 - await request(app) - .patch(`/config/updateRelay/${configId}`) - .set('Authorization', `Bearer ${adminMasterAccountToken}`) - .send(bodyRelay(RelayMethods.POST, RelayTypes.NOTIFICATION)).expect(200); - - // 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_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 - await request(app) - .patch(`/config/updateRelay/${configId}`) - .set('Authorization', `Bearer ${adminMasterAccountToken}`) - .send(bodyRelay(RelayMethods.GET, RelayTypes.NOTIFICATION)).expect(200); - - //disabling strategy to proceed calling with no input - await changeStrategy(configStrategyUSERId, undefined, false, EnvType.DEFAULT); - await changeStrategy(configStrategyCIDRId, undefined, false, EnvType.DEFAULT); - - // 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_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 - await request(app) - .patch(`/config/updateRelay/${configId}`) - .set('Authorization', `Bearer ${adminMasterAccountToken}`) - .send(bodyRelay(RelayMethods.POST, RelayTypes.NOTIFICATION)).expect(200); - - //disabling strategy to proceed calling with no input - await changeStrategy(configStrategyUSERId, undefined, false, EnvType.DEFAULT); - await changeStrategy(configStrategyCIDRId, undefined, false, EnvType.DEFAULT); - - // 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_SUITE - Should NOT return success when validating relay using GET method - Service exception', async () => { - // Mock - axiosStub = sinon.stub(axios, 'get'); - axiosStub.throwsException(); - - // Setup Switcher - await request(app) - .patch(`/config/updateRelay/${configId}`) - .set('Authorization', `Bearer ${adminMasterAccountToken}`) - .send(bodyRelay(RelayMethods.GET, RelayTypes.VALIDATION)).expect(200); - - // 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_SUITE - Should NOT return success when validating relay using POST method - Service exception', async () => { - // Mock - axiosStub = sinon.stub(axios, 'post'); - axiosStub.throwsException(); - - // Setup Switcher - await request(app) - .patch(`/config/updateRelay/${configId}`) - .set('Authorization', `Bearer ${adminMasterAccountToken}`) - .send(bodyRelay(RelayMethods.POST, RelayTypes.VALIDATION)).expect(200); - - // 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 () => { - 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_SUITE - Should return Relay could not be reached - Relay HTTPS required', async () => { - // Given - // HTTPS not required - process.env.RELAY_BYPASS_HTTPS = true; - - // Setup Switcher - await request(app) - .patch(`/config/updateRelay/${configId}`) - .set('Authorization', `Bearer ${adminMasterAccountToken}`) - .send(bodyRelay('http://localhost:3001')).expect(200); - - // 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 () => { - 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_SUITE - Should return Relay could not be reached - Not verified', async () => { - // Given - // Verification required - process.env.RELAY_BYPASS_VERIFICATION = false; - - // Setup Switcher - await request(app) - .patch(`/config/updateRelay/${configId}`) - .set('Authorization', `Bearer ${adminMasterAccountToken}`) - .send(bodyRelay('https://localhost:3001')).expect(200); - - // 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_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 - await request(app) - .patch(`/config/updateRelay/${configId}`) - .set('Authorization', `Bearer ${adminMasterAccountToken}`) - .send(bodyRelay('https://localhost:3001')).expect(200); - - // Config has a verified Relay - const 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); - - // 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/client/relay.test.js b/tests/unit-test/client/relay.test.js deleted file mode 100644 index 961c729..0000000 --- a/tests/unit-test/client/relay.test.js +++ /dev/null @@ -1,46 +0,0 @@ -import { resolveValidation } from '../../../src/client/relay'; -import sinon from 'sinon'; -import axios from 'axios'; -import { RelayMethods } from '../../../src/models/config'; -import { StrategiesType } from '../../../src/models/config-strategy'; -import { EnvType } from '../../../src/models/environment'; -import { Client } from 'switcher-client'; - -describe('Testing Client Relay', () => { - - let axiosStub; - - beforeAll(() => { - process.env.SWITCHER_API_ENABLE = true; - }); - - test('CLIENT_RELAY_SUITE - Should resolve validation', async () => { - // Mock - axiosStub = sinon.stub(axios, 'get'); - - // Given - const mockRelayService = { data: { result: true, reason: 'Success' } }; - axiosStub.returns(Promise.resolve(mockRelayService)); - Client.assume('HTTPS_AGENT').true(); - - const relay = { - endpoint: { - default: 'https://localhost' - }, - method: RelayMethods.GET, - auth_prefix: 'Bearer', - auth_token: { - default: 'token' - } - }; - - const entry = [{ - strategy: StrategiesType.VALUE, - input: 'test' - }]; - - const res = await resolveValidation(relay, entry, EnvType.DEFAULT); - expect(res.result).toBe(true); - }); - -}); \ No newline at end of file diff --git a/tests/unit-test/config-strategy.test.js b/tests/unit-test/config-strategy.test.js deleted file mode 100644 index e43e4a1..0000000 --- a/tests/unit-test/config-strategy.test.js +++ /dev/null @@ -1,661 +0,0 @@ -import { payloadReader } from '../../src/helpers'; -import { - processOperation, - validateStrategyValue, - StrategiesType, - OperationsType -} from '../../src/models/config-strategy'; - -describe('Processing strategy: NETWORK', () => { - - const fixture_values1 = [ - '10.0.0.0/30' - ]; - - const fixture_values2 = [ - '10.0.0.0/30', '192.168.0.0/30' - ]; - - const fixture_values3 = [ - '192.168.56.56', - '192.168.56.57', - '192.168.56.58' - ]; - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input range EXIST', async () => { - const result = await processOperation( - StrategiesType.NETWORK, OperationsType.EXIST, '10.0.0.3', fixture_values1); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when input range DOES NOT EXIST', async () => { - const result = await processOperation( - StrategiesType.NETWORK, OperationsType.EXIST, '10.0.0.4', fixture_values1); - expect(result).toBe(false); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input NOT_EXIST', async () => { - const result = await processOperation( - StrategiesType.NETWORK, OperationsType.NOT_EXIST, '10.0.0.4', fixture_values1); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input IP EXIST', async () => { - const result = await processOperation( - StrategiesType.NETWORK, OperationsType.EXIST, '192.168.56.58', fixture_values3); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input IP DOES NOT EXIST', async () => { - const result = await processOperation( - StrategiesType.NETWORK, OperationsType.NOT_EXIST, '192.168.56.50', fixture_values3); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input range EXIST for multiple ranges', async () => { - const result = await processOperation( - StrategiesType.NETWORK, OperationsType.EXIST, '192.168.0.3', fixture_values2); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when input range DOES NOT EXIST for multiple ranges', async () => { - const result = await processOperation( - StrategiesType.NETWORK, OperationsType.NOT_EXIST, '127.0.0.0', fixture_values2); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return true for a valid IPv4 address as input', () => { - try { - const result = validateStrategyValue(StrategiesType.NETWORK, '10.0.0.3'); - expect(result).toBe(true); - } catch (e) { - expect(e.message).toBeNull(); - } - }); - - test('UNIT_STRATEGY_SUITE - Should NOT return for an invalid IPv4 address as input', () => { - try { - validateStrategyValue(StrategiesType.NETWORK, '10.0.0.322A'); - } catch (e) { - expect(e.message).not.toBeNull(); - } - }); - - test('UNIT_STRATEGY_SUITE - Should return true for a valid CIDR address as input', () => { - try { - const result = validateStrategyValue(StrategiesType.NETWORK, '10.0.0.0/24'); - expect(result).toBe(true); - } catch (e) { - expect(e.message).toBeNull(); - } - }); - -}); - -describe('Processing strategy: VALUE', () => { - const fixture_values1 = [ - 'USER_1' - ]; - - const fixture_values2 = [ - 'USER_1', 'USER_2' - ]; - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input EXIST', async () => { - const result = await processOperation( - StrategiesType.VALUE, OperationsType.EXIST, 'USER_1', fixture_values1); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when input DOES NOT EXIST', async () => { - const result = await processOperation( - StrategiesType.VALUE, OperationsType.EXIST, 'USER_123', fixture_values1); - expect(result).toBe(false); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input DOES NOT EXIST', async () => { - const result = await processOperation( - StrategiesType.VALUE, OperationsType.NOT_EXIST, 'USER_123', fixture_values1); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is EQUAL', async () => { - const result = await processOperation( - StrategiesType.VALUE, OperationsType.EQUAL, 'USER_1', fixture_values1); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when input is NOT EQUAL', async () => { - const result = await processOperation( - StrategiesType.VALUE, OperationsType.EQUAL, 'USER_2', fixture_values1); - expect(result).toBe(false); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is NOT EQUAL', async () => { - const result = await processOperation( - StrategiesType.VALUE, OperationsType.NOT_EQUAL, 'USER_123', fixture_values2); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when input is NOT EQUAL', async () => { - const result = await processOperation( - StrategiesType.VALUE, OperationsType.NOT_EQUAL, 'USER_2', fixture_values2); - expect(result).toBe(false); - }); -}); - -describe('Processing strategy: NUMERIC', () => { - const fixture_values1 = [ - '1' - ]; - - const fixture_values2 = [ - '1', '3' - ]; - - const fixture_values3 = [ - '1.5' - ]; - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input EXIST in values - String type', async () => { - const result = await processOperation( - StrategiesType.NUMERIC, OperationsType.EXIST, '3', fixture_values2); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input EXIST in values - Number type', async () => { - const result = await processOperation( - StrategiesType.NUMERIC, OperationsType.EXIST, 3, fixture_values2); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when input exist but test as DOES NOT EXIST ', async () => { - const result = await processOperation( - StrategiesType.NUMERIC, OperationsType.NOT_EXIST, '1', fixture_values2); - expect(result).toBe(false); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input DOES NOT EXIST in values', async () => { - const result = await processOperation( - StrategiesType.NUMERIC, OperationsType.NOT_EXIST, '2', fixture_values2); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is EQUAL to value', async () => { - const result = await processOperation( - StrategiesType.NUMERIC, OperationsType.EQUAL, '1', fixture_values1); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when input is not equal but test as EQUAL', async () => { - const result = await processOperation( - StrategiesType.NUMERIC, OperationsType.EQUAL, '2', fixture_values1); - expect(result).toBe(false); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is NOT EQUAL to value', async () => { - const result = await processOperation( - StrategiesType.NUMERIC, OperationsType.NOT_EQUAL, '2', fixture_values1); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is GREATER than value', async () => { - let result = await processOperation( - StrategiesType.NUMERIC, OperationsType.GREATER, '2', fixture_values1); - expect(result).toBe(true); - - // test decimal - result = await processOperation( - StrategiesType.NUMERIC, OperationsType.GREATER, '1.01', fixture_values1); - expect(result).toBe(true); - - result = await processOperation( - StrategiesType.NUMERIC, OperationsType.GREATER, '1.55', fixture_values3); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when input is lower but tested as GREATER than value', async () => { - let result = await processOperation( - StrategiesType.NUMERIC, OperationsType.GREATER, '0', fixture_values1); - expect(result).toBe(false); - - // test decimal - result = await processOperation( - StrategiesType.NUMERIC, OperationsType.GREATER, '0.99', fixture_values1); - expect(result).toBe(false); - - result = await processOperation( - StrategiesType.NUMERIC, OperationsType.GREATER, '1.49', fixture_values3); - expect(result).toBe(false); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is LOWER than value', async () => { - let result = await processOperation( - StrategiesType.NUMERIC, OperationsType.LOWER, '0', fixture_values1); - expect(result).toBe(true); - - // test decimal - result = await processOperation( - StrategiesType.NUMERIC, OperationsType.LOWER, '0.99', fixture_values1); - expect(result).toBe(true); - - result = await processOperation( - StrategiesType.NUMERIC, OperationsType.LOWER, '1.49', fixture_values3); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is BETWEEN values', async () => { - let result = await processOperation( - StrategiesType.NUMERIC, OperationsType.BETWEEN, '1', fixture_values2); - expect(result).toBe(true); - - // test decimal - result = await processOperation( - StrategiesType.NUMERIC, OperationsType.BETWEEN, '2.99', fixture_values2); - expect(result).toBe(true); - - result = await processOperation( - StrategiesType.NUMERIC, OperationsType.BETWEEN, '1.001', fixture_values2); - expect(result).toBe(true); - }); - -}); - -describe('Processing strategy: TIME', () => { - const fixture_values1 = [ - '08:00' - ]; - - const fixture_values2 = [ - '08:00', '10:00' - ]; - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is LOWER', async () => { - const result = await processOperation( - StrategiesType.TIME, OperationsType.LOWER, '06:00', fixture_values1); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is LOWER or SAME', async () => { - const result = await processOperation( - StrategiesType.TIME, OperationsType.LOWER, '08:00', fixture_values1); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when input is NOT LOWER', async () => { - const result = await processOperation( - StrategiesType.TIME, OperationsType.LOWER, '10:00', fixture_values1); - expect(result).toBe(false); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is GREATER', async () => { - const result = await processOperation( - StrategiesType.TIME, OperationsType.GREATER, '10:00', fixture_values1); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is GREATER or SAME', async () => { - const result = await processOperation( - StrategiesType.TIME, OperationsType.GREATER, '08:00', fixture_values1); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when input is NOT GREATER', async () => { - const result = await processOperation( - StrategiesType.TIME, OperationsType.GREATER, '06:00', fixture_values1); - expect(result).toBe(false); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is in BETWEEN', async () => { - const result = await processOperation( - StrategiesType.TIME, OperationsType.BETWEEN, '09:00', fixture_values2); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when input is NOT in BETWEEN', async () => { - const result = await processOperation( - StrategiesType.TIME, OperationsType.BETWEEN, '07:00', fixture_values2); - expect(result).toBe(false); - }); - - test('UNIT_STRATEGY_SUITE - Should return true for a valid time as input', () => { - try { - const result = validateStrategyValue(StrategiesType.TIME, '16:00'); - expect(result).toBe(true); - } catch (e) { - expect(e.message).toBeNull(); - } - }); - - test('UNIT_STRATEGY_SUITE - Should NOT return for an invalid time as input', () => { - try { - validateStrategyValue(StrategiesType.TIME, '2019-12-10'); - } catch (e) { - expect(e.message).not.toBeNull(); - } - }); -}); - -describe('Processing strategy: DATE', () => { - const fixture_values1 = [ - '2019-12-01' - ]; - - const fixture_values2 = [ - '2019-12-01', '2019-12-05' - ]; - - const fixture_values3 = [ - '2019-12-01T08:30' - ]; - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is LOWER', async () => { - const result = await processOperation( - StrategiesType.DATE, OperationsType.LOWER, '2019-11-26', fixture_values1); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is LOWER or SAME', async () => { - const result = await processOperation( - StrategiesType.DATE, OperationsType.LOWER, '2019-12-01', fixture_values1); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when input is NOT LOWER', async () => { - const result = await processOperation( - StrategiesType.DATE, OperationsType.LOWER, '2019-12-02', fixture_values1); - expect(result).toBe(false); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is GREATER', async () => { - const result = await processOperation( - StrategiesType.DATE, OperationsType.GREATER, '2019-12-02', fixture_values1); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is GREATER or SAME', async () => { - const result = await processOperation( - StrategiesType.DATE, OperationsType.GREATER, '2019-12-01', fixture_values1); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when input is NOT GREATER', async () => { - const result = await processOperation( - StrategiesType.DATE, OperationsType.GREATER, '2019-11-10', fixture_values1); - expect(result).toBe(false); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is in BETWEEN', async () => { - const result = await processOperation( - StrategiesType.DATE, OperationsType.BETWEEN, '2019-12-03', fixture_values2); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when input is NOT in BETWEEN', async () => { - const result = await processOperation( - StrategiesType.DATE, OperationsType.BETWEEN, '2019-12-12', fixture_values2); - expect(result).toBe(false); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is LOWER including time', async () => { - const result = await processOperation( - StrategiesType.DATE, OperationsType.LOWER, '2019-12-01T07:00', fixture_values3); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when input is NOT LOWER including time', async () => { - const result = await processOperation( - StrategiesType.DATE, OperationsType.LOWER, '2019-12-01T07:00', fixture_values1); - expect(result).toBe(false); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when input is GREATER including time', async () => { - const result = await processOperation( - StrategiesType.DATE, OperationsType.GREATER, '2019-12-01T08:40', fixture_values3); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return true for a valid date as input', () => { - try { - const result = validateStrategyValue(StrategiesType.DATE, '2019-12-10'); - expect(result).toBe(true); - } catch (e) { - expect(e.message).toBeNull(); - } - }); - - test('UNIT_STRATEGY_SUITE - Should NOT return for an invalid date format as input', () => { - try { - validateStrategyValue(StrategiesType.DATE, '19-12-10'); - } catch (e) { - expect(e.message).not.toBeNull(); - } - }); - - test('UNIT_STRATEGY_SUITE - Should return true for a valid date/time as input', () => { - try { - const result = validateStrategyValue(StrategiesType.DATE, '2019-12-10T08:00'); - expect(result).toBe(true); - } catch (e) { - expect(e.message).toBeNull(); - } - }); -}); - -describe('Processing strategy: REGEX', () => { - const fixture_values1 = [ - '\\bUSER_[0-9]{1,2}\\b' - ]; - - const fixture_values2 = [ - '\\bUSER_[0-9]{1,2}\\b', '\\buser-[0-9]{1,2}\\b' - ]; - - const fixture_values3 = [ - 'USER_[0-9]{1,2}' - ]; - - test('UNIT_STRATEGY_SUITE - Should return TRUE when expect to exist using EXIST operation', async () => { - let result = await processOperation( - StrategiesType.REGEX, OperationsType.EXIST, 'USER_1', fixture_values1); - expect(result).toBe(true); - - result = await processOperation( - StrategiesType.REGEX, OperationsType.EXIST, 'user-01', fixture_values2); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when expect to exist using EXIST operation', async () => { - let result = await processOperation( - StrategiesType.REGEX, OperationsType.EXIST, 'USER_123', fixture_values1); - expect(result).toBe(false); - - //fixture_values3 does not require exact match - result = await processOperation( - StrategiesType.REGEX, OperationsType.EXIST, 'USER_123', fixture_values3); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when expect to not exist using NOT_EXIST operation', async () => { - let result = await processOperation( - StrategiesType.REGEX, OperationsType.NOT_EXIST, 'USER_123', fixture_values1); - expect(result).toBe(true); - - result = await processOperation( - StrategiesType.REGEX, OperationsType.NOT_EXIST, 'user-123', fixture_values2); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when expect to not exist using NOT_EXIST operation', async () => { - const result = await processOperation( - StrategiesType.REGEX, OperationsType.NOT_EXIST, 'USER_12', fixture_values1); - expect(result).toBe(false); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when expect to be equal using EQUAL operation', async () => { - const result = await processOperation( - StrategiesType.REGEX, OperationsType.EQUAL, 'USER_11', fixture_values3); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when expect to be equal using EQUAL operation', async () => { - const result = await processOperation( - StrategiesType.REGEX, OperationsType.EQUAL, 'user-11', fixture_values3); - expect(result).toBe(false); - }); - - test('UNIT_STRATEGY_SUITE - Should return TRUE when expect to not be equal using NOT_EQUAL operation', async () => { - const result = await processOperation( - StrategiesType.REGEX, OperationsType.NOT_EQUAL, 'USER_123', fixture_values3); - expect(result).toBe(true); - }); - - test('UNIT_STRATEGY_SUITE - Should return FALSE when expect to not be equal using NOT_EQUAL operation', async () => { - const result = await processOperation( - StrategiesType.REGEX, OperationsType.NOT_EQUAL, 'USER_1', fixture_values3); - expect(result).toBe(false); - }); - - test('UNIT_STRATEGY_SUITE - Should fail for reDoS attempt', async () => { - const result = await processOperation( - StrategiesType.REGEX, OperationsType.EXIST, - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!', - ['^(([a-z])+.)+[A-Z]([a-z])+$']); - - expect(result).toBe(false); - }); -}); - -describe('Processing strategy: PAYLOAD', () => { - - const fixture_1 = JSON.stringify({ - id: '1', - login: 'petruki' - }); - - const fixture_values2 = JSON.stringify({ - product: 'product-1', - order: { - qty: 1, - deliver: { - expect: '2019-12-10', - tracking: [ - { - date: '2019-12-09', - status: 'sent' - }, - { - date: '2019-12-10', - status: 'delivered', - comments: 'comments' - } - ] - } - } - }); - - const fixture_values3 = JSON.stringify({ - description: 'Allowed IP address', - strategy: 'NETWORK_VALIDATION', - values: ['10.0.0.3/24'], - operation: 'EXIST', - env: 'default' - }); - - test('UNIT_PAYLOAD_SUITE - Should read keys from payload #1', () => { - const keys = payloadReader(JSON.parse(fixture_values2)); - expect(keys).toEqual([ - 'product', - 'order', - 'order.qty', - 'order.deliver', - 'order.deliver.expect', - 'order.deliver.tracking', - 'order.deliver.tracking.date', - 'order.deliver.tracking.status', - 'order.deliver.tracking.comments' - ]); - }); - - test('UNIT_PAYLOAD_SUITE - Should read keys from payload #2', () => { - const keys = payloadReader(JSON.parse(fixture_values3)); - expect(keys).toEqual([ - 'description', - 'strategy', - 'values', - 'operation', - 'env' - ]); - }); - - test('UNIT_PAYLOAD_SUITE - Should read keys from payload with array values', () => { - const keys = payloadReader({ - order: { - items: ['item_1', 'item_2'] - } - }); - expect(keys).toEqual([ - 'order', - 'order.items' - ]); - }); - - test('UNIT_PAYLOAD_SUITE - Should return TRUE when payload has field', async () => { - expect(await processOperation( - StrategiesType.PAYLOAD, OperationsType.HAS_ONE, fixture_1, ['login']) - ).toBe(true); - }); - - test('UNIT_PAYLOAD_SUITE - Should return FALSE when payload does not have field', async () => { - expect(await processOperation( - StrategiesType.PAYLOAD, OperationsType.HAS_ONE, fixture_1, ['user']) - ).toBe(false); - }); - - test('UNIT_PAYLOAD_SUITE - Should return TRUE when payload has nested field', async () => { - expect(await processOperation( - StrategiesType.PAYLOAD, OperationsType.HAS_ONE, fixture_values2, [ - 'order.qty', 'order.total' - ]) - ).toBe(true); - }); - - test('UNIT_PAYLOAD_SUITE - Should return TRUE when payload has nested field with arrays', async () => { - expect(await processOperation( - StrategiesType.PAYLOAD, OperationsType.HAS_ONE, fixture_values2, [ - 'order.deliver.tracking.status' - ]) - ).toBe(true); - }); - - test('UNIT_PAYLOAD_SUITE - Should return TRUE when payload has all', async () => { - expect(await processOperation( - StrategiesType.PAYLOAD, OperationsType.HAS_ALL, fixture_values2, [ - 'product', - 'order', - 'order.qty', - 'order.deliver', - 'order.deliver.expect', - 'order.deliver.tracking', - 'order.deliver.tracking.date', - 'order.deliver.tracking.status' - ]) - ).toBe(true); - }); - - test('UNIT_PAYLOAD_SUITE - Should return FALSE when payload does not have all', async () => { - expect(await processOperation( - StrategiesType.PAYLOAD, OperationsType.HAS_ALL, fixture_values2, [ - 'product', - 'order', - 'order.NOT_EXIST_KEY', - ]) - ).toBe(false); - }); - - test('UNIT_PAYLOAD_SUITE - Should return FALSE when payload is not a JSON string', async () => { - expect(await processOperation( - StrategiesType.PAYLOAD, OperationsType.HAS_ALL, 'NOT_JSON', []) - ).toBe(false); - }); -}); \ No newline at end of file diff --git a/tests/unit-test/switcher-api-facade.test.js b/tests/unit-test/switcher-api-facade.test.js index b682d37..e7b6593 100644 --- a/tests/unit-test/switcher-api-facade.test.js +++ b/tests/unit-test/switcher-api-facade.test.js @@ -28,6 +28,7 @@ import { component1Key } from '../fixtures/db_api'; import { Client } from 'switcher-client'; +import { DEFAULT_RATE_LIMIT } from '../../src/middleware/limiter'; afterAll(async () => { await new Promise(resolve => setTimeout(resolve, 1000)); @@ -255,7 +256,7 @@ describe('Testing Switcher API Facade', () => { return getRateLimit(component1Key, component1); }; - await expect(call()).resolves.toBe(parseInt(process.env.MAX_REQUEST_PER_MINUTE)); + await expect(call()).resolves.toBe(parseInt(DEFAULT_RATE_LIMIT)); }); }); \ No newline at end of file diff --git a/tests/unit-test/try-match.test.js b/tests/unit-test/try-match.test.js deleted file mode 100644 index 6fabe89..0000000 --- a/tests/unit-test/try-match.test.js +++ /dev/null @@ -1,90 +0,0 @@ -import TimedMatch from '../../src/helpers/timed-match'; - -describe('Test tryMatch', () => { - - const evilRE = '^(([a-z])+.)+[A-Z]([a-z])+$'; - const evilInput1 = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; - const evilInput2 = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; - - beforeEach(() => { - TimedMatch.setMaxBlackListed(50); - TimedMatch.setMaxTimeLimit(3000); - TimedMatch.clearBlackList(); - }); - - test('UNIT_TRY_MATCH - Should match value', async () => { - const result = await TimedMatch.tryMatch(['USER_[0-9]{1,2}'], 'USER_1'); - expect(result).toBe(true); - }); - - test('UNIT_TRY_MATCH - Should fail for reDoS attempt - default 3000 ms', async () => { - let timer = Date.now(); - const result = await TimedMatch.tryMatch([evilRE], evilInput1); - timer = Date.now() - timer; - - expect(timer).not.toBeLessThan(3000); - expect(result).toBe(false); - }); - - test('UNIT_TRY_MATCH - Should fail for reDoS attempt - 2000 ms', async () => { - TimedMatch.setMaxTimeLimit(2000); - - let timer = Date.now(); - const result = await TimedMatch.tryMatch([evilRE], evilInput1); - timer = Date.now() - timer; - - expect(timer).not.toBeLessThan(2000); - expect(timer).not.toBeGreaterThan(2500); - expect(result).toBe(false); - }); - - test('UNIT_TRY_MATCH - Should return from black list after fail to perfom match', async () => { - TimedMatch.setMaxTimeLimit(1000); - let timer, result; - - timer = Date.now(); - result = await TimedMatch.tryMatch([evilRE], evilInput1); - timer = Date.now() - timer; - - expect(timer).not.toBeLessThan(1000); - expect(timer).not.toBeGreaterThan(1500); - expect(result).toBe(false); - - timer = Date.now(); - result = await TimedMatch.tryMatch([evilRE], evilInput1); - timer = Date.now() - timer; - - expect(timer).not.toBeGreaterThan(100); - expect(result).toBe(false); - }); - - test('UNIT_TRY_MATCH - Should replace black listed failed match', async () => { - TimedMatch.setMaxTimeLimit(500); - TimedMatch.setMaxBlackListed(1); - let timer, result; - - timer = Date.now(); - result = await TimedMatch.tryMatch([evilRE], evilInput1); - timer = Date.now() - timer; - - expect(timer).not.toBeLessThan(500); - expect(timer).not.toBeGreaterThan(1000); - expect(result).toBe(false); - - timer = Date.now(); - result = await TimedMatch.tryMatch([evilRE], evilInput2); - timer = Date.now() - timer; - - expect(timer).not.toBeLessThan(500); - expect(timer).not.toBeGreaterThan(1000); - expect(result).toBe(false); - - timer = Date.now(); - result = await TimedMatch.tryMatch([evilRE], evilInput2); - timer = Date.now() - timer; - - expect(timer).not.toBeGreaterThan(100); - expect(result).toBe(false); - }); - -}); \ No newline at end of file