diff --git a/.circleci/config.yml b/.circleci/config.yml index fec74ad4..459ec723 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -76,7 +76,7 @@ workflows: - test filters: branches: - only: ['dev', 'feature/timeline-milestone'] + only: ['dev'] - deployProd: requires: - test diff --git a/src/constants.js b/src/constants.js index c41703e8..b2dca636 100644 --- a/src/constants.js +++ b/src/constants.js @@ -101,11 +101,14 @@ export const BUS_API_EVENT = { // When milestone is added/deleted to/from the phase, // When milestone is updated for duration/startDate/endDate/status - TIMELINE_MODIFIED: 'notifications.connect.project.phase.timelineModified', + TIMELINE_ADJUSTED: 'notifications.connect.project.phase.timeline.adjusted', // When specification of a product is modified PROJECT_PRODUCT_SPECIFICATION_MODIFIED: 'notifications.connect.project.productSpecificationModified', + MILESTONE_ADDED: 'notifications.connect.project.phase.milestone.added', + MILESTONE_REMOVED: 'notifications.connect.project.phase.milestone.removed', + MILESTONE_UPDATED: 'notifications.connect.project.phase.milestone.updated', // When milestone is marked as active MILESTONE_TRANSITION_ACTIVE: 'notifications.connect.project.phase.milestone.transition.active', // When milestone is marked as completed diff --git a/src/events/busApi.js b/src/events/busApi.js index f35c4700..50ff0f61 100644 --- a/src/events/busApi.js +++ b/src/events/busApi.js @@ -474,10 +474,22 @@ module.exports = (app, logger) => { * @param {Object} original the original milestone * @param {Object} updated the updated milestone * @param {Object} project the project + * @param {Object} timeline the updated timeline * @returns {Promise} void */ - function sendMilestoneNotification(req, original, updated, project) { + function sendMilestoneNotification(req, original, updated, project, timeline) { logger.debug('sendMilestoneNotification', original, updated); + // throw generic milestone updated bus api event + createEvent(BUS_API_EVENT.MILESTONE_UPDATED, { + projectId: project.id, + projectName: project.name, + projectUrl: connectProjectUrl(project.id), + timeline, + originalMilestone: original, + updatedMilestone: updated, + userId: req.authUser.userId, + initiatorUserId: req.authUser.userId, + }, logger); // Send transition events if (original.status !== updated.status) { let event; @@ -492,8 +504,7 @@ module.exports = (app, logger) => { projectId: project.id, projectName: project.name, projectUrl: connectProjectUrl(project.id), - timelineId: req.timeline.id, - timelineName: req.timeline.name, + timeline, originalMilestone: original, updatedMilestone: updated, userId: req.authUser.userId, @@ -510,8 +521,7 @@ module.exports = (app, logger) => { projectId: project.id, projectName: project.name, projectUrl: connectProjectUrl(project.id), - timelineId: req.timeline.id, - timelineName: req.timeline.name, + timeline, originalMilestone: original, updatedMilestone: updated, userId: req.authUser.userId, @@ -533,15 +543,16 @@ module.exports = (app, logger) => { }) .then((project) => { if (project) { - createEvent(BUS_API_EVENT.PROJECT_PLAN_UPDATED, { + createEvent(BUS_API_EVENT.MILESTONE_ADDED, { projectId, projectName: project.name, projectUrl: connectProjectUrl(projectId), + addedMilestone: created, userId: req.authUser.userId, initiatorUserId: req.authUser.userId, }, logger); } - sendMilestoneNotification(req, {}, created, project); + // sendMilestoneNotification(req, {}, created, project); }) .catch(err => null); // eslint-disable-line no-unused-vars }); @@ -551,60 +562,54 @@ module.exports = (app, logger) => { */ // eslint-disable-next-line no-unused-vars app.on(EVENT.ROUTING_KEY.MILESTONE_UPDATED, ({ req, original, updated, cascadedUpdates }) => { - logger.debug('receive MILESTONE_UPDATED event'); + logger.debug(`receive MILESTONE_UPDATED event for milestone ${original.id}`); const projectId = _.parseInt(req.params.projectId); + const timeline = _.omit(req.timeline.toJSON(), 'deletedAt', 'deletedBy'); models.Project.findOne({ where: { id: projectId }, }) - .then((project) => { - // send PROJECT_UPDATED Kafka message when one of the specified below properties changed - const watchProperties = ['startDate', 'endDate', 'duration', 'details', 'status', 'order']; - if (!_.isEqual(_.pick(original, watchProperties), - _.pick(updated, watchProperties))) { - createEvent(BUS_API_EVENT.PROJECT_PLAN_UPDATED, { - projectId, - projectName: project.name, - projectUrl: connectProjectUrl(projectId), - userId: req.authUser.userId, - initiatorUserId: req.authUser.userId, - }, logger); - } - sendMilestoneNotification(req, original, updated, project); + .then((project) => { + logger.debug(`Found project with id ${projectId}`); + return models.Milestone.getTimelineDuration(timeline.id) + .then(({ duration, progress }) => { + timeline.duration = duration; + timeline.progress = progress; + sendMilestoneNotification(req, original, updated, project, timeline); logger.debug('cascadedUpdates', cascadedUpdates); if (cascadedUpdates && cascadedUpdates.milestones && cascadedUpdates.milestones.length > 0) { _.each(cascadedUpdates.milestones, cascadedUpdate => - sendMilestoneNotification(req, cascadedUpdate.original, cascadedUpdate.updated, project), + sendMilestoneNotification(req, cascadedUpdate.original, cascadedUpdate.updated, project, timeline), ); } // if timeline is modified if (cascadedUpdates && cascadedUpdates.timeline) { - const timeline = cascadedUpdates.timeline; - // if endDate of the timeline is modified, raise TIMELINE_MODIFIED event - if (timeline.original.endDate !== timeline.updated.endDate) { + const cTimeline = cascadedUpdates.timeline; + // if endDate of the timeline is modified, raise TIMELINE_ADJUSTED event + if (cTimeline.original.endDate !== cTimeline.updated.endDate) { // Raise Timeline changed event - createEvent(BUS_API_EVENT.TIMELINE_MODIFIED, { + createEvent(BUS_API_EVENT.TIMELINE_ADJUSTED, { projectId: project.id, projectName: project.name, projectUrl: connectProjectUrl(project.id), - original: timeline.original, - updated: timeline.updated, + originalTimeline: cTimeline.original, + updatedTimeline: cTimeline.updated, userId: req.authUser.userId, initiatorUserId: req.authUser.userId, }, logger); } } - }) - .catch(err => null); // eslint-disable-line no-unused-vars + }); + }).catch(err => null); // eslint-disable-line no-unused-vars }); /** * MILESTONE_REMOVED. */ - app.on(EVENT.ROUTING_KEY.MILESTONE_REMOVED, ({ req }) => { + app.on(EVENT.ROUTING_KEY.MILESTONE_REMOVED, ({ req, deleted }) => { logger.debug('receive MILESTONE_REMOVED event'); // req.params.projectId is set by validateTimelineIdParam middleware const projectId = _.parseInt(req.params.projectId); @@ -614,10 +619,11 @@ module.exports = (app, logger) => { }) .then((project) => { if (project) { - createEvent(BUS_API_EVENT.PROJECT_PLAN_UPDATED, { + createEvent(BUS_API_EVENT.MILESTONE_REMOVED, { projectId, projectName: project.name, projectUrl: connectProjectUrl(projectId), + removedMilestone: deleted, userId: req.authUser.userId, initiatorUserId: req.authUser.userId, }, logger); @@ -639,10 +645,12 @@ module.exports = (app, logger) => { }) .then((project) => { if (project) { - createEvent(BUS_API_EVENT.PROJECT_PLAN_UPDATED, { + createEvent(BUS_API_EVENT.TIMELINE_ADJUSTED, { projectId, projectName: project.name, projectUrl: connectProjectUrl(projectId), + originalTimeline: original, + updatedTimeline: updated, userId: req.authUser.userId, initiatorUserId: req.authUser.userId, }, logger); diff --git a/src/events/index.js b/src/events/index.js index 38a1ed00..23a3f037 100644 --- a/src/events/index.js +++ b/src/events/index.js @@ -10,8 +10,18 @@ import { projectPhaseAddedHandler, projectPhaseRemovedHandler, projectPhaseUpdatedHandler } from './projectPhases'; import { phaseProductAddedHandler, phaseProductRemovedHandler, phaseProductUpdatedHandler } from './phaseProducts'; -import { timelineAddedHandler, timelineUpdatedHandler, timelineRemovedHandler } from './timelines'; -import { milestoneAddedHandler, milestoneUpdatedHandler, milestoneRemovedHandler } from './milestones'; +import { + timelineAddedHandler, + timelineUpdatedHandler, + timelineRemovedHandler, + timelineAdjustedKafkaHandler, +} from './timelines'; +import { + milestoneAddedHandler, + milestoneUpdatedHandler, + milestoneRemovedHandler, + milestoneUpdatedKafkaHandler, +} from './milestones'; export const rabbitHandlers = { 'project.initial': projectCreatedHandler, @@ -56,4 +66,8 @@ export const kafkaHandlers = { [BUS_API_EVENT.TOPIC_UPDATED]: projectUpdatedKafkaHandler, [BUS_API_EVENT.POST_CREATED]: projectUpdatedKafkaHandler, [BUS_API_EVENT.POST_UPDATED]: projectUpdatedKafkaHandler, + + // Events coming from timeline/milestones (considering it as a separate module/service in future) + [BUS_API_EVENT.MILESTONE_TRANSITION_COMPLETED]: milestoneUpdatedKafkaHandler, + [BUS_API_EVENT.TIMELINE_ADJUSTED]: timelineAdjustedKafkaHandler, }; diff --git a/src/events/milestones/index.js b/src/events/milestones/index.js index 71fd0d6b..d8f884a9 100644 --- a/src/events/milestones/index.js +++ b/src/events/milestones/index.js @@ -3,8 +3,12 @@ */ import config from 'config'; import _ from 'lodash'; +import Joi from 'joi'; import Promise from 'bluebird'; import util from '../../util'; +// import { createEvent } from '../../services/busApi'; +import { EVENT, TIMELINE_REFERENCES, MILESTONE_STATUS, REGEX } from '../../constants'; +import models from '../../models'; const ES_TIMELINE_INDEX = config.get('elasticsearchConfig.timelineIndexName'); const ES_TIMELINE_TYPE = config.get('elasticsearchConfig.timelineDocType'); @@ -154,9 +158,105 @@ const milestoneRemovedHandler = Promise.coroutine(function* (logger, msg, channe } }); +/** + * Kafka event handlers + */ + +const payloadSchema = Joi.object().keys({ + projectId: Joi.number().integer().positive().required(), + projectName: Joi.string().optional(), + projectUrl: Joi.string().regex(REGEX.URL).optional(), + userId: Joi.number().integer().positive().required(), + initiatorUserId: Joi.number().integer().positive().required(), +}).unknown(true).required(); + +const findProjectPhaseProduct = function (logger, productId, raw = true) { // eslint-disable-line func-names + let product; + return models.PhaseProduct.findOne({ + where: { id: productId }, + raw, + }).then((_product) => { + logger.debug('_product', _product); + if (_product) { + product = _product; + const phaseId = product.phaseId; + const projectId = product.projectId; + return Promise.all([ + models.ProjectPhase.findOne({ + where: { id: phaseId, projectId }, + raw, + }), + models.Project.findOne({ + where: { id: projectId }, + raw, + }), + ]); + } + return Promise.reject('Unable to find product'); + }).then((projectAndPhase) => { + logger.debug('projectAndPhase', projectAndPhase); + if (projectAndPhase) { + const phase = projectAndPhase[0]; + const project = projectAndPhase[1]; + return Promise.resolve({ product, phase, project }); + } + return Promise.reject('Unable to find phase/project'); + }); +}; + +/** + * Raises the project plan modified event + * @param {Object} app Application object used to interact with RMQ service + * @param {String} topic Kafka topic + * @param {Object} payload Message payload + * @return {Promise} Promise + */ +async function milestoneUpdatedKafkaHandler(app, topic, payload) { + app.logger.info(`Handling Kafka event for ${topic}`); + // Validate payload + const result = Joi.validate(payload, payloadSchema); + if (result.error) { + throw new Error(result.error); + } + + const timeline = payload.timeline; + // process only if timeline is related to a product reference + if (timeline && timeline.reference === TIMELINE_REFERENCES.PRODUCT) { + const productId = timeline.referenceId; + const original = payload.originalMilestone; + const updated = payload.updatedMilestone; + app.logger.debug('Calling findProjectPhaseProduct'); + const { project, phase } = await findProjectPhaseProduct(app.logger, productId, false); + app.logger.debug('Successfully fetched project, phase and product'); + if (original.status !== updated.status) { + if (updated.status === MILESTONE_STATUS.COMPLETED) { + app.logger.debug('Found milestone status to be completed'); + app.logger.debug(`Duration: ${timeline.duration}`); + if (!isNaN(timeline.duration) && !isNaN(timeline.progress)) { + app.logger.debug(`Current phase progress ${phase.progress} and duration ${phase.duration}`); + const updatedPhase = await phase.update({ + progress: timeline.progress, + duration: timeline.duration, + }, ['progress', 'duration']); + app.logger.debug(`Updated phase progress ${timeline.progress} and duration ${timeline.duration}`); + app.logger.debug('Raising node event for PROJECT_PHASE_UPDATED'); + app.emit(EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED, { + req: { + params: { projectId: project.id, phaseId: phase.id }, + authUser: { userId: payload.userId }, + }, + original: phase, + updated: _.omit(updatedPhase.toJSON(), 'deletedAt', 'deletedBy'), + }); + } + } + } + } +} module.exports = { milestoneAddedHandler, milestoneRemovedHandler, milestoneUpdatedHandler, + milestoneUpdatedKafkaHandler, }; diff --git a/src/events/timelines/index.js b/src/events/timelines/index.js index 0de36410..39ed0636 100644 --- a/src/events/timelines/index.js +++ b/src/events/timelines/index.js @@ -2,14 +2,29 @@ * Event handlers for timeline create, update and delete */ import _ from 'lodash'; +import Joi from 'joi'; import Promise from 'bluebird'; import config from 'config'; import util from '../../util'; +import { BUS_API_EVENT, TIMELINE_REFERENCES, REGEX } from '../../constants'; +import models from '../../models'; +import { createEvent } from '../../services/busApi'; const ES_TIMELINE_INDEX = config.get('elasticsearchConfig.timelineIndexName'); const ES_TIMELINE_TYPE = config.get('elasticsearchConfig.timelineDocType'); const eClient = util.getElasticSearchClient(); + +/** + * Builds the connect project url for the given project id. + * + * @param {string|number} projectId the project id + * @returns {string} the connect project url + */ +function connectProjectUrl(projectId) { + return `${config.get('connectProjectsUrl')}${projectId}`; +} + /** * Handler for timeline creation event * @param {Object} logger logger to log along with trace id @@ -81,10 +96,89 @@ const timelineRemovedHandler = Promise.coroutine(function* (logger, msg, channel channel.nack(msg, false, !msg.fields.redelivered); } }); +/** + * Kafka event handlers + */ + +const payloadSchema = Joi.object().keys({ + projectId: Joi.number().integer().positive().required(), + projectName: Joi.string().optional(), + projectUrl: Joi.string().regex(REGEX.URL).optional(), + userId: Joi.number().integer().positive().required(), + initiatorUserId: Joi.number().integer().positive().required(), +}).unknown(true).required(); +const findProjectPhaseProduct = function (logger, productId) { // eslint-disable-line func-names + let product; + return models.PhaseProduct.findOne({ + where: { id: productId }, + raw: true, + }).then((_product) => { + logger.debug('_product', _product); + if (_product) { + product = _product; + const phaseId = product.phaseId; + const projectId = product.projectId; + return Promise.all([ + models.ProjectPhase.findOne({ + where: { id: phaseId, projectId }, + raw: true, + }), + models.Project.findOne({ + where: { id: projectId }, + raw: true, + }), + ]); + } + return Promise.reject('Unable to find product'); + }).then((projectAndPhase) => { + logger.debug('projectAndPhase', projectAndPhase); + if (projectAndPhase) { + const phase = projectAndPhase[0]; + const project = projectAndPhase[1]; + return Promise.resolve({ product, phase, project }); + } + return Promise.reject('Unable to find phase/project'); + }); +}; + +/** + * Raises the project plan modified event + * @param {Object} app Application object used to interact with RMQ service + * @param {String} topic Kafka topic + * @param {Object} payload Message payload + * @return {Promise} Promise + */ +async function timelineAdjustedKafkaHandler(app, topic, payload) { + app.logger.debug(`Handling Kafka event for ${topic}`); + // Validate payload + const result = Joi.validate(payload, payloadSchema); + if (result.error) { + throw new Error(result.error); + } + + const timeline = payload.updatedTimeline; + // process only if timeline is related to a product reference + if (timeline && timeline.reference === TIMELINE_REFERENCES.PRODUCT) { + app.logger.debug('Found product timelin event '); + const productId = timeline.referenceId; + app.logger.debug('Calling findProjectPhaseProduct'); + const { project } = await findProjectPhaseProduct(app.logger, productId); + app.logger.debug('Successfully fetched project, phase and product'); + app.logger.debug('Raising BUS event for PROJECT_PLAN_UPDATED'); + createEvent(BUS_API_EVENT.PROJECT_PLAN_UPDATED, { + projectId: project.id, + projectName: project.name, + projectUrl: connectProjectUrl(project.id), + userId: payload.userId, + initiatorUserId: payload.userId, + }, app.logger); + } +} module.exports = { timelineAddedHandler, timelineUpdatedHandler, timelineRemovedHandler, + timelineAdjustedKafkaHandler, }; diff --git a/src/models/milestone.js b/src/models/milestone.js index 0f4bc4ec..53429883 100644 --- a/src/models/milestone.js +++ b/src/models/milestone.js @@ -1,3 +1,4 @@ +import moment from 'moment'; /* eslint-disable valid-jsdoc */ /** @@ -35,6 +36,54 @@ module.exports = (sequelize, DataTypes) => { updatedAt: 'updatedAt', createdAt: 'createdAt', deletedAt: 'deletedAt', + classMethods: { + /** + * Get total duration of the given timeline by summing up individual milestone durations + * @param timelineId the id of timeline + */ + getTimelineDuration(timelineId) { + console.log('getTimelineDuration'); + const where = { timelineId, hidden: false }; + return this.findAll({ + where, + order: [['order', 'asc']], + attributes: ['id', 'duration', 'startDate', 'endDate', 'actualStartDate', 'completionDate'], + raw: true, + }) + .then((milestones) => { + let scheduledDuration = 0; + let completedDuration = 0; + let duration = 0; + let progress = 0; + if (milestones) { + const fMilestone = milestones[0]; + const lMilestone = milestones[milestones.length - 1]; + const startDate = fMilestone.actualStartDate ? fMilestone.actualStartDate : fMilestone.startDate; + const endDate = lMilestone.completionDate ? lMilestone.completionDate : lMilestone.endDate; + duration = moment.utc(endDate).diff(moment.utc(startDate), 'days') + 1; + milestones.forEach((m) => { + if (m.completionDate !== null) { + let mDuration = 0; + if (m.actualStartDate !== null) { + mDuration = moment.utc(m.completionDate).diff(moment.utc(m.actualStartDate), 'days') + 1; + } else { + mDuration = moment.utc(m.completionDate).diff(moment.utc(m.startDate), 'days') + 1; + } + scheduledDuration += mDuration; + completedDuration += mDuration; + } else { + scheduledDuration += m.duration; + } + }); + console.log(`${completedDuration} completed out of ${scheduledDuration} duration`); + if (scheduledDuration > 0) { + progress = Math.round((completedDuration / scheduledDuration) * 100); + } + } + return Promise.resolve({ duration, progress }); + }); + }, + }, }); return Milestone; diff --git a/src/routes/milestones/create.spec.js b/src/routes/milestones/create.spec.js index bf652b28..ab1424ca 100644 --- a/src/routes/milestones/create.spec.js +++ b/src/routes/milestones/create.spec.js @@ -629,7 +629,7 @@ describe('CREATE milestone', () => { sandbox.restore(); }); - it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when milestone created', (done) => { + it('should send message BUS_API_EVENT.TIMELINE_ADJUSTED when milestone created', (done) => { request(server) .post('/v4/timelines/1/milestones') .set({ @@ -644,7 +644,7 @@ describe('CREATE milestone', () => { } else { testUtil.wait(() => { createEventSpy.calledOnce.should.be.true; - createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_ADDED, sinon.match({ projectId: 1, projectName: 'test1', projectUrl: 'https://local.topcoder-dev.com/projects/1', diff --git a/src/routes/milestones/delete.spec.js b/src/routes/milestones/delete.spec.js index ee9c41e3..c756b7b0 100644 --- a/src/routes/milestones/delete.spec.js +++ b/src/routes/milestones/delete.spec.js @@ -373,8 +373,8 @@ describe('DELETE milestone', () => { }); // not testing fields separately as startDate is required parameter, - // thus PROJECT_PLAN_UPDATED will be always sent - it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when milestone removed', (done) => { + // thus TIMELINE_ADJUSTED will be always sent + it('should send message BUS_API_EVENT.TIMELINE_ADJUSTED when milestone removed', (done) => { request(server) .delete('/v4/timelines/1/milestones/1') .set({ @@ -387,7 +387,7 @@ describe('DELETE milestone', () => { } else { testUtil.wait(() => { createEventSpy.calledOnce.should.be.true; - createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_REMOVED, sinon.match({ projectId: 1, projectName: 'test1', projectUrl: 'https://local.topcoder-dev.com/projects/1', diff --git a/src/routes/milestones/update.js b/src/routes/milestones/update.js index 90e179d5..98a5cdf1 100644 --- a/src/routes/milestones/update.js +++ b/src/routes/milestones/update.js @@ -55,7 +55,7 @@ function updateComingMilestones(origMilestone, updMilestone) { } // Calculate the endDate, and update it if different - const endDate = moment.utc(startDate).add(milestone.duration - 1, 'days').toDate(); + const endDate = moment.utc(milestone.startDate).add(milestone.duration - 1, 'days').toDate(); if (!_.isEqual(milestone.endDate, endDate)) { milestone.endDate = endDate; milestone.updatedBy = updMilestone.updatedBy; @@ -69,10 +69,13 @@ function updateComingMilestones(origMilestone, updMilestone) { firstMilestoneFound = true; } - // Set the next startDate value to the next day after completionDate if present or the endDate - startDate = moment.utc(milestone.completionDate - ? milestone.completionDate - : milestone.endDate).add(1, 'days').toDate(); + // if milestone is not hidden, update the startDate for the next milestone, otherwise keep the same startDate for next milestone + if (!milestone.hidden) { + // Set the next startDate value to the next day after completionDate if present or the endDate + startDate = moment.utc(milestone.completionDate + ? milestone.completionDate + : milestone.endDate).add(1, 'days').toDate(); + } return milestone.save(); }); @@ -174,6 +177,7 @@ module.exports = [ // if status has changed to be completed, set the compeltionDate if not provided if (entityToUpdate.status === MILESTONE_STATUS.COMPLETED) { entityToUpdate.completionDate = entityToUpdate.completionDate ? entityToUpdate.completionDate : today; + entityToUpdate.duration = entityToUpdate.completionDate.diff(entityToUpdate.actualStartDate, 'days') + 1; } // if status has changed to be active, set the startDate to today if (entityToUpdate.status === MILESTONE_STATUS.ACTIVE) { @@ -198,6 +202,7 @@ module.exports = [ // if completionDate has changed if (!statusChanged && completionDateChanged) { + entityToUpdate.duration = entityToUpdate.completionDate.diff(entityToUpdate.actualStartDate, 'days') + 1; entityToUpdate.status = MILESTONE_STATUS.COMPLETED; } @@ -253,6 +258,7 @@ module.exports = [ const needToCascade = !_.isEqual(original.completionDate, updated.completionDate) // completion date changed || original.duration !== updated.duration // duration changed || original.actualStartDate !== updated.actualStartDate; // actual start date updated + req.log.debug('needToCascade', needToCascade); // Update dates of the other milestones only if cascade updates needed if (needToCascade) { return updateComingMilestones(original, updated) diff --git a/src/routes/milestones/update.spec.js b/src/routes/milestones/update.spec.js index 6286c5d4..ad591287 100644 --- a/src/routes/milestones/update.spec.js +++ b/src/routes/milestones/update.spec.js @@ -1102,7 +1102,7 @@ describe('UPDATE Milestone', () => { sandbox.restore(); }); - it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when milestone duration updated', (done) => { + it('should send message BUS_API_EVENT.TIMELINE_ADJUSTED when milestone duration updated', (done) => { request(server) .patch('/v4/timelines/1/milestones/1') .set({ @@ -1119,22 +1119,25 @@ describe('UPDATE Milestone', () => { done(err); } else { testUtil.wait(() => { - createEventSpy.calledTwice.should.be.true; - createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + // 5 milestones in total, so it would trigger 5 events + // 4 MILESTONE_UPDATED events are for 4 non deleted milestones + // 1 TIMELINE_ADJUSTED event, because timeline's end date updated + createEventSpy.callCount.should.be.eql(5); + createEventSpy.firstCall.calledWith(BUS_API_EVENT.MILESTONE_UPDATED, sinon.match({ projectId: 1, projectName: 'test1', projectUrl: 'https://local.topcoder-dev.com/projects/1', userId: 40051332, initiatorUserId: 40051332, })).should.be.true; - createEventSpy.secondCall.calledWith(BUS_API_EVENT.TIMELINE_MODIFIED); + createEventSpy.lastCall.calledWith(BUS_API_EVENT.TIMELINE_ADJUSTED); done(); }); } }); }); - it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when milestone status updated', (done) => { + it('should send message BUS_API_EVENT.MILESTONE_UPDATED when milestone status updated', (done) => { request(server) .patch('/v4/timelines/1/milestones/1') .set({ @@ -1152,7 +1155,7 @@ describe('UPDATE Milestone', () => { } else { testUtil.wait(() => { createEventSpy.calledOnce.should.be.true; - createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + createEventSpy.firstCall.calledWith(BUS_API_EVENT.MILESTONE_UPDATED, sinon.match({ projectId: 1, projectName: 'test1', projectUrl: 'https://local.topcoder-dev.com/projects/1', @@ -1165,7 +1168,7 @@ describe('UPDATE Milestone', () => { }); }); - it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when milestone order updated', (done) => { + it('should ONLY send message BUS_API_EVENT.MILESTONE_UPDATED when milestone order updated', (done) => { request(server) .patch('/v4/timelines/1/milestones/1') .set({ @@ -1183,7 +1186,7 @@ describe('UPDATE Milestone', () => { } else { testUtil.wait(() => { createEventSpy.calledOnce.should.be.true; - createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + createEventSpy.firstCall.calledWith(BUS_API_EVENT.MILESTONE_UPDATED, sinon.match({ projectId: 1, projectName: 'test1', projectUrl: 'https://local.topcoder-dev.com/projects/1', @@ -1196,7 +1199,7 @@ describe('UPDATE Milestone', () => { }); }); - it('should not send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when milestone plannedText updated', (done) => { + it('should ONLY send message BUS_API_EVENT.MILESTONE_UPDATED when milestone plannedText updated', (done) => { request(server) .patch('/v4/timelines/1/milestones/1') .set({ @@ -1213,7 +1216,14 @@ describe('UPDATE Milestone', () => { done(err); } else { testUtil.wait(() => { - createEventSpy.notCalled.should.be.true; + createEventSpy.calledOnce.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.MILESTONE_UPDATED, sinon.match({ + projectId: 1, + projectName: 'test1', + projectUrl: 'https://local.topcoder-dev.com/projects/1', + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; done(); }); } diff --git a/src/routes/timelines/update.spec.js b/src/routes/timelines/update.spec.js index 4cb811a9..72fffcb3 100644 --- a/src/routes/timelines/update.spec.js +++ b/src/routes/timelines/update.spec.js @@ -648,8 +648,8 @@ describe('UPDATE timeline', () => { }); // not testing fields separately as startDate is required parameter, - // thus PROJECT_PLAN_UPDATED will be always sent - it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when timeline updated', (done) => { + // thus TIMELINE_ADJUSTED will be always sent + it('should send message BUS_API_EVENT.TIMELINE_ADJUSTED when timeline updated', (done) => { request(server) .patch('/v4/timelines/1') .set({ @@ -663,7 +663,7 @@ describe('UPDATE timeline', () => { } else { testUtil.wait(() => { createEventSpy.calledOnce.should.be.true; - createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + createEventSpy.calledWith(BUS_API_EVENT.TIMELINE_ADJUSTED, sinon.match({ projectId: 1, projectName: 'test1', projectUrl: 'https://local.topcoder-dev.com/projects/1',