diff --git a/config/default.json b/config/default.json index 2b0ac247..4da2970e 100644 --- a/config/default.json +++ b/config/default.json @@ -35,6 +35,7 @@ "idleTimeout": 1000 }, "kafkaConfig": { + "hosts": "localhost:9092" }, "analyticsKey": "", "VALID_ISSUERS": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\"]", diff --git a/migrations/20190129_organization_config.sql b/migrations/20190129_organization_config.sql new file mode 100644 index 00000000..3e9e041e --- /dev/null +++ b/migrations/20190129_organization_config.sql @@ -0,0 +1,28 @@ +-- +-- CREATE NEW TABLE: +-- org_config +-- +CREATE TABLE org_config ( + id bigint NOT NULL, + orgId character varying(45) NOT NULL, + configName character varying(45) NOT NULL, + configValue character varying(512), + "deletedAt" timestamp with time zone, + "createdAt" timestamp with time zone, + "updatedAt" timestamp with time zone, + "deletedBy" bigint, + "createdBy" bigint NOT NULL, + "updatedBy" bigint NOT NULL +); + +CREATE SEQUENCE org_config_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE org_config_id_seq OWNED BY org_config.id; + +ALTER TABLE org_config + ALTER COLUMN id SET DEFAULT nextval('org_config_id_seq'); diff --git a/postman.json b/postman.json index ecd5c408..38312bf2 100644 --- a/postman.json +++ b/postman.json @@ -3301,6 +3301,180 @@ } ] }, + { + "name": "Organization Config", + "description": "", + "item": [ + { + "name": "Create organization config", + "request": { + "url": "{{api-url}}/v4/orgConfig", + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"param\":{\r\n \"orgId\": \"20000013\",\r\n \"configName\": \"project_catalog_url\",\r\n \"configValue\": \"/projects/1\"\r\n }\r\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "List organization config", + "request": { + "url": "{{api-url}}/v4/orgConfig", + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "List organization config - filter", + "request": { + "url": { + "raw": "{{api-url}}/v4/orgConfig?filter=orgId=in(20000010,20000013,20000015)%26configName%3Dproject_catalog_url", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "orgConfig" + ], + "query": [ + { + "key": "filter", + "value": "orgId=in(20000010,20000013,20000015)%26configName%3Dproject_catalog_url", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Get organization config", + "request": { + "url": "{{api-url}}/v4/orgConfig/1", + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Update organization config", + "request": { + "url": "{{api-url}}/v4/orgConfig/1", + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"param\":{\r\n \"configName\": \"project_catalog_url\"\r\n }\r\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Delete organization config", + "request": { + "url": "{{api-url}}/v4/orgConfig/1", + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + } + ] + }, { "name": "Product Category", "item": [ @@ -5010,4 +5184,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/models/orgConfig.js b/src/models/orgConfig.js new file mode 100644 index 00000000..79c2ae9b --- /dev/null +++ b/src/models/orgConfig.js @@ -0,0 +1,28 @@ +/* eslint-disable valid-jsdoc */ + +/** + * The Organization config model + */ +module.exports = (sequelize, DataTypes) => { + const OrgConfig = sequelize.define('OrgConfig', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + orgId: { type: DataTypes.STRING(45), allowNull: false }, + configName: { type: DataTypes.STRING(45), allowNull: false }, + configValue: { type: DataTypes.STRING(512) }, + deletedAt: DataTypes.DATE, + createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + deletedBy: DataTypes.BIGINT, + createdBy: { type: DataTypes.BIGINT, allowNull: false }, + updatedBy: { type: DataTypes.BIGINT, allowNull: false }, + }, { + tableName: 'org_config', + paranoid: true, + timestamps: true, + updatedAt: 'updatedAt', + createdAt: 'createdAt', + deletedAt: 'deletedAt', + }); + + return OrgConfig; +}; diff --git a/src/permissions/index.js b/src/permissions/index.js index 0b9880e3..e2bce5ca 100644 --- a/src/permissions/index.js +++ b/src/permissions/index.js @@ -54,6 +54,11 @@ module.exports = () => { Authorizer.setPolicy('projectType.delete', projectAdmin); Authorizer.setPolicy('projectType.view', true); // anyone can view project types + Authorizer.setPolicy('orgConfig.create', projectAdmin); + Authorizer.setPolicy('orgConfig.edit', projectAdmin); + Authorizer.setPolicy('orgConfig.delete', projectAdmin); + Authorizer.setPolicy('orgConfig.view', true); // anyone can view project types + Authorizer.setPolicy('productCategory.create', projectAdmin); Authorizer.setPolicy('productCategory.edit', projectAdmin); Authorizer.setPolicy('productCategory.delete', projectAdmin); diff --git a/src/routes/index.js b/src/routes/index.js index dabe7454..716cdd2e 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -42,6 +42,12 @@ router.route('/v4/projects/metadata/projectTypes') router.route('/v4/projects/metadata/projectTypes/:key') .get(require('./projectTypes/get')); +router.route('/v4/orgConfig') + .get(require('./orgConfig/list')); + +router.route('/v4/orgConfig/:id(\\d+)') + .get(require('./orgConfig/get')); + router.route('/v4/projects/metadata/productCategories') .get(require('./productCategories/list')); router.route('/v4/projects/metadata/productCategories/:key') @@ -53,7 +59,7 @@ router.route('/v4/projects/metadata') .get(require('./metadata/list')); router.all( - RegExp(`\\/${apiVersion}\\/(projects|timelines)(?!\\/health).*`), (req, res, next) => ( + RegExp(`\\/${apiVersion}\\/(projects|timelines|orgConfig)(?!\\/health).*`), (req, res, next) => ( // JWT authentication jwtAuth(config)(req, res, next) ), @@ -182,6 +188,13 @@ router.route('/v4/projects/:projectId(\\d+)/members/invite') .put(require('./projectMemberInvites/update')) .get(require('./projectMemberInvites/get')); +router.route('/v4/orgConfig') + .post(require('./orgConfig/create')); + +router.route('/v4/orgConfig/:id(\\d+)') + .patch(require('./orgConfig/update')) + .delete(require('./orgConfig/delete')); + // register error handler router.use((err, req, res, next) => { // eslint-disable-line no-unused-vars // DO NOT REMOVE next arg.. even though eslint diff --git a/src/routes/orgConfig/create.js b/src/routes/orgConfig/create.js new file mode 100644 index 00000000..6b53d6d2 --- /dev/null +++ b/src/routes/orgConfig/create.js @@ -0,0 +1,58 @@ +/** + * API to add a organization config + */ +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../util'; +import models from '../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + body: { + param: Joi.object().keys({ + id: Joi.any().strip(), + orgId: Joi.string().max(45).required(), + configName: Joi.string().max(45).required(), + configValue: Joi.string().max(512), + createdAt: Joi.any().strip(), + updatedAt: Joi.any().strip(), + deletedAt: Joi.any().strip(), + createdBy: Joi.any().strip(), + updatedBy: Joi.any().strip(), + deletedBy: Joi.any().strip(), + }).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('orgConfig.create'), + (req, res, next) => { + const entity = _.assign(req.body.param, { + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + }); + + // Check if duplicated key + return models.OrgConfig.findOne({ where: { orgId: req.body.param.orgId, configName: req.body.param.configName } }) + .then((existing) => { + if (existing) { + const apiErr = new Error(`Organization config exists for orgId ${req.body.param.orgId} + and configName ${req.body.param.configName}`); + apiErr.status = 422; + return Promise.reject(apiErr); + } + + // Create + return models.OrgConfig.create(entity); + }).then((createdEntity) => { + // Omit deletedAt, deletedBy + res.status(201).json(util.wrapResponse( + req.id, _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'), 1, 201)); + }) + .catch(next); + }, +]; diff --git a/src/routes/orgConfig/create.spec.js b/src/routes/orgConfig/create.spec.js new file mode 100644 index 00000000..4f4ef848 --- /dev/null +++ b/src/routes/orgConfig/create.spec.js @@ -0,0 +1,161 @@ +/** + * Tests for create.js + */ +import _ from 'lodash'; +import chai from 'chai'; +import request from 'supertest'; + +import server from '../../app'; +import testUtil from '../../tests/util'; +import models from '../../models'; + +const should = chai.should(); + +describe('CREATE organization config', () => { + beforeEach(() => testUtil.clearDb() + .then(() => models.OrgConfig.create({ + orgId: 'ORG1', + configName: 'project_catefory_url', + configValue: 'http://localhost/url', + createdBy: 1, + updatedBy: 1, + })).then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('POST /orgConfig', () => { + const body = { + param: { + orgId: 'ORG2', + configName: 'project_catefory_url', + configValue: 'http://localhost/url', + }, + }; + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .post('/v4/orgConfig') + .send(body) + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .post('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .post('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(body) + .expect(403, done); + }); + + it('should return 403 for manager', (done) => { + request(server) + .post('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send(body) + .expect(403, done); + }); + + it('should return 422 for missing orgId', (done) => { + const invalidBody = _.cloneDeep(body); + delete invalidBody.param.orgId; + + request(server) + .post('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect('Content-Type', /json/) + .expect(422, done); + }); + + it('should return 422 for missing configName', (done) => { + const invalidBody = _.cloneDeep(body); + delete invalidBody.param.configName; + + request(server) + .post('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect('Content-Type', /json/) + .expect(422, done); + }); + + it('should return 422 for duplicated orgId and configName', (done) => { + const invalidBody = _.cloneDeep(body); + invalidBody.param.orgId = 'ORG1'; + invalidBody.param.configName = 'project_catefory_url'; + + request(server) + .post('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect('Content-Type', /json/) + .expect(422, done); + }); + + it('should return 201 for admin', (done) => { + request(server) + .post('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.orgId.should.be.eql(body.param.orgId); + resJson.configName.should.be.eql(body.param.configName); + resJson.configValue.should.be.eql(body.param.configValue); + + resJson.createdBy.should.be.eql(40051333); // admin + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + + done(); + }); + }); + + it('should return 201 for connect admin', (done) => { + request(server) + .post('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.orgId.should.be.eql(body.param.orgId); + resJson.configName.should.be.eql(body.param.configName); + resJson.configValue.should.be.eql(body.param.configValue); + resJson.createdBy.should.be.eql(40051336); // connect admin + resJson.updatedBy.should.be.eql(40051336); // connect admin + done(); + }); + }); + }); +}); diff --git a/src/routes/orgConfig/delete.js b/src/routes/orgConfig/delete.js new file mode 100644 index 00000000..d33e7608 --- /dev/null +++ b/src/routes/orgConfig/delete.js @@ -0,0 +1,37 @@ +/** + * API to delete a organization config + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + id: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('orgConfig.delete'), + (req, res, next) => + models.sequelize.transaction(() => + models.OrgConfig.findById(req.params.id) + .then((entity) => { + if (!entity) { + const apiErr = new Error(`Organization config not found for id ${req.params.id}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + // Update the deletedBy, then delete + return entity.update({ deletedBy: req.authUser.userId }); + }) + .then(entity => entity.destroy())) + .then(() => { + res.status(204).end(); + }) + .catch(next), +]; diff --git a/src/routes/orgConfig/delete.spec.js b/src/routes/orgConfig/delete.spec.js new file mode 100644 index 00000000..85f39033 --- /dev/null +++ b/src/routes/orgConfig/delete.spec.js @@ -0,0 +1,126 @@ +/** + * Tests for delete.js + */ +import request from 'supertest'; +import chai from 'chai'; +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const expectAfterDelete = (id, err, next) => { + if (err) throw err; + setTimeout(() => + models.OrgConfig.findOne({ + where: { + id, + }, + paranoid: false, + }) + .then((res) => { + if (!res) { + throw new Error('Should found the entity'); + } else { + chai.assert.isNotNull(res.deletedAt); + chai.assert.isNotNull(res.deletedBy); + + request(server) + .get(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, next); + } + }), 500); +}; + +describe('DELETE organization config', () => { + const id = 1; + + beforeEach(() => testUtil.clearDb() + .then(() => models.OrgConfig.create({ + id: 1, + orgId: 'ORG1', + configName: 'project_category_url', + configValue: '/projects/1', + createdBy: 1, + updatedBy: 1, + })).then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('DELETE /orgConfig/{id}', () => { + it('should return 403 if user is not authenticated', (done) => { + request(server) + .delete(`/v4/orgConfig/${id}`) + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .delete(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .delete(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + + it('should return 403 for manager', (done) => { + request(server) + .delete(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(403, done); + }); + + it('should return 404 for non-existed config', (done) => { + request(server) + .delete('/v4/orgConfig/not_existed') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 404 for deleted config', (done) => { + models.OrgConfig.destroy({ where: { id } }) + .then(() => { + request(server) + .delete(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + }); + + it('should return 204, for admin, if config was successfully removed', (done) => { + request(server) + .delete(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204) + .end(err => expectAfterDelete(id, err, done)); + }); + + it('should return 204, for connect admin, if config was successfully removed', (done) => { + request(server) + .delete(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(204) + .end(err => expectAfterDelete(id, err, done)); + }); + }); +}); diff --git a/src/routes/orgConfig/get.js b/src/routes/orgConfig/get.js new file mode 100644 index 00000000..5c14779b --- /dev/null +++ b/src/routes/orgConfig/get.js @@ -0,0 +1,39 @@ +/** + * API to get a organization config + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../util'; +import models from '../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + id: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('orgConfig.view'), + (req, res, next) => models.OrgConfig.findOne({ + where: { + id: req.params.id, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((orgConfig) => { + // Not found + if (!orgConfig) { + const apiErr = new Error(`Organization config not found for id ${req.params.id}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + res.json(util.wrapResponse(req.id, orgConfig)); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/orgConfig/get.spec.js b/src/routes/orgConfig/get.spec.js new file mode 100644 index 00000000..34a4d337 --- /dev/null +++ b/src/routes/orgConfig/get.spec.js @@ -0,0 +1,121 @@ +/** + * Tests for get.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); + +describe('GET organization config', () => { + const config = { + id: 1, + orgId: 'ORG1', + configName: 'project_category_url', + configValue: '/projects/1', + createdBy: 1, + updatedBy: 1, + }; + + const id = config.id; + + beforeEach(() => testUtil.clearDb() + .then(() => models.OrgConfig.create(config)) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /orgConfig/{id}', () => { + it('should return 404 for non-existed config', (done) => { + request(server) + .get('/v4/orgConfig/1234') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 404 for deleted config', (done) => { + models.OrgConfig.destroy({ where: { id } }) + .then(() => { + request(server) + .get(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + }); + + it('should return 200 for admin', (done) => { + request(server) + .get(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.id.should.be.eql(config.id); + resJson.orgId.should.be.eql(config.orgId); + resJson.configName.should.be.eql(config.configName); + resJson.configValue.should.be.eql(config.configValue); + resJson.createdBy.should.be.eql(config.createdBy); + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(config.updatedBy); + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + + done(); + }); + }); + + it('should return 200 even if user is not authenticated', (done) => { + request(server) + .get(`/v4/orgConfig/${id}`) + .expect(200, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/orgConfig/list.js b/src/routes/orgConfig/list.js new file mode 100644 index 00000000..55f05f1d --- /dev/null +++ b/src/routes/orgConfig/list.js @@ -0,0 +1,31 @@ +/** + * API to list organization config + */ +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; +import util from '../../util'; + +const permissions = tcMiddleware.permissions; + +module.exports = [ + permissions('orgConfig.view'), + (req, res, next) => { + // handle filters + const filters = util.parseQueryFilter(req.query.filter); + if (!util.isValidFilter(filters, ['orgId', 'configName'])) { + return util.handleError('Invalid filters', null, req, next); + } + req.log.debug(filters); + // Get all organization config + const where = filters || {}; + return models.OrgConfig.findAll({ + where, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + raw: true, + }) + .then((orgConfigs) => { + res.json(util.wrapResponse(req.id, orgConfigs)); + }) + .catch(next); + }, +]; diff --git a/src/routes/orgConfig/list.spec.js b/src/routes/orgConfig/list.spec.js new file mode 100644 index 00000000..91ac0cdb --- /dev/null +++ b/src/routes/orgConfig/list.spec.js @@ -0,0 +1,137 @@ +/** + * Tests for list.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); + +describe('LIST organization config', () => { + const configs = [ + { + id: 1, + orgId: 'ORG1', + configName: 'project_category_url', + configValue: '/projects/1', + createdBy: 1, + updatedBy: 1, + }, + { + id: 2, + orgId: 'ORG1', + configName: 'project_catalog_url', + configValue: '/projects/2', + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.OrgConfig.create(configs[0])) + .then(() => models.OrgConfig.create(configs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /orgConfig', () => { + it('should return 200 for admin', (done) => { + request(server) + .get('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const config = configs[0]; + + const resJson = res.body.result.content; + resJson.should.have.length(2); + resJson[0].id.should.be.eql(config.id); + resJson[0].orgId.should.be.eql(config.orgId); + resJson[0].configName.should.be.eql(config.configName); + resJson[0].configValue.should.be.eql(config.configValue); + should.exist(resJson[0].createdAt); + resJson[0].updatedBy.should.be.eql(config.updatedBy); + should.exist(resJson[0].updatedAt); + should.not.exist(resJson[0].deletedBy); + should.not.exist(resJson[0].deletedAt); + + done(); + }); + }); + + it('should return 200 with filters', (done) => { + request(server) + .get(`/v4/orgConfig?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const config = configs[0]; + + const resJson = res.body.result.content; + resJson.should.have.length(1); + resJson[0].id.should.be.eql(config.id); + resJson[0].orgId.should.be.eql(config.orgId); + resJson[0].configName.should.be.eql(config.configName); + resJson[0].configValue.should.be.eql(config.configValue); + should.exist(resJson[0].createdAt); + resJson[0].updatedBy.should.be.eql(config.updatedBy); + should.exist(resJson[0].updatedAt); + should.not.exist(resJson[0].deletedBy); + should.not.exist(resJson[0].deletedAt); + + done(); + }); + }); + + it('should return 200 even if user is not authenticated', (done) => { + request(server) + .get('/v4/orgConfig') + .expect(200, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/orgConfig/update.js b/src/routes/orgConfig/update.js new file mode 100644 index 00000000..7bc2b52d --- /dev/null +++ b/src/routes/orgConfig/update.js @@ -0,0 +1,63 @@ +/** + * API to update a organization config + */ +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../util'; +import models from '../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + id: Joi.number().integer().positive().required(), + }, + body: { + param: Joi.object().keys({ + id: Joi.any().strip(), + orgId: Joi.string().max(45).optional(), + configName: Joi.string().max(45).optional(), + configValue: Joi.string().max(512).optional(), + createdAt: Joi.any().strip(), + updatedAt: Joi.any().strip(), + deletedAt: Joi.any().strip(), + createdBy: Joi.any().strip(), + updatedBy: Joi.any().strip(), + deletedBy: Joi.any().strip(), + }).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('orgConfig.edit'), + (req, res, next) => { + const entityToUpdate = _.assign(req.body.param, { + updatedBy: req.authUser.userId, + }); + + return models.OrgConfig.findOne({ + where: { + id: req.params.id, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((orgConfig) => { + // Not found + if (!orgConfig) { + const apiErr = new Error(`Organization config not found for id ${req.params.id}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + return orgConfig.update(entityToUpdate); + }) + .then((orgConfig) => { + res.json(util.wrapResponse(req.id, orgConfig)); + return Promise.resolve(); + }) + .catch(next); + }, +]; diff --git a/src/routes/orgConfig/update.spec.js b/src/routes/orgConfig/update.spec.js new file mode 100644 index 00000000..69ff3319 --- /dev/null +++ b/src/routes/orgConfig/update.spec.js @@ -0,0 +1,229 @@ +/** + * Tests for get.js + */ +import _ from 'lodash'; +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); + +describe('UPDATE organization config', () => { + const config = { + id: 1, + orgId: 'ORG1', + configName: 'project_category_url', + configValue: '/projects/1', + createdBy: 1, + updatedBy: 1, + }; + const id = config.id; + + beforeEach(() => testUtil.clearDb() + .then(() => models.OrgConfig.create(config)) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('PATCH /orgConfig/{id}', () => { + const body = { + param: { + id: 1, + orgId: 'ORG2', + configName: 'project_category_url_update', + configValue: '/projects/2', + }, + }; + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .patch(`/v4/orgConfig/${id}`) + .send(body) + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .patch(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .patch(`/v4/orgConfig/${id}`) + .send(body) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + + it('should return 403 for manager', (done) => { + request(server) + .patch(`/v4/orgConfig/${id}`) + .send(body) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(403, done); + }); + + it('should return 404 for non-existed config', (done) => { + request(server) + .patch('/v4/orgConfig/1234') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect(404, done); + }); + + it('should return 404 for deleted config', (done) => { + models.OrgConfig.destroy({ where: { id } }) + .then(() => { + request(server) + .patch(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect(404, done); + }); + }); + + it('should return 200 for admin configValue updated', (done) => { + const partialBody = _.cloneDeep(body); + delete partialBody.param.orgId; + delete partialBody.param.configName; + request(server) + .patch(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(partialBody) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.id.should.be.eql(id); + resJson.orgId.should.be.eql(config.orgId); + resJson.configName.should.be.eql(config.configName); + resJson.configValue.should.be.eql(partialBody.param.configValue); + resJson.createdBy.should.be.eql(config.createdBy); + resJson.createdBy.should.be.eql(config.createdBy); // should not update createdAt + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + + done(); + }); + }); + + it('should return 200 for admin orgId updated', (done) => { + const partialBody = _.cloneDeep(body); + delete partialBody.param.configName; + delete partialBody.param.configValue; + request(server) + .patch(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(partialBody) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.id.should.be.eql(id); + resJson.orgId.should.be.eql(partialBody.param.orgId); + resJson.configName.should.be.eql(config.configName); + resJson.configValue.should.be.eql(config.configValue); + resJson.createdBy.should.be.eql(config.createdBy); + resJson.createdBy.should.be.eql(config.createdBy); // should not update createdAt + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + + done(); + }); + }); + + it('should return 200 for admin configName updated', (done) => { + const partialBody = _.cloneDeep(body); + delete partialBody.param.orgId; + delete partialBody.param.configValue; + request(server) + .patch(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(partialBody) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.id.should.be.eql(id); + resJson.orgId.should.be.eql(config.orgId); + resJson.configName.should.be.eql(partialBody.param.configName); + resJson.configValue.should.be.eql(config.configValue); + resJson.createdBy.should.be.eql(config.createdBy); + resJson.createdBy.should.be.eql(config.createdBy); // should not update createdAt + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + + done(); + }); + }); + + it('should return 200 for admin all fields updated', (done) => { + request(server) + .patch(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.id.should.be.eql(id); + resJson.orgId.should.be.eql(body.param.orgId); + resJson.configName.should.be.eql(body.param.configName); + resJson.configValue.should.be.eql(body.param.configValue); + resJson.createdBy.should.be.eql(config.createdBy); // should not update createdAt + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + + done(); + }); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .patch(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send(body) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.id.should.be.eql(id); + resJson.orgId.should.be.eql(body.param.orgId); + resJson.configName.should.be.eql(body.param.configName); + resJson.configValue.should.be.eql(body.param.configValue); + resJson.createdBy.should.be.eql(config.createdBy); // should not update createdAt + resJson.updatedBy.should.be.eql(40051336); // connect admin + done(); + }); + }); + }); +});