diff --git a/.circleci/config.yml b/.circleci/config.yml index df0c2ae9..84f1cf7c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -48,6 +48,7 @@ jobs: - POSTGRES_USER: circle_test - POSTGRES_DB: circle_test - image: elasticsearch:2.3 + - image: rabbitmq:3-management environment: DB_MASTER_URL: postgres://circle_test:@127.0.0.1:5432/circle_test AUTH_SECRET: secret diff --git a/.eslintrc b/.eslintrc index b0115e3f..00dbe70b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,7 +8,7 @@ "mocha": true }, "rules": { - "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js", "**/*.spec.js", "**/serviceMocks.js"]}], + "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js", "**/*.spec.js", "src/tests/*.js"]}], "max-len": ["error", { "ignoreComments": true, "code": 120 }], "valid-jsdoc": ["error", { "requireReturn": true, diff --git a/package-lock.json b/package-lock.json index 24db92ed..8b18977b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3308,6 +3308,11 @@ "object-assign": "^4.0.1" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -5598,18 +5603,26 @@ } }, "libpq": { - "version": "1.8.8", - "resolved": "https://registry.npmjs.org/libpq/-/libpq-1.8.8.tgz", - "integrity": "sha512-0TVzqkbAZZiM8JJy5sagRyXOkvU9zTBlgGX6YdzuWECobc5F81Tp6uuS+djMZrnB5YN4O/ff52hsvXYBRW2gdQ==", + "version": "1.8.9", + "resolved": "https://registry.npmjs.org/libpq/-/libpq-1.8.9.tgz", + "integrity": "sha512-herU0STiW3+/XBoYRycKKf49O9hBKK0JbdC2QmvdC5pyCSu8prb9idpn5bUSbxj8XwcEsWPWWWwTDZE9ZTwJ7g==", "requires": { - "bindings": "1.2.1", - "nan": "^2.10.0" + "bindings": "1.5.0", + "nan": "^2.14.0" }, "dependencies": { + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "nan": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz", - "integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==" + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" } } }, diff --git a/src/events/index.js b/src/events/index.js index ce0921c1..1327a01e 100644 --- a/src/events/index.js +++ b/src/events/index.js @@ -44,7 +44,6 @@ export const rabbitHandlers = { [EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_ADDED]: phaseProductAddedHandler, [EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_REMOVED]: phaseProductRemovedHandler, [EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_UPDATED]: phaseProductUpdatedHandler, - // Timeline and milestone 'timeline.initial': timelineAddedHandler, [EVENT.ROUTING_KEY.TIMELINE_ADDED]: timelineAddedHandler, diff --git a/src/events/projectPhases/index.js b/src/events/projectPhases/index.js index d9e8c218..e8e33379 100644 --- a/src/events/projectPhases/index.js +++ b/src/events/projectPhases/index.js @@ -7,6 +7,8 @@ import config from 'config'; import _ from 'lodash'; import Promise from 'bluebird'; import util from '../../util'; +import { TIMELINE_REFERENCES } from '../../constants'; + import messageService from '../../services/messageService'; const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); @@ -14,6 +16,39 @@ const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); const eClient = util.getElasticSearchClient(); +/** + * Build topics data based on route parameter. + * + * @param {Object} logger logger to log along with trace id + * @param {Object} phase phase object + * @param {String} route route value can be PHASE/WORK + * @returns {undefined} + */ +const buildTopicsData = (logger, phase, route) => { + if (route === TIMELINE_REFERENCES.WORK) { + return [{ + tag: `work#${phase.id}-details`, + title: `${phase.name} - Details`, + reference: 'project', + referenceId: `${phase.projectId}`, + body: 'This is the beginning of your phase discussion. During execution of this phase, all related communication will be conducted here - phase updates, questions and answers, suggestions, etc. If you haven\'t already, do please take a moment to review the form in the Specification tab above and fill in as much detail as possible. This will help get started faster. Thanks!', // eslint-disable-line + }, { + tag: `work#${phase.id}-requirements`, + title: `${phase.name} - Requirements`, + reference: 'project', + referenceId: `${phase.projectId}`, + body: 'This is the beginning of your phase discussion. During execution of this phase, all related communication will be conducted here - phase updates, questions and answers, suggestions, etc. If you haven\'t already, do please take a moment to review the form in the Specification tab above and fill in as much detail as possible. This will help get started faster. Thanks!', // eslint-disable-line + }]; + } + return [{ + tag: `phase#${phase.id}`, + title: phase.name, + reference: 'project', + referenceId: `${phase.projectId}`, + body: 'This is the beginning of your phase discussion. During execution of this phase, all related communication will be conducted here - phase updates, questions and answers, suggestions, etc. If you haven\'t already, do please take a moment to review the form in the Specification tab above and fill in as much detail as possible. This will help get started faster. Thanks!', // eslint-disable-line + }]; +}; + /** * Indexes the project phase in the elastic search. * @@ -59,26 +94,22 @@ const indexProjectPhase = Promise.coroutine(function* (logger, phase) { // eslin }); /** - * Creates a new phase topic in message api. + * Creates topics in message api * * @param {Object} logger logger to log along with trace id - * @param {Object} msg event payload + * @param {Object} phase phase object + * @param {String} route route value can be `phase`/`work` * @returns {undefined} */ -const createPhaseTopic = Promise.coroutine(function* (logger, phase) { // eslint-disable-line func-names +const createTopics = Promise.coroutine(function* (logger, phase, route) { // eslint-disable-line func-names try { - logger.debug('Creating topic for phase with phase', phase); - const topic = yield messageService.createTopic({ - reference: 'project', - referenceId: `${phase.projectId}`, - tag: `phase#${phase.id}`, - title: phase.name, - body: 'This is the beginning of your phase discussion. During execution of this phase, all related communication will be conducted here - phase updates, questions and answers, suggestions, etc. If you haven\'t already, do please take a moment to review the form in the Specification tab above and fill in as much detail as possible. This will help get started faster. Thanks!', // eslint-disable-line - }, logger); - logger.debug('topic for the phase created successfully'); - logger.debug('created topic', topic); + logger.debug(`Creating topics for ${route} with phase`, phase); + const topicsData = buildTopicsData(logger, phase, route); + const topics = yield Promise.all(_.map(topicsData, topicData => messageService.createTopic(topicData, logger))); + logger.debug(`topics for the ${route} created successfully`); + logger.debug('created topics', topics); } catch (error) { - logger.error('Error in creating topic for the project phase', error); + logger.error(`Error in creating topic for ${route}`, error); // don't throw the error back to nack the bus, because we don't want to get multiple topics per phase // we can create topic for a phase manually, if somehow it fails } @@ -92,12 +123,14 @@ const createPhaseTopic = Promise.coroutine(function* (logger, phase) { // eslint * @returns {undefined} */ const projectPhaseAddedHandler = Promise.coroutine(function* (logger, msg, channel) { // eslint-disable-line func-names - const phase = JSON.parse(msg.content.toString()); + const data = JSON.parse(msg.content.toString()); + const phase = _.get(data, 'added', {}); + const route = _.get(data, 'route', 'PHASE'); try { logger.debug('calling indexProjectPhase', phase); yield indexProjectPhase(logger, phase, channel); logger.debug('calling createPhaseTopic', phase); - yield createPhaseTopic(logger, phase); + yield createTopics(logger, phase, route); channel.ack(msg); } catch (error) { logger.error('Error handling project.phase.added event', error); @@ -135,31 +168,46 @@ const updateIndexProjectPhase = Promise.coroutine(function* (logger, data) { // }); /** - * Creates a new phase topic in message api. + * Update one topic + * + * @param {Object} logger logger to log along with trace id + * @param {Object} phase phase object + * @param {Object} topicUpdate updated topic data + * @returns {undefined} + */ +const updateOneTopic = Promise.coroutine(function* (logger, phase, topicUpdate) { // eslint-disable-line func-names + const topic = yield messageService.getTopicByTag(phase.projectId, topicUpdate.tag, logger); + logger.trace('Topic', topic); + const title = topicUpdate.title; + const titleChanged = topic && topic.title !== title; + logger.trace('titleChanged', titleChanged); + const contentPost = topic && topic.posts && topic.posts.length > 0 ? topic.posts[0] : null; + logger.trace('contentPost', contentPost); + const postId = _.get(contentPost, 'id'); + const content = _.get(contentPost, 'body'); + if (postId && content && titleChanged) { + const updatedTopic = yield messageService.updateTopic(topic.id, { title, postId, content }, logger); + logger.debug('topic updated successfully'); + logger.trace('updated topic', updatedTopic); + } +}); + +/** + * Update topics in message api. * * @param {Object} logger logger to log along with trace id - * @param {Object} msg event payload + * @param {Object} phase phase object + * @param {String} route route value can be `phase`/`work` * @returns {undefined} */ -const updatePhaseTopic = Promise.coroutine(function* (logger, phase) { // eslint-disable-line func-names +const updateTopics = Promise.coroutine(function* (logger, phase, route) { // eslint-disable-line func-names try { - logger.debug('Updating topic for phase with phase', phase); - const topic = yield messageService.getPhaseTopic(phase.projectId, phase.id, logger); - logger.trace('Topic', topic); - const title = phase.name; - const titleChanged = topic && topic.title !== title; - logger.trace('titleChanged', titleChanged); - const contentPost = topic && topic.posts && topic.posts.length > 0 ? topic.posts[0] : null; - logger.trace('contentPost', contentPost); - const postId = _.get(contentPost, 'id'); - const content = _.get(contentPost, 'body'); - if (postId && content && titleChanged) { - const updatedTopic = yield messageService.updateTopic(topic.id, { title, postId, content }, logger); - logger.debug('topic for the phase updated successfully'); - logger.trace('updated topic', updatedTopic); - } + logger.debug(`Updating topic for ${route} with phase`, phase); + const topicsData = buildTopicsData(logger, phase, route); + yield Promise.all(_.map(topicsData, topicData => updateOneTopic(logger, phase, topicData))); + logger.debug(`topics for the ${route} updated successfully`); } catch (error) { - logger.error('Error in updating topic for the project phase', error); + logger.error(`Error in updating topic for ${route}`, error); // don't throw the error back to nack the bus, because we don't want to get multiple topics per phase // we can create topic for a phase manually, if somehow it fails } @@ -175,10 +223,11 @@ const updatePhaseTopic = Promise.coroutine(function* (logger, phase) { // eslint const projectPhaseUpdatedHandler = Promise.coroutine(function* (logger, msg, channel) { // eslint-disable-line func-names try { const data = JSON.parse(msg.content.toString()); + const route = _.get(data, 'route', 'PHASE'); logger.debug('calling updateIndexProjectPhase', data); yield updateIndexProjectPhase(logger, data, channel); - logger.debug('calling updatePhaseTopic', data.updated); - yield updatePhaseTopic(logger, data.updated); + logger.debug('calling updateTopics', data.updated); + yield updateTopics(logger, data.updated, route); channel.ack(msg); } catch (error) { logger.error('Error handling project.phase.updated event', error); @@ -197,13 +246,14 @@ const projectPhaseUpdatedHandler = Promise.coroutine(function* (logger, msg, cha const removePhaseFromIndex = Promise.coroutine(function* (logger, msg) { // eslint-disable-line func-names try { const data = JSON.parse(msg.content.toString()); - const doc = yield eClient.get({ index: ES_PROJECT_INDEX, type: ES_PROJECT_TYPE, id: data.projectId }); - const phases = _.filter(doc._source.phases, single => single.id !== data.id); // eslint-disable-line no-underscore-dangle + const phase = _.get(data, 'deleted', {}); + const doc = yield eClient.get({ index: ES_PROJECT_INDEX, type: ES_PROJECT_TYPE, id: phase.projectId }); + const phases = _.filter(doc._source.phases, single => single.id !== phase.id); // eslint-disable-line no-underscore-dangle const merged = _.assign(doc._source, { phases }); // eslint-disable-line no-underscore-dangle yield eClient.update({ index: ES_PROJECT_INDEX, type: ES_PROJECT_TYPE, - id: data.projectId, + id: phase.projectId, body: { doc: merged, }, @@ -217,26 +267,46 @@ const removePhaseFromIndex = Promise.coroutine(function* (logger, msg) { // esli }); /** - * Removes the phase topic from the message api. + * Removes one topic from the message api. * - * @param {Object} logger logger to log along with trace id - * @param {Object} msg event payload + * @param {Object} logger logger to log along with trace id + * @param {Object} phase phase object + * @param {Object} tag topic tag * @returns {undefined} */ -const removePhaseTopic = Promise.coroutine(function* (logger, msg) { // eslint-disable-line func-names +const removeOneTopic = Promise.coroutine(function* (logger, phase, tag) { // eslint-disable-line func-names try { - const phase = JSON.parse(msg.content.toString()); - const phaseTopic = yield messageService.getPhaseTopic(phase.projectId, phase.id, logger); + const phaseTopic = yield messageService.getTopicByTag(phase.projectId, tag, logger); yield messageService.deletePosts(phaseTopic.id, phaseTopic.postIds, logger); yield messageService.deleteTopic(phaseTopic.id, logger); - logger.debug('topic for the phase removed successfully'); } catch (error) { - logger.error('Error in removing topic for the project phase', error); + logger.error(`Error removing topic by tab ${tag}`, error); // don't throw the error back to nack the bus // we can delete topic for a phase manually, if somehow it fails } }); +/** + * Remove topics in message api. + * + * @param {Object} logger logger to log along with trace id + * @param {Object} phase phase object + * @param {String} route route value can be `phase`/`work` + * @returns {undefined} + */ +const removeTopics = Promise.coroutine(function* (logger, phase, route) { // eslint-disable-line func-names + try { + logger.debug(`Removing topic for ${route} with phase`, phase); + const topicsData = buildTopicsData(logger, phase, route); + yield Promise.all(_.map(topicsData, topicData => removeOneTopic(logger, phase, topicData.tag))); + logger.debug(`topics for the ${route} removed successfully`); + } catch (error) { + logger.error(`Error in removing topic for ${route}`, error); + // don't throw the error back to nack the bus, because we don't want to get multiple topics per phase + // we can create topic for a phase manually, if somehow it fails + } +}); + /** * Handler for project phase deleted event * @param {Object} logger logger to log along with trace id @@ -247,7 +317,11 @@ const removePhaseTopic = Promise.coroutine(function* (logger, msg) { // eslint-d const projectPhaseRemovedHandler = Promise.coroutine(function* (logger, msg, channel) { // eslint-disable-line func-names try { yield removePhaseFromIndex(logger, msg, channel); - yield removePhaseTopic(logger, msg); + const data = JSON.parse(msg.content.toString()); + const phase = _.get(data, 'deleted', {}); + const route = _.get(data, 'route'); + logger.debug('calling removeTopics'); + yield removeTopics(logger, phase, route); channel.ack(msg); } catch (error) { logger.error('Error fetching project document from elasticsearch', error); @@ -261,5 +335,5 @@ module.exports = { projectPhaseAddedHandler, projectPhaseRemovedHandler, projectPhaseUpdatedHandler, - createPhaseTopic, + createPhaseTopic: createTopics, }; diff --git a/src/routes/phases/create.js b/src/routes/phases/create.js index 23ea17eb..e4a9b250 100644 --- a/src/routes/phases/create.js +++ b/src/routes/phases/create.js @@ -5,7 +5,7 @@ import Sequelize from 'sequelize'; import models from '../../models'; import util from '../../util'; -import { EVENT } from '../../constants'; +import { EVENT, TIMELINE_REFERENCES } from '../../constants'; const permissions = require('tc-core-library-js').middleware.permissions; @@ -127,7 +127,7 @@ module.exports = [ // Send events to buses req.log.debug('Sending event to RabbitMQ bus for project phase %d', newProjectPhase.id); req.app.services.pubsub.publish(EVENT.ROUTING_KEY.PROJECT_PHASE_ADDED, - newProjectPhase, + { added: newProjectPhase, route: TIMELINE_REFERENCES.PHASE }, { correlationId: req.id }, ); req.log.debug('Sending event to Kafka bus for project phase %d', newProjectPhase.id); diff --git a/src/routes/phases/create.spec.js b/src/routes/phases/create.spec.js index f4b26e6b..9b587ed6 100644 --- a/src/routes/phases/create.spec.js +++ b/src/routes/phases/create.spec.js @@ -2,15 +2,22 @@ import _ from 'lodash'; import chai from 'chai'; import sinon from 'sinon'; +import config from 'config'; import request from 'supertest'; import server from '../../app'; import models from '../../models'; import testUtil from '../../tests/util'; import busApi from '../../services/busApi'; +import messageService from '../../services/messageService'; +import RabbitMQService from '../../services/rabbitmq'; +import mockRabbitMQ from '../../tests/mockRabbitMQ'; import { BUS_API_EVENT, } from '../../constants'; +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); + const should = chai.should(); const body = { @@ -57,22 +64,23 @@ describe('Project Phases', () => { lastName: 'lName', email: 'some@abc.com', }; + const project = { + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }; let productTemplateId; beforeEach((done) => { // mocks testUtil.clearDb() - .then(() => models.Project.create({ - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1, - lastActivityAt: 1, - lastActivityUserId: '1', - }).then((p) => { + .then(() => models.Project.create(project).then((p) => { projectId = p.id; projectName = p.name; // create members @@ -456,5 +464,95 @@ describe('Project Phases', () => { }); }); }); + + describe('RabbitMQ Message topic', () => { + let createMessageSpy; + let publishSpy; + let sandbox; + + before(async (done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(async (done) => { + sandbox = sinon.sandbox.create(); + server.services.pubsub = new RabbitMQService(server.logger); + + // initialize RabbitMQ + server.services.pubsub.init( + config.get('rabbitmqURL'), + config.get('pubsubExchangeName'), + config.get('pubsubQueueName'), + ); + + // add project to ES index + await server.services.es.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id: projectId, + body: { + doc: project, + }, + }); + + testUtil.wait(() => { + publishSpy = sandbox.spy(server.services.pubsub, 'publish'); + createMessageSpy = sandbox.spy(messageService, 'createTopic'); + done(); + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + after(() => { + mockRabbitMQ(server); + }); + + it('should send message topic when phase added', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: {}, + }, + }, + }), + }); + sandbox.stub(messageService, 'getClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${projectId}/phases/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ param: body }) + .expect('Content-Type', /json/) + .expect(201) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + publishSpy.calledOnce.should.be.true; + publishSpy.calledWith('project.phase.added').should.be.true; + createMessageSpy.calledOnce.should.be.true; + createMessageSpy.calledWith(sinon.match({ reference: 'project', + referenceId: '1', + tag: 'phase#1', + title: 'test project phase', + })).should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/phases/delete.js b/src/routes/phases/delete.js index afa440e0..bbe270eb 100644 --- a/src/routes/phases/delete.js +++ b/src/routes/phases/delete.js @@ -3,7 +3,7 @@ import _ from 'lodash'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; -import { EVENT } from '../../constants'; +import { EVENT, TIMELINE_REFERENCES } from '../../constants'; const permissions = tcMiddleware.permissions; @@ -40,7 +40,7 @@ module.exports = [ // Send events to buses req.app.services.pubsub.publish( EVENT.ROUTING_KEY.PROJECT_PHASE_REMOVED, - deleted, + { deleted, route: TIMELINE_REFERENCES.PHASE }, { correlationId: req.id }, ); req.app.emit(EVENT.ROUTING_KEY.PROJECT_PHASE_REMOVED, { req, deleted }); @@ -49,4 +49,3 @@ module.exports = [ }).catch(err => next(err)); }, ]; - diff --git a/src/routes/phases/delete.spec.js b/src/routes/phases/delete.spec.js index bea3d2be..b6a0f042 100644 --- a/src/routes/phases/delete.spec.js +++ b/src/routes/phases/delete.spec.js @@ -3,15 +3,21 @@ import _ from 'lodash'; import request from 'supertest'; import sinon from 'sinon'; import chai from 'chai'; +import config from 'config'; import server from '../../app'; import models from '../../models'; import testUtil from '../../tests/util'; import busApi from '../../services/busApi'; - +import messageService from '../../services/messageService'; +import RabbitMQService from '../../services/rabbitmq'; +import mockRabbitMQ from '../../tests/mockRabbitMQ'; import { BUS_API_EVENT, } from '../../constants'; +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); + const expectAfterDelete = (projectId, id, err, next) => { if (err) throw err; setTimeout(() => @@ -70,22 +76,32 @@ describe('Project Phases', () => { lastName: 'lName', email: 'some@abc.com', }; + const project = { + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }; + const topic = { + id: 1, + title: 'test project phase', + posts: + [{ id: 1, + type: 'post', + body: 'body', + }], + }; beforeEach((done) => { // mocks testUtil.clearDb() .then(() => { - models.Project.create({ - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1, - lastActivityAt: 1, - lastActivityUserId: '1', - }).then((p) => { + models.Project.create(project).then((p) => { projectId = p.id; projectName = p.name; // create members @@ -268,5 +284,82 @@ describe('Project Phases', () => { }); }); }); + + describe('RabbitMQ Message topic', () => { + let deleteTopicSpy; + let deletePostsSpy; + let publishSpy; + let sandbox; + + before(async (done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(async (done) => { + sandbox = sinon.sandbox.create(); + server.services.pubsub = new RabbitMQService(server.logger); + + // initialize RabbitMQ + server.services.pubsub.init( + config.get('rabbitmqURL'), + config.get('pubsubExchangeName'), + config.get('pubsubQueueName'), + ); + + // add project to ES index + await server.services.es.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id: projectId, + body: { + doc: _.assign(project, { phases: [_.assign(body, { id: phaseId, projectId })] }), + }, + }); + + testUtil.wait(() => { + publishSpy = sandbox.spy(server.services.pubsub, 'publish'); + deleteTopicSpy = sandbox.spy(messageService, 'deleteTopic'); + deletePostsSpy = sandbox.spy(messageService, 'deletePosts'); + sandbox.stub(messageService, 'getTopicByTag', () => Promise.resolve(topic)); + done(); + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + after(() => { + mockRabbitMQ(server); + }); + + it('should send message topic when phase deleted', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + delete: () => Promise.resolve(true), + }); + sandbox.stub(messageService, 'getClient', () => mockHttpClient); + request(server) + .delete(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + publishSpy.calledOnce.should.be.true; + publishSpy.firstCall.calledWith('project.phase.removed').should.be.true; + deleteTopicSpy.calledOnce.should.be.true; + deleteTopicSpy.calledWith(topic.id).should.be.true; + deletePostsSpy.calledWith(topic.id).should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/phases/update.js b/src/routes/phases/update.js index d5588fc3..9fcfcfba 100644 --- a/src/routes/phases/update.js +++ b/src/routes/phases/update.js @@ -6,7 +6,7 @@ import Sequelize from 'sequelize'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; -import { EVENT, ROUTES } from '../../constants'; +import { EVENT, ROUTES, TIMELINE_REFERENCES } from '../../constants'; const permissions = tcMiddleware.permissions; @@ -156,7 +156,7 @@ module.exports = [ // emit original and updated project phase information req.app.services.pubsub.publish( EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED, - { original: previousValue, updated, allPhases }, + { original: previousValue, updated, allPhases, route: TIMELINE_REFERENCES.PHASE }, { correlationId: req.id }, ); req.app.emit(EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED, { diff --git a/src/routes/phases/update.spec.js b/src/routes/phases/update.spec.js index fc7544fc..2db36e68 100644 --- a/src/routes/phases/update.spec.js +++ b/src/routes/phases/update.spec.js @@ -2,16 +2,22 @@ import _ from 'lodash'; import sinon from 'sinon'; import chai from 'chai'; +import config from 'config'; import request from 'supertest'; import server from '../../app'; import models from '../../models'; import testUtil from '../../tests/util'; import busApi from '../../services/busApi'; - +import messageService from '../../services/messageService'; +import RabbitMQService from '../../services/rabbitmq'; +import mockRabbitMQ from '../../tests/mockRabbitMQ'; import { BUS_API_EVENT, } from '../../constants'; +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); + const should = chai.should(); const body = { @@ -75,22 +81,32 @@ describe('Project Phases', () => { lastName: 'lName', email: 'some@abc.com', }; + const project = { + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }; + const topic = { + id: 1, + title: 'test project phase', + posts: + [{ id: 1, + type: 'post', + body: 'body', + }], + }; beforeEach((done) => { // mocks testUtil.clearDb() .then(() => { - models.Project.create({ - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1, - lastActivityAt: 1, - lastActivityUserId: '1', - }).then((p) => { + models.Project.create(project).then((p) => { projectId = p.id; projectName = p.name; // create members @@ -635,5 +651,95 @@ describe('Project Phases', () => { }); }); }); + + describe('RabbitMQ Message topic', () => { + let updateMessageSpy; + let publishSpy; + let sandbox; + + before(async (done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(async (done) => { + sandbox = sinon.sandbox.create(); + server.services.pubsub = new RabbitMQService(server.logger); + + // initialize RabbitMQ + server.services.pubsub.init( + config.get('rabbitmqURL'), + config.get('pubsubExchangeName'), + config.get('pubsubQueueName'), + ); + + // add project to ES index + await server.services.es.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id: projectId, + body: { + doc: _.assign(project, { phases: [_.assign(body, { id: phaseId, projectId })] }), + }, + }); + + testUtil.wait(() => { + publishSpy = sandbox.spy(server.services.pubsub, 'publish'); + updateMessageSpy = sandbox.spy(messageService, 'updateTopic'); + sandbox.stub(messageService, 'getTopicByTag', () => Promise.resolve(topic)); + done(); + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + after(() => { + mockRabbitMQ(server); + }); + + it('should send message topic when phase Updated', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: {}, + }, + }, + }), + }); + sandbox.stub(messageService, 'getClient', () => mockHttpClient); + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ param: _.assign(updateBody, { budget: 123 }) }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + publishSpy.calledOnce.should.be.true; + publishSpy.calledWith('project.phase.updated').should.be.true; + updateMessageSpy.calledOnce.should.be.true; + updateMessageSpy.calledWith(topic.id, sinon.match({ + title: updateBody.name, + postId: topic.posts[0].id, + content: topic.posts[0].body })).should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/works/create.js b/src/routes/works/create.js index 44be86a0..fc0184a5 100644 --- a/src/routes/works/create.js +++ b/src/routes/works/create.js @@ -8,7 +8,7 @@ import Sequelize from 'sequelize'; import models from '../../models'; import util from '../../util'; -import { EVENT } from '../../constants'; +import { EVENT, TIMELINE_REFERENCES } from '../../constants'; const permissions = require('tc-core-library-js').middleware.permissions; @@ -138,7 +138,7 @@ module.exports = [ // Send events to buses req.log.debug('Sending event to RabbitMQ bus for project phase %d', newProjectPhase.id); req.app.services.pubsub.publish(EVENT.ROUTING_KEY.PROJECT_PHASE_ADDED, - newProjectPhase, + { added: newProjectPhase, route: TIMELINE_REFERENCES.WORK }, { correlationId: req.id }, ); req.log.debug('Sending event to Kafka bus for project phase %d', newProjectPhase.id); diff --git a/src/routes/works/create.spec.js b/src/routes/works/create.spec.js index 259dde28..e2061431 100644 --- a/src/routes/works/create.spec.js +++ b/src/routes/works/create.spec.js @@ -7,13 +7,19 @@ import _ from 'lodash'; import chai from 'chai'; import sinon from 'sinon'; import request from 'supertest'; - +import config from 'config'; import models from '../../models'; import server from '../../app'; import testUtil from '../../tests/util'; import busApi from '../../services/busApi'; +import messageService from '../../services/messageService'; +import RabbitMQService from '../../services/rabbitmq'; +import mockRabbitMQ from '../../tests/mockRabbitMQ'; import { BUS_API_EVENT } from '../../constants'; +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); + const should = chai.should(); const validatePhase = (resJson, expectedPhase) => { @@ -46,6 +52,18 @@ describe('CREATE work', () => { lastName: 'lName', email: 'some@abc.com', }; + const project = { + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }; beforeEach((done) => { testUtil.clearDb() @@ -82,22 +100,10 @@ describe('CREATE work', () => { }) .then(() => { // Create projects - models.Project.create({ - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - templateId: template.id, - details: {}, - createdBy: 1, - updatedBy: 1, - lastActivityAt: 1, - lastActivityUserId: '1', - }) - .then((project) => { - projectId = project.id; - projectName = project.name; + models.Project.create(_.assign(project, { templateId: template.id })) + .then((_project) => { + projectId = _project.id; + projectName = _project.name; models.WorkStream.create({ name: 'Work Stream', type: 'generic', @@ -328,5 +334,89 @@ describe('CREATE work', () => { }); }); }); + + describe('RabbitMQ Message topic', () => { + let createMessageSpy; + let publishSpy; + let sandbox; + + before(async (done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(async (done) => { + sandbox = sinon.sandbox.create(); + server.services.pubsub = new RabbitMQService(server.logger); + + // initialize RabbitMQ + server.services.pubsub.init( + config.get('rabbitmqURL'), + config.get('pubsubExchangeName'), + config.get('pubsubQueueName'), + ); + + // add project to ES index + await server.services.es.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id: projectId, + body: { + doc: project, + }, + }); + + testUtil.wait(() => { + publishSpy = sandbox.spy(server.services.pubsub, 'publish'); + createMessageSpy = sandbox.spy(messageService, 'createTopic'); + done(); + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + after(() => { + mockRabbitMQ(server); + }); + + it('should send message topic when work added', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: {}, + }, + }, + }), + }); + sandbox.stub(messageService, 'getClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${projectId}/workstreams/${workStreamId}/works`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send(body) + .expect(201) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + publishSpy.calledOnce.should.be.true; + publishSpy.calledWith('project.phase.added').should.be.true; + createMessageSpy.calledTwice.should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/works/delete.js b/src/routes/works/delete.js index 3aa82f73..6102db20 100644 --- a/src/routes/works/delete.js +++ b/src/routes/works/delete.js @@ -5,7 +5,7 @@ import validate from 'express-validation'; import Joi from 'joi'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; -import { EVENT } from '../../constants'; +import { EVENT, TIMELINE_REFERENCES } from '../../constants'; const permissions = tcMiddleware.permissions; @@ -62,7 +62,7 @@ module.exports = [ // Send events to buses req.app.services.pubsub.publish( EVENT.ROUTING_KEY.PROJECT_PHASE_REMOVED, - deleted, + { deleted, route: TIMELINE_REFERENCES.WORK }, { correlationId: req.id }, ); req.app.emit(EVENT.ROUTING_KEY.PROJECT_PHASE_REMOVED, { req, deleted }); diff --git a/src/routes/works/delete.spec.js b/src/routes/works/delete.spec.js index 3eb55dfb..cffe9034 100644 --- a/src/routes/works/delete.spec.js +++ b/src/routes/works/delete.spec.js @@ -2,16 +2,23 @@ /** * Tests for delete.js */ +import _ from 'lodash'; import request from 'supertest'; import chai from 'chai'; import sinon from 'sinon'; - +import config from 'config'; import models from '../../models'; import server from '../../app'; import testUtil from '../../tests/util'; import busApi from '../../services/busApi'; +import messageService from '../../services/messageService'; +import RabbitMQService from '../../services/rabbitmq'; +import mockRabbitMQ from '../../tests/mockRabbitMQ'; import { BUS_API_EVENT } from '../../constants'; +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); + chai.should(); const expectAfterDelete = (workId, projectId, workStreamId, err, next) => { @@ -60,7 +67,27 @@ describe('DELETE work', () => { lastName: 'lName', email: 'some@abc.com', }; - + const project = { + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }; + const topic = { + id: 1, + title: 'test project phase', + posts: + [{ id: 1, + type: 'post', + body: 'body', + }], + }; beforeEach((done) => { testUtil.clearDb() .then(() => { @@ -96,22 +123,10 @@ describe('DELETE work', () => { }) .then(() => { // Create projects - models.Project.create({ - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - templateId: template.id, - details: {}, - createdBy: 1, - updatedBy: 1, - lastActivityAt: 1, - lastActivityUserId: '1', - }) - .then((project) => { - projectId = project.id; - projectName = project.name; + models.Project.create(_.assign(project, { templateId: template.id })) + .then((_project) => { + projectId = _project.id; + projectName = _project.name; models.WorkStream.create({ name: 'Work Stream', type: 'generic', @@ -290,5 +305,94 @@ describe('DELETE work', () => { }); }); }); + + describe('RabbitMQ Message topic', () => { + let deleteTopicSpy; + let deletePostsSpy; + let publishSpy; + let sandbox; + + before(async (done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(async (done) => { + sandbox = sinon.sandbox.create(); + server.services.pubsub = new RabbitMQService(server.logger); + + // initialize RabbitMQ + server.services.pubsub.init( + config.get('rabbitmqURL'), + config.get('pubsubExchangeName'), + config.get('pubsubQueueName'), + ); + + // add project to ES index + await server.services.es.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id: projectId, + body: { + doc: _.assign(project, { phases: [_.assign({ + name: 'test project phase', + status: 'active', + startDate: '2018-05-15T00:00:00Z', + endDate: '2018-05-15T12:00:00Z', + budget: 20.0, + progress: 1.23456, + details: { + message: 'This can be any json', + }, + createdBy: 1, + updatedBy: 1, + projectId, + }, { id: workId, projectId })] }), + }, + }); + + testUtil.wait(() => { + publishSpy = sandbox.spy(server.services.pubsub, 'publish'); + deleteTopicSpy = sandbox.spy(messageService, 'deleteTopic'); + deletePostsSpy = sandbox.spy(messageService, 'deletePosts'); + sandbox.stub(messageService, 'getTopicByTag', () => Promise.resolve(topic)); + done(); + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + after(() => { + mockRabbitMQ(server); + }); + + it('should send message topic when work deleted', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + delete: () => Promise.resolve(true), + }); + sandbox.stub(messageService, 'getClient', () => mockHttpClient); + request(server) + .delete(`/v4/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + publishSpy.calledOnce.should.be.true; + publishSpy.calledWith('project.phase.removed').should.be.true; + deleteTopicSpy.calledTwice.should.be.true; + deletePostsSpy.calledTwice.should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/works/update.js b/src/routes/works/update.js index d828fdb6..82aaa08c 100644 --- a/src/routes/works/update.js +++ b/src/routes/works/update.js @@ -8,7 +8,7 @@ import Sequelize from 'sequelize'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; -import { EVENT, ROUTES } from '../../constants'; +import { EVENT, ROUTES, TIMELINE_REFERENCES } from '../../constants'; const permissions = tcMiddleware.permissions; @@ -175,7 +175,7 @@ module.exports = [ // emit original and updated project phase information req.app.services.pubsub.publish( EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED, - { original: previousValue, updated, allPhases }, + { original: previousValue, updated, allPhases, route: TIMELINE_REFERENCES.WORK }, { correlationId: req.id }, ); req.app.emit(EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED, { diff --git a/src/routes/works/update.spec.js b/src/routes/works/update.spec.js index 56dbe231..5fd1014a 100644 --- a/src/routes/works/update.spec.js +++ b/src/routes/works/update.spec.js @@ -6,13 +6,19 @@ import _ from 'lodash'; import chai from 'chai'; import request from 'supertest'; import sinon from 'sinon'; - +import config from 'config'; import models from '../../models'; import server from '../../app'; import testUtil from '../../tests/util'; import busApi from '../../services/busApi'; +import messageService from '../../services/messageService'; +import RabbitMQService from '../../services/rabbitmq'; +import mockRabbitMQ from '../../tests/mockRabbitMQ'; import { BUS_API_EVENT } from '../../constants'; +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); + const should = chai.should(); const body = { @@ -78,7 +84,27 @@ describe('UPDATE work', () => { lastName: 'lName', email: 'some@abc.com', }; - + const project = { + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }; + const topic = { + id: 1, + title: 'test project phase', + posts: + [{ id: 1, + type: 'post', + body: 'body', + }], + }; beforeEach((done) => { testUtil.clearDb() .then(() => { @@ -114,22 +140,10 @@ describe('UPDATE work', () => { }) .then(() => { // Create projects - models.Project.create({ - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - templateId: template.id, - details: {}, - createdBy: 1, - updatedBy: 1, - lastActivityAt: 1, - lastActivityUserId: '1', - }) - .then((project) => { - projectId = project.id; - projectName = project.name; + models.Project.create(_.assign(project, { templateId: template.id })) + .then((_project) => { + projectId = _project.id; + projectName = _project.name; models.WorkStream.create({ name: 'Work Stream', type: 'generic', @@ -632,5 +646,91 @@ describe('UPDATE work', () => { }); }); }); + + describe('RabbitMQ Message topic', () => { + let updateMessageSpy; + let publishSpy; + let sandbox; + + before(async (done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(async (done) => { + sandbox = sinon.sandbox.create(); + server.services.pubsub = new RabbitMQService(server.logger); + + // initialize RabbitMQ + server.services.pubsub.init( + config.get('rabbitmqURL'), + config.get('pubsubExchangeName'), + config.get('pubsubQueueName'), + ); + + // add project to ES index + await server.services.es.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id: projectId, + body: { + doc: _.assign(project, { phases: [_.assign(body, { id: workId, projectId })] }), + }, + }); + + testUtil.wait(() => { + publishSpy = sandbox.spy(server.services.pubsub, 'publish'); + updateMessageSpy = sandbox.spy(messageService, 'updateTopic'); + sandbox.stub(messageService, 'getTopicByTag', () => Promise.resolve(topic)); + done(); + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + after(() => { + mockRabbitMQ(server); + }); + + it('should send message topic when work updated', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: {}, + }, + }, + }), + }); + sandbox.stub(messageService, 'getClient', () => mockHttpClient); + request(server) + .patch(`/v4/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ param: _.assign(updateBody, { budget: 123 }) }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + publishSpy.calledOnce.should.be.true; + publishSpy.calledWith('project.phase.updated').should.be.true; + updateMessageSpy.calledTwice.should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/services/messageService.js b/src/services/messageService.js index 949c0134..c596dddf 100644 --- a/src/services/messageService.js +++ b/src/services/messageService.js @@ -1,6 +1,5 @@ import config from 'config'; import _ from 'lodash'; -// import util from '../util'; const Promise = require('bluebird'); const axios = require('axios'); @@ -141,19 +140,19 @@ function deletePosts(topicId, postIds, logger) { * Fetches the topic of given phase of the project. * * @param {Integer} projectId id of the project - * @param {Integer} phaseId id of the phase of the project + * @param {String} tag tag * @param {Object} logger object * @return {Promise} topic promise */ -function getPhaseTopic(projectId, phaseId, logger) { - logger.debug(`getPhaseTopic for projectId: ${projectId} phaseId: ${phaseId}`); +function getTopicByTag(projectId, tag, logger) { + logger.debug(`getTopicByTag for projectId: ${projectId} tag: ${tag}`); return getClient(logger).then((msgClient) => { - logger.debug(`calling message service for fetching phaseId#${phaseId}`); - const encodedFilter = encodeURIComponent(`reference=project&referenceId=${projectId}&tag=phase#${phaseId}`); + logger.debug(`calling message service for fetching ${tag}`); + const encodedFilter = encodeURIComponent(`reference=project&referenceId=${projectId}&tag=${tag}`); return msgClient.get(`/topics/list/db?filter=${encodedFilter}`) .then((resp) => { - logger.debug('Fetched phase topic', resp); const topics = _.get(resp.data, 'result.content', []); + logger.debug(`Fetched ${topics.length} topics`); if (topics && topics.length > 0) { return topics[0]; } @@ -181,6 +180,7 @@ module.exports = { createTopic, updateTopic, deletePosts, - getPhaseTopic, + getTopicByTag, deleteTopic, + getClient, }; diff --git a/src/tests/mockRabbitMQ.js b/src/tests/mockRabbitMQ.js new file mode 100644 index 00000000..3913bd73 --- /dev/null +++ b/src/tests/mockRabbitMQ.js @@ -0,0 +1,18 @@ +/** + * Mock RabbitMQ service + */ +/* globals Promise */ + +import sinon from 'sinon'; +import _ from 'lodash'; + +module.exports = (app) => { + _.assign(app.services, { + pubsub: { + init: () => {}, + publish: () => {}, + }, + }); + sinon.stub(app.services.pubsub, 'init', () => Promise.resolve(true)); + sinon.stub(app.services.pubsub, 'publish', () => Promise.resolve(true)); +}; diff --git a/src/tests/serviceMocks.js b/src/tests/serviceMocks.js index 662bd2c4..060596df 100644 --- a/src/tests/serviceMocks.js +++ b/src/tests/serviceMocks.js @@ -7,16 +7,13 @@ import _ from 'lodash'; import config from 'config'; import elasticsearch from 'elasticsearch'; import util from '../util'; +import mockRabbitMQ from './mockRabbitMQ'; module.exports = (app) => { + mockRabbitMQ(app); + _.assign(app.services, { - pubsub: { - init: () => {}, - publish: () => {}, - }, es: new elasticsearch.Client(_.cloneDeep(config.elasticsearchConfig)), }); - sinon.stub(app.services.pubsub, 'init', () => Promise.resolve(true)); - sinon.stub(app.services.pubsub, 'publish', () => Promise.resolve(true)); sinon.stub(util, 'getM2MToken', () => Promise.resolve('MOCK_TOKEN')); };