From 3d58b7eadf936ff41578e5ae0879e67e3c005eb3 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Wed, 12 Jul 2023 00:20:35 -0700 Subject: [PATCH] Added route to expose Switcher Client to UI --- requests/Switcher API.postman_collection.json | 39 ++++++ src/api-docs/paths/path-api-management.js | 43 ++++++ src/api-docs/swagger-document.js | 4 +- src/app.js | 2 + src/external/switcher-api-facade.js | 14 ++ src/routers/api-management.js | 23 ++++ tests/api-management.test.js | 122 ++++++++++++++++++ 7 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 src/api-docs/paths/path-api-management.js create mode 100644 src/routers/api-management.js create mode 100644 tests/api-management.test.js diff --git a/requests/Switcher API.postman_collection.json b/requests/Switcher API.postman_collection.json index 4f101f0..18d184a 100644 --- a/requests/Switcher API.postman_collection.json +++ b/requests/Switcher API.postman_collection.json @@ -5723,6 +5723,45 @@ } ] }, + { + "name": "API Management", + "item": [ + { + "name": "Management - Feature", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"feature\": \"FEATURE\",\n\t\"parameters\": {\n \"value\": \"test\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/api-management/feature", + "host": [ + "{{url}}" + ], + "path": [ + "api-management", + "feature" + ] + } + }, + "response": [] + } + ] + }, { "name": "API Check", "event": [ diff --git a/src/api-docs/paths/path-api-management.js b/src/api-docs/paths/path-api-management.js new file mode 100644 index 0000000..5b43296 --- /dev/null +++ b/src/api-docs/paths/path-api-management.js @@ -0,0 +1,43 @@ +export default { + '/api-management/feature': { + post: { + tags: ['API Management'], + description: 'Run feature validation', + security: [{ appAuth: [] }], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + feature: { + type: 'string' + }, + parameters: { + type: 'object' + } + } + } + } + } + }, + responses: { + 200: { + description: 'Success', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + status: { + type: 'boolean' + } + } + } + } + } + } + } + } + } +}; \ No newline at end of file diff --git a/src/api-docs/swagger-document.js b/src/api-docs/swagger-document.js index 8c3824a..d31742d 100644 --- a/src/api-docs/swagger-document.js +++ b/src/api-docs/swagger-document.js @@ -10,6 +10,7 @@ import pathPermission from './paths/path-permission'; import pathMetric from './paths/path-metric'; import pathSlack from './paths/path-slack'; import pathClient from './paths/path-client'; +import pathApiManagement from './paths/path-api-management'; import { commonSchema } from './schemas/common'; import adminSchema from './schemas/admin'; @@ -90,6 +91,7 @@ export default { ...pathPermission, ...pathMetric, ...pathClient, - ...pathSlack + ...pathSlack, + ...pathApiManagement } }; \ No newline at end of file diff --git a/src/app.js b/src/app.js index 0271c90..5348fbe 100644 --- a/src/app.js +++ b/src/app.js @@ -20,6 +20,7 @@ import metricRouter from './routers/metric'; import teamRouter from './routers/team'; import permissionRouter from './routers/permission'; import slackRouter from './routers/slack'; +import apiManagementRouter from './routers/api-management'; import schema from './client/schema'; import { appAuth, auth, resourcesAuth, slackAuth } from './middleware/auth'; import { clientLimiter, defaultLimiter } from './middleware/limiter'; @@ -50,6 +51,7 @@ app.use(metricRouter); app.use(teamRouter); app.use(permissionRouter); app.use(slackRouter); +app.use(apiManagementRouter); /** * GraphQL Routes diff --git a/src/external/switcher-api-facade.js b/src/external/switcher-api-facade.js index 0db1572..d00759d 100644 --- a/src/external/switcher-api-facade.js +++ b/src/external/switcher-api-facade.js @@ -246,4 +246,18 @@ export async function checkHttpsAgent(value) { return; return checkFeature(SwitcherKeys.HTTPS_AGENT, [checkRegex(value)]); +} + +export async function checkManagementFeature(feature, params) { + if (process.env.SWITCHER_API_ENABLE != 'true') + return true; + + const switcher = Switcher.factory(); + const entries = []; + + if (params?.value) { + entries.push(checkValue(params.value)); + } + + return switcher.isItOn(feature, entries); } \ No newline at end of file diff --git a/src/routers/api-management.js b/src/routers/api-management.js new file mode 100644 index 0000000..baefa15 --- /dev/null +++ b/src/routers/api-management.js @@ -0,0 +1,23 @@ +import express from 'express'; +import { auth } from '../middleware/auth'; +import { responseException } from '../exceptions'; +import * as SwitcherAPI from '../external/switcher-api-facade'; +import { validate, verifyInputUpdateParameters } from '../middleware/validators'; +import { body } from 'express-validator'; + +const router = new express.Router(); + +router.post('/api-management/feature', auth, +verifyInputUpdateParameters(['feature', 'parameters']), [ + body('feature').isString().notEmpty(), + body('parameters').optional().isObject() +], validate, async (req, res) => { + try { + const status = await SwitcherAPI.checkManagementFeature(req.body.feature, req.body.parameters); + res.send({ status }); + } catch (e) { + responseException(res, e, 400); + } +}); + +export default router; \ No newline at end of file diff --git a/tests/api-management.test.js b/tests/api-management.test.js new file mode 100644 index 0000000..6b8665b --- /dev/null +++ b/tests/api-management.test.js @@ -0,0 +1,122 @@ +import mongoose from 'mongoose'; +import app from '../src/app'; +import request from 'supertest'; +import { + adminMasterAccount, + setupDatabase +} from './fixtures/db_api'; +import { Switcher } from 'switcher-client'; + +afterAll(async () => { + await new Promise(resolve => setTimeout(resolve, 1000)); + await mongoose.disconnect(); +}); + +describe('API Management', () => { + + let token; + + beforeAll(async () => { + await setupDatabase(); + + const res = await request(app) + .post('/admin/login') + .send({ + email: adminMasterAccount.email, + password: adminMasterAccount.password + }).expect(200); + + token = res.body.jwt.token; + }); + + beforeEach(async () => { + process.env.SWITCHER_API_ENABLE = true; + Switcher.forget('MY_FEATURE'); + }); + + test('API_MANAGEMENT - Should return TRUE when SWITCHER_API_ENABLE disabled', async () => { + process.env.SWITCHER_API_ENABLE = false; + + const res = await request(app) + .post('/api-management/feature') + .set('Authorization', `Bearer ${token}`) + .send({ + feature: 'MY_FEATURE' + }).expect(200); + + expect(res.body.status).toEqual(true); + }); + + test('API_MANAGEMENT - Should return TRUE when requesting feature `MY_FEATURE`', async () => { + Switcher.assume('MY_FEATURE').true(); + + const res = await request(app) + .post('/api-management/feature') + .set('Authorization', `Bearer ${token}`) + .send({ + feature: 'MY_FEATURE' + }).expect(200); + + expect(res.body.status).toEqual(true); + }); + + test('API_MANAGEMENT - Should return TRUE when requesting feature `MY_FEATURE` with parameters - value', async () => { + Switcher.assume('MY_FEATURE').false(); + + const res = await request(app) + .post('/api-management/feature') + .set('Authorization', `Bearer ${token}`) + .send({ + feature: 'MY_FEATURE', + parameters: { + value: 'my-value' + } + }).expect(200); + + expect(res.body.status).toEqual(false); + }); + + test('API_MANAGEMENT - Should NOT return when body has invalid payload', async () => { + await request(app) + .post('/api-management/feature') + .set('Authorization', `Bearer ${token}`) + .send({ + features: ['MY_FEATURE'] + }).expect(400); + }); + + test('API_MANAGEMENT - Should NOT return when API cannot respond', async () => { + await request(app) + .post('/api-management/feature') + .set('Authorization', `Bearer ${token}`) + .send({ + feature: 'MY_FEATURE_1' + }).expect(400); + }); + + test('API_MANAGEMENT - Should NOT return when feature not specified', async () => { + await request(app) + .post('/api-management/feature') + .set('Authorization', `Bearer ${token}`) + .send().expect(422); + }); + + test('API_MANAGEMENT - Should NOT return when paramaters is not an object', async () => { + await request(app) + .post('/api-management/feature') + .set('Authorization', `Bearer ${token}`) + .send({ + feature: 'MY_FEATURE', + parameters: 'my-value' + }).expect(422); + }); + + test('API_MANAGEMENT - Should NOT return when not logged', async () => { + await request(app) + .post('/api-management/feature') + .send({ + feature: 'MY_FEATURE' + }).expect(401); + }); + +}); \ No newline at end of file