diff --git a/requirements/informationController/addInformation.md b/requirements/informationController/addInformation.md new file mode 100644 index 000000000..d62066735 --- /dev/null +++ b/requirements/informationController/addInformation.md @@ -0,0 +1,16 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Add Information + +> ## Positive case +1. ✅ Returns 201 if adding new information successfullyn and no cache. +2. ✅ Returns if adding new information successfully and hascache. + +> ## Negative case +1. ✅ Returns error 500 if if there are no information in the database and any error occurs when finding the infoName. +2. ✅ Returns error 400 if if there are duplicate infoName in the database. +3. ✅ Returns error 400 if if there are issues when saving new informations. +4. ✅ Returns error 400 if if there are errors when saving the new information. + +> ## Edge case \ No newline at end of file diff --git a/requirements/informationController/deleteInformation.md b/requirements/informationController/deleteInformation.md new file mode 100644 index 000000000..fb5c3c867 --- /dev/null +++ b/requirements/informationController/deleteInformation.md @@ -0,0 +1,13 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Delete Information + +> ## Positive case +1. ✅ Returns 200 if deleting informations successfull and no cache. +2. ✅ Returns if deleting informations successfully and has cache. + +> ## Negative case +1. ✅ Returns error 400 if if there is any error when finding the information by information id. + +> ## Edge case \ No newline at end of file diff --git a/requirements/informationController/getInformation.md b/requirements/informationController/getInformation.md new file mode 100644 index 000000000..bd8976a5a --- /dev/null +++ b/requirements/informationController/getInformation.md @@ -0,0 +1,13 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get Information + +> ## Positive case +1. ✅ Returns 200 if the informations key exists in NodeCache. +2. ✅ Returns 200 if there are information in the database. + +> ## Negative case +1. ✅ Returns error 404 if if there are no information in the database and any error occurs when finding the information. + +> ## Edge case \ No newline at end of file diff --git a/requirements/informationController/updateInformation.md b/requirements/informationController/updateInformation.md new file mode 100644 index 000000000..709bcba54 --- /dev/null +++ b/requirements/informationController/updateInformation.md @@ -0,0 +1,13 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Update Information + +> ## Positive case +1. ✅ Returns 200 if updating informations successfully when no cache. +2. ✅ Returns if updating informations successfully when hascache. + +> ## Negative case +1. ✅ Returns error 400 if if there is any error when finding the information by information id. + +> ## Edge case \ No newline at end of file diff --git a/requirements/reasonSchedulingController/deleteReason.md b/requirements/reasonSchedulingController/deleteReason.md new file mode 100644 index 000000000..8df5ccb16 --- /dev/null +++ b/requirements/reasonSchedulingController/deleteReason.md @@ -0,0 +1,16 @@ +Check mark: ✅ +Cross Mark: ❌ + +# deleteReason + +> ## Positive case +1. ✅ Receives a POST request in the **/api/reason/:userId/** route. +2. ✅ Return 200 if delete reason successfully. + +> ## Negative case +1. ✅ Returns 403 when no permission to delete. +2. ✅ Returns 404 when error in finding user Id. +3. ✅ Returns 404 when error in finding reason. +4. ✅ Returns 500 when error in deleting. + +> ## Edge case \ No newline at end of file diff --git a/requirements/reasonSchedulingController/getAllReasons.md b/requirements/reasonSchedulingController/getAllReasons.md new file mode 100644 index 000000000..58499a41b --- /dev/null +++ b/requirements/reasonSchedulingController/getAllReasons.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getAllReasons + +> ## Positive case +1. ✅ Receives a GET request in the **/api/reason/:userId** route. +2. ✅ Return 200 if get schedule reason successfully. + +> ## Negative case +1. ✅ Returns 404 when error in finding user by Id. +2. ✅ Returns 400 when any error in fetching the user + +> ## Edge case \ No newline at end of file diff --git a/requirements/reasonSchedulingController/getSingleReason.md b/requirements/reasonSchedulingController/getSingleReason.md new file mode 100644 index 000000000..bc81fd9d9 --- /dev/null +++ b/requirements/reasonSchedulingController/getSingleReason.md @@ -0,0 +1,15 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getSingleReason + +> ## Positive case +1. ✅ Receives a GET request in the **/api/reason/single/:userId** route. +2. ✅ Return 200 if not found schedule reason and return empty object successfully. +3. ✅ Return 200 if found schedule reason and return reason successfully. + +> ## Negative case +1. ✅ Returns 404 when any error in find user by Id +2. ✅ Returns 400 when any error in fetching the user + +> ## Edge case \ No newline at end of file diff --git a/requirements/reasonSchedulingController/patchReason.md b/requirements/reasonSchedulingController/patchReason.md new file mode 100644 index 000000000..6e84a8ba7 --- /dev/null +++ b/requirements/reasonSchedulingController/patchReason.md @@ -0,0 +1,16 @@ +Check mark: ✅ +Cross Mark: ❌ + +# patchReason + +> ## Positive case +1. ✅ Receives a POST request in the **/api/breason/** route. +2. ✅ Return 200 if updated schedule reason and send blue sqaure email successfully. + +> ## Negative case +1. ✅ Returns 400 for not providing reason. +2. ✅ Returns 404 when error in finding user Id. +3. ✅ Returns 404 when not finding provided reason. +4. ✅ Returns 400 when any error in saving. + +> ## Edge case \ No newline at end of file diff --git a/requirements/reasonSchedulingController/postReason.md b/requirements/reasonSchedulingController/postReason.md new file mode 100644 index 000000000..ac8ea8f2d --- /dev/null +++ b/requirements/reasonSchedulingController/postReason.md @@ -0,0 +1,18 @@ +Check mark: ✅ +Cross Mark: ❌ + +# postReason + +> ## Positive case +1. ✅ Receives a POST request in the **/api/reason/** route. +2. ✅ Return 200 if s dchedule reason and send blue sqaure email successfully. + +> ## Negative case +1. ✅ Returns 400 for warning to choose Sunday. +2. ✅ Returns 400 for warning to choose a funture date. +3. ✅ Returns 400 for not providing reason. +4. ✅ Returns 404 when error in finding user Id. +5. ✅ Returns 403 when duplicate reason to the date. +6. ✅ Returns 400 when any error in saving. + +> ## Edge case \ No newline at end of file diff --git a/src/controllers/informationController.js b/src/controllers/informationController.js index 792620995..03a23ba57 100644 --- a/src/controllers/informationController.js +++ b/src/controllers/informationController.js @@ -1,17 +1,17 @@ -const mongoose = require('mongoose'); +// const mongoose = require('mongoose'); // const userProfile = require('../models/userProfile'); // const hasPermission = require('../utilities/permissions'); const escapeRegex = require('../utilities/escapeRegex'); -const cache = require('../utilities/nodeCache')(); +const cacheClosure = require('../utilities/nodeCache'); const informationController = function (Information) { + const cache = cacheClosure(); const getInformations = function (req, res) { // return all informations if cache is available if (cache.hasCache('informations')) { res.status(200).send(cache.getCache('informations')); return; } - Information.find({}, 'infoName infoContent visibility') .then((results) => { // cache results diff --git a/src/controllers/informationController.spec.js b/src/controllers/informationController.spec.js new file mode 100644 index 000000000..e69dd2a32 --- /dev/null +++ b/src/controllers/informationController.spec.js @@ -0,0 +1,392 @@ +/* eslint-disable no-unused-vars */ +// const mongoose = require('mongoose'); +const mongoose = require('mongoose'); + +jest.mock('../utilities/nodeCache'); +const cache = require('../utilities/nodeCache'); +const Information = require('../models/information'); +const escapeRegex = require('../utilities/escapeRegex'); +const informationController = require('./informationController'); +const { mockReq, mockRes, assertResMock } = require('../test'); + +/* eslint-disable no-unused-vars */ +/* eslint-disable prefer-promise-reject-errors */ + +const makeSut = () => { + const { addInformation, getInformations, updateInformation, deleteInformation } = + informationController(Information); + + return { + addInformation, + getInformations, + updateInformation, + deleteInformation, + }; +}; +// Define flushPromises function)); +const flushPromises = () => new Promise(setImmediate); + +const makeMockCache = (method, value) => { + const cacheObject = { + getCache: jest.fn(), + removeCache: jest.fn(), + hasCache: jest.fn(), + setCache: jest.fn(), + }; + + const mockCache = jest.spyOn(cacheObject, method).mockImplementationOnce(() => value); + + cache.mockImplementationOnce(() => cacheObject); + + return { mockCache, cacheObject }; +}; + +describe('informationController module', () => { + beforeEach(() => {}); + afterEach(() => { + jest.clearAllMocks(); + }); + describe('addInformation function', () => { + test('Ensure addInformation returns 500 if any error when finding any information', async () => { + const { addInformation } = makeSut(); + const newMockReq = { + ...mockReq.body, + body: { + infoName: 'some infoName', + }, + }; + jest + .spyOn(Information, 'find') + .mockImplementationOnce(() => Promise.reject(new Error('Error when finding'))); + const response = addInformation(newMockReq, mockRes); + await flushPromises(); + assertResMock(500, { error: new Error('Error when finding') }, response, mockRes); + }); + test('Ensure addInformation returns 400 if duplicate info Name', async () => { + const { addInformation } = makeSut(); + const data = [{ infoName: 'test Info' }]; + const findSpy = jest + .spyOn(Information, 'find') + .mockImplementationOnce(() => Promise.resolve(data)); + const newMockReq = { + body: { + ...mockReq.body, + infoName: 'test Info', + }, + }; + const response = addInformation(newMockReq, mockRes); + await flushPromises(); + expect(findSpy).toHaveBeenCalledWith({ + infoName: { $regex: escapeRegex(newMockReq.body.infoName), $options: 'i' }, + }); + assertResMock( + 400, + { + error: `Info Name must be unique. Another infoName with name ${newMockReq.body.infoName} already exists. Please note that info names are case insensitive`, + }, + response, + mockRes, + ); + }); + test('Ensure addInformations returns 400 if any error when saving new Information', async () => { + const { addInformation } = makeSut(); + const newMockReq = { + body: { + ...mockReq.body, + infoName: 'some Info', + }, + }; + const findSpy = jest + .spyOn(Information, 'find') + .mockImplementationOnce(() => Promise.resolve(true)); + jest + .spyOn(Information.prototype, 'save') + .mockImplementationOnce(() => Promise.reject(new Error('Error when saving'))); + const response = addInformation(newMockReq, mockRes); + await flushPromises(); + + expect(findSpy).toHaveBeenCalledWith({ + infoName: { $regex: escapeRegex(newMockReq.body.infoName), $options: 'i' }, + }); + assertResMock(400, new Error('Error when saving'), response, mockRes); + }); + + test('Ensure addInformation returns 201 if creating information successfully when no cache', async () => { + const { mockCache: hasCacheMock } = makeMockCache('hasCache', ''); + const { addInformation } = makeSut(); + const data = { + infoName: 'mockAdd', + infoContent: 'mockContent', + visibility: '1', + }; + + const findSpy = jest + .spyOn(Information, 'find') + .mockImplementationOnce(() => Promise.resolve([])); + jest.spyOn(Information.prototype, 'save').mockImplementationOnce(() => Promise.resolve(data)); + const newMockReq = { + body: { + ...mockReq.body, + infoName: 'some addInfo', + infoContent: '1', + visibility: '1', + }, + }; + const response = addInformation(newMockReq, mockRes); + await flushPromises(); + expect(findSpy).toHaveBeenCalledWith({ + infoName: { $regex: escapeRegex(newMockReq.body.infoName), $options: 'i' }, + }); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + assertResMock(201, data, response, mockRes); + }); + test('Ensure addInformation returns 201 if creating information successfully', async () => { + const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', '[{_id: 1}]'); + const removeCacheMock = jest + .spyOn(cacheObject, 'removeCache') + .mockImplementationOnce(() => null); + const { addInformation } = makeSut(); + const data = [ + { + infoName: 'mockAdd', + infoContent: 'mockContent', + visibility: '1', + }, + ]; + + const findSpy = jest + .spyOn(Information, 'find') + .mockImplementationOnce(() => Promise.resolve([])); + jest.spyOn(Information.prototype, 'save').mockImplementationOnce(() => Promise.resolve(data)); + const newMockReq = { + body: { + ...mockReq.body, + infoName: 'some addInfo', + infoContent: '1', + visibility: '1', + }, + }; + addInformation(newMockReq, mockRes); + await flushPromises(); + expect(findSpy).toHaveBeenCalledWith({ + infoName: { $regex: escapeRegex(newMockReq.body.infoName), $options: 'i' }, + }); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + expect(removeCacheMock).toHaveBeenCalledWith('informations'); + }); + }); + describe('getInformations function', () => { + test('Ensure getInformations returns 200 if when informations key in cache', async () => { + const data = [ + { + _id: 1, + infoName: 'infoName', + infoContent: 'infoContent', + visibility: '1', + }, + ]; + const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', data); + const getCacheMock = jest.spyOn(cacheObject, 'getCache').mockImplementationOnce(() => data); + const { getInformations } = makeSut(); + + const response = getInformations(mockReq, mockRes); + await flushPromises(); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + expect(getCacheMock).toHaveBeenCalledWith('informations'); + assertResMock(200, data, response, mockRes); + }); + test('Ensure getInformations returns 404 if any error when no informations key and catch error in finding', async () => { + const { mockCache: hasCacheMock } = makeMockCache('hasCache', ''); + const findSpy = jest + .spyOn(Information, 'find') + .mockImplementationOnce(() => Promise.reject(new Error('Error when finding information'))); + const { getInformations } = makeSut(); + + const response = getInformations(mockReq, mockRes); + await flushPromises(); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + expect(findSpy).toHaveBeenCalledWith({}, 'infoName infoContent visibility'); + assertResMock(404, new Error('Error when finding information'), response, mockRes); + }); + + test('Ensure getInformations returns 200 when no informations key and no duplicated information', async () => { + const data = [ + { + infoName: 'mockAdd', + infoContent: 'mockContent', + visibility: '1', + }, + ]; + const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', ''); + const findSpy = jest + .spyOn(Information, 'find') + .mockImplementationOnce(() => Promise.resolve(data)); + const setCacheMock = jest.spyOn(cacheObject, 'setCache').mockImplementationOnce(() => data); + + const { getInformations } = makeSut(); + const newMockReq = { + body: { + ...mockReq.body, + infoName: 'some getInfo', + infoContent: '1', + visibility: '1', + }, + }; + const response = getInformations(newMockReq, mockRes); + await flushPromises(); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + expect(findSpy).toHaveBeenCalledWith({}, 'infoName infoContent visibility'); + expect(setCacheMock).toHaveBeenCalledWith('informations', data); + assertResMock(200, data, response, mockRes); + }); + }); + describe('deleteInformation function', () => { + test('Ensure deleteInformation returns 400 if any error when finding and delete information', async () => { + const errorMsg = 'Error when finding and deleting information by Id'; + const { deleteInformation } = makeSut(); + jest + .spyOn(Information, 'findOneAndDelete') + .mockImplementationOnce(() => Promise.reject(new Error(errorMsg))); + const response = deleteInformation(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, new Error(errorMsg), response, mockRes); + }); + test('Ensure deleteInformation returns 200 if delete information successfully no cache', async () => { + const { mockCache: hasCacheMock } = makeMockCache('hasCache', ''); + const deletedData = { + id: '601acda376045c7879d13a77', + infoName: 'deletedInfo', + infoContent: 'deleted', + visibility: '1', + }; + const { deleteInformation } = makeSut(); + const newMockReq = { + ...mockReq.body, + params: { + ...mockReq.params, + id: '601acda376045c7879d13a77', + }, + }; + const findOneDeleteSpy = jest + .spyOn(Information, 'findOneAndDelete') + .mockImplementationOnce(() => Promise.resolve(deletedData)); + const response = deleteInformation(newMockReq, mockRes); + await flushPromises(); + expect(findOneDeleteSpy).toHaveBeenCalledWith({ _id: deletedData.id }); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + assertResMock(200, deletedData, response, mockRes); + }); + test('Ensure deleteInformation returns if delete information successfully and has cache', async () => { + const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', '[{_id:123}]'); + const removeCacheMock = jest + .spyOn(cacheObject, 'removeCache') + .mockImplementationOnce(() => null); + const deletedData = { + id: '601acda376045c7879d13a77', + infoName: 'deletedInfo', + infoContent: 'deleted', + visibility: '1', + }; + const { deleteInformation } = makeSut(); + const newMockReq = { + ...mockReq.body, + params: { + ...mockReq.params, + id: '601acda376045c7879d13a77', + infoName: 'deletedInfo', + infoContent: 'deleted', + visibility: '1', + }, + }; + const findOneDeleteSpy = jest + .spyOn(Information, 'findOneAndDelete') + .mockImplementationOnce(() => Promise.resolve(deletedData)); + deleteInformation(newMockReq, mockRes); + await flushPromises(); + expect(findOneDeleteSpy).toHaveBeenCalledWith({ _id: deletedData.id }); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + expect(removeCacheMock).toHaveBeenCalledWith('informations'); + }); + }); + describe('updateInformation function', () => { + test('Ensure updateInformation returns 400 if any error when finding and update information', async () => { + const errorMsg = 'Error when finding and updating information by Id'; + const { updateInformation } = makeSut(); + jest + .spyOn(Information, 'findOneAndUpdate') + .mockImplementationOnce(() => Promise.reject(new Error(errorMsg))); + const response = updateInformation(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, new Error(errorMsg), response, mockRes); + }); + test('Ensure updateInformation returns 200 if finding and update information successfuly when nocache', async () => { + const { mockCache: hasCacheMock } = makeMockCache('hasCache', ''); + const data = { + id: '601acda376045c7879d13a77', + infoName: 'updatedInfo', + infoContent: 'updated', + visibility: '1', + }; + const newMockReq = { + body: { + id: '601acda376045c7879d13a77', + infoName: 'oldInfo', + infoContent: 'old', + visibility: '0', + }, + params: { + ...mockReq.params, + id: '601acda376045c7879d13a77', + }, + }; + const findOneUpdateSpy = jest + .spyOn(Information, 'findOneAndUpdate') + .mockImplementationOnce(() => Promise.resolve(data)); + const { updateInformation } = makeSut(); + const response = updateInformation(newMockReq, mockRes); + await flushPromises(); + expect(findOneUpdateSpy).toHaveBeenCalledWith({ _id: data.id }, newMockReq.body, { + new: true, + }); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + assertResMock(200, data, response, mockRes); + }); + test('Ensure updateInformation returns if finding and update information successfuly when hascache', async () => { + const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', '[{_id:123}]'); + const removeCacheMock = jest + .spyOn(cacheObject, 'removeCache') + .mockImplementationOnce(() => null); + const data = { + id: '601acda376045c7879d13a77', + infoName: 'updatedInfo', + infoContent: 'updated', + visibility: '1', + }; + const newMockReq = { + body: { + id: '601acda376045c7879d13a77', + infoName: 'oldInfo', + infoContent: 'old', + visibility: '0', + }, + params: { + ...mockReq.params, + id: '601acda376045c7879d13a77', + }, + }; + const findOneUpdateSpy = jest + .spyOn(Information, 'findOneAndUpdate') + .mockImplementationOnce(() => Promise.resolve(data)); + const { updateInformation } = makeSut(); + updateInformation(newMockReq, mockRes); + await flushPromises(); + expect(findOneUpdateSpy).toHaveBeenCalledWith({ _id: data.id }, newMockReq.body, { + new: true, + }); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + expect(removeCacheMock).toHaveBeenCalledWith('informations'); + }); + }); +}); diff --git a/src/controllers/reasonSchedulingController.spec.js b/src/controllers/reasonSchedulingController.spec.js new file mode 100644 index 000000000..e58c77466 --- /dev/null +++ b/src/controllers/reasonSchedulingController.spec.js @@ -0,0 +1,626 @@ +const moment = require('moment-timezone'); +const { mockReq, mockRes, mockUser } = require('../test'); +const UserModel = require('../models/userProfile'); + +jest.mock('../utilities/emailSender'); +const emailSender = require('../utilities/emailSender') + +const { + postReason, + getAllReasons, + getSingleReason, + patchReason, + deleteReason, +} = require('./reasonSchedulingController'); + +// assertResMock +const ReasonModel = require('../models/reason'); + +const flushPromises = () => new Promise(setImmediate); + +function mockDay(dayIdx, past = false) { + const date = moment().tz('America/Los_Angeles').startOf('day'); + while (date.day() !== dayIdx) { + date.add(past ? -1 : 1, 'days'); + } + return date; +} + +describe('reasonScheduling Controller', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockRes.json = jest.fn(); + mockReq.body = { + ...mockReq.body, + ...mockUser(), + reasonData: { + date: mockDay(0), + message: 'some reason', + }, + currentDate: moment.tz('America/Los_Angeles').startOf('day'), + }; + mockReq.params = { + ...mockReq.params, + ...mockUser(), + }; + }); + afterEach(() => { + jest.clearAllMocks(); + }); + describe('postReason method', () => { + test('Ensure postReason returns 400 for warning to choose Sunday', async () => { + mockReq.body.reasonData.date = mockDay(1, true); + await postReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: + "You must choose the Sunday YOU'LL RETURN as your date. This is so your reason ends up as a note on that blue square.", + errorCode: 0, + }), + ); + }); + test('Ensure postReason returns 400 for warning to choose a future date', async () => { + mockReq.body.reasonData.date = mockDay(0, true); + await postReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'You should select a date that is yet to come', + errorCode: 7, + }), + ); + }); + test('Ensure postReason returns 400 for not providing reason', async () => { + mockReq.body.reasonData.message = null; + await postReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'You must provide a reason.', + errorCode: 6, + }), + ); + }); + test('Ensure postReason returns 404 when error in finding user Id', async () => { + const mockFindUser = jest.spyOn(UserModel, 'findById').mockImplementationOnce(() => + Promise.resolve(null)); + + await postReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.body.userId); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'User not found', + errorCode: 2, + }), + ); + }); + test('Ensure postReason returns 403 when duplicate reason to the date', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + jest.spyOn(UserModel, 'findOneAndUpdate').mockResolvedValueOnce({ + _id: mockReq.body.userId, + timeOffFrom: mockReq.body.currentDate, + timeOffTill: mockReq.body.reasonData.date, + }); + const mockReason = { + reason: 'Some Reason', + userId: mockReq.body.userId, + date: moment.tz('America/Los_Angeles').startOf('day').toISOString(), + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValue(mockReason); + + await postReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(403); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.body.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.body.userId, + }); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'The reason must be unique to the date', + errorCode: 3, + }), + ); + }); + test('Ensure postReason returns 400 when any error in saving.', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + jest.spyOn(UserModel, 'findOneAndUpdate').mockResolvedValueOnce({ + _id: mockReq.body.userId, + timeOffFrom: mockReq.body.currentDate, + timeOffTill: mockReq.body.reasonData.date, + }); + mockRes.sendStatus = jest.fn().mockReturnThis(); + const newReason = { + reason: mockReq.body.reasonData.message, + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.body.userId, + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValue(); + const mockSave = jest.spyOn(ReasonModel.prototype, 'save').mockRejectedValue(newReason); + emailSender.mockImplementation(() => { + throw new Error('Failed to send email'); + }); + + await postReason(mockReq, mockRes); + await flushPromises(); + emailSender.mockRejectedValue(new Error('Failed')); + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.body.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.body.userId, + }); + expect(mockSave).toHaveBeenCalledWith(); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + errMessage: 'Something went wrong', + }), + ); + }); + test('Ensure postReason returns 200 if schedule reason and send blue sqaure email successfully', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + jest.spyOn(UserModel, 'findOneAndUpdate').mockResolvedValueOnce({ + _id: mockReq.body.userId, + timeOffFrom: mockReq.body.currentDate, + timeOffTill: mockReq.body.reasonData.date, + }); + mockRes.sendStatus = jest.fn().mockReturnThis(); + const newReason = { + reason: mockReq.body.reasonData.message, + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.body.userId, + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValue(); + const mockSave = jest.spyOn(ReasonModel.prototype, 'save').mockResolvedValue(newReason); + emailSender.mockImplementation(() => { + Promise.resolve(); + }); + await postReason(mockReq, mockRes); + await flushPromises(); + expect(mockRes.sendStatus).toHaveBeenCalledWith(200); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.body.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.body.userId, + }); + expect(mockSave).toHaveBeenCalledWith(); + }); + }); + describe('getAllReason method', () => { + test('Ensure get AllReason returns 404 when error in finding user Id', async () => { + const mockFindUser = jest.spyOn(UserModel, 'findById').mockImplementationOnce(() => null); + + await getAllReasons(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'User not found', + }), + ); + }); + test('Ensure get AllReason returns 400 when any error in fetching the user', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + const mockFoundReason = jest.spyOn(ReasonModel, 'find').mockRejectedValueOnce(null); + await getAllReasons(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + userId: mockReq.params.userId, + }); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + errMessage: 'Something went wrong while fetching the user', + }), + ); + }); + test('Ensure get AllReason returns 200 when get schedule reason successfully', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + const reasons = { + reason: 'Some Reason', + userId: mockReq.params.userId, + date: moment.tz('America/Los_Angeles').startOf('day').toISOString(), + isSet: true, + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'find').mockResolvedValue(reasons); + await getAllReasons(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + userId: mockReq.params.userId, + }); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + reasons, + }), + ); + }); + }); + describe('getSingleReason method', () => { + test('Ensure getSingleReason return 400 when any error in fetching the user', async () => { + await getSingleReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Something went wrong while fetching single reason', + }), + ); + }); + test('Ensure getSingleReason return 404 when any error in find user by Id', async () => { + mockReq.query = { + queryData: mockDay(0), + }; + const mockFindUser = jest.spyOn(UserModel, 'findById').mockImplementationOnce(() => null); + + await getSingleReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'User not found', + errorCode: 2, + }), + ); + }); + test('Ensure getSingleReason return 200 if not found schedule reason and return empty object successfully.', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + + mockReq.query = { + queryDate: mockDay(0), + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValueOnce(); + + await getSingleReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.query.queryDate, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + }); + expect(mockRes.json).toHaveBeenCalledWith({ + reason: '', + date: '', + userId: '', + isSet: false, + }); + }); + test('Ensure getSingleReason return 200 if found schedule reason and return reason successfully.', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + + mockReq.query = { + queryDate: mockDay(0), + }; + const singleReason = { + reason: 'Some Reason', + userId: mockReq.params.userId, + date: moment.tz('America/Los_Angeles').startOf('day').toISOString(), + isSet: true, + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValue(singleReason); + + await getSingleReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.query.queryDate, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + }); + expect(mockRes.json).toHaveBeenCalledWith(singleReason); + }); + }); + describe('patchReason method', () => { + test('Ensure patchReason returns 400 for not providing reason', async () => { + mockReq.body.reasonData.message = null; + await patchReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'You must provide a reason.', + errorCode: 6, + }), + ); + }); + test('Ensure patchReason returns 404 when error in finding user Id', async () => { + const mockFindUser = jest.spyOn(UserModel, 'findById').mockImplementationOnce(() => null); + + await patchReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'User not found', + errorCode: 2, + }), + ); + }); + test('Ensure patchReason returns 404 when error in finding reason', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValueOnce(); + await patchReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + }); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Reason not found', + errorCode: 4, + }), + ); + }); + test('Ensure patchReason returns 400 when any error in saving.', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + const oldReason = { + reason: 'old message', + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + save: jest.fn().mockRejectedValueOnce(), + }; + emailSender.mockImplementation(() => { + throw new Error('Failed to send email'); + }); + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValueOnce(oldReason); + await patchReason(mockReq, mockRes); + await flushPromises(); + emailSender.mockRejectedValue(new Error('Failed')); + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + }); + expect(oldReason.save).toHaveBeenCalledWith(); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'something went wrong while patching the reason', + }), + ); + }); + test('Ensure patchReason returns 200 when any error in saving.', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + const oldReason = { + reason: mockReq.body.reasonData.message, + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + save: jest.fn().mockResolvedValueOnce(), + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValueOnce(oldReason); + emailSender.mockImplementation(() => { + Promise.resolve(); + }); + await patchReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + }); + expect(oldReason.save).toHaveBeenCalledWith(); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Reason Updated!', + }), + ); + }); + }); + describe('deleteReason method', () => { + test('Ensure deleteReason return 403 when no permission to delete', async () => { + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + ...mockReq.requestor, + requestor: { + role: 'Volunteer', + }, + }, + }; + await deleteReason(newMockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(403); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'You must be an Owner or Administrator to schedule a reason for a Blue Square', + + errorCode: 1, + }), + ); + }); + test('Ensure deleteReason return 404 when not finding user by ID', async () => { + const mockFindUser = jest.spyOn(UserModel, 'findById').mockImplementationOnce(() => null); + await deleteReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'User not found', + errorCode: 2, + }), + ); + }); + test('Ensure deleteReason returns 404 when error in finding reason', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValueOnce(); + await deleteReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + }); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Reason not found', + errorCode: 4, + }), + ); + }); + test('Ensure deleteReason returns 500 when error in removing reason', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + const foundReason = { + reason: mockReq.body.reasonData.message, + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + remove: jest.fn((cb) => cb(true)), + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockReturnValueOnce(foundReason); + + await deleteReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(500); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + }); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Error while deleting document', + errorCode: 5, + }), + ); + }); + test('Ensure deleteReason returns 200 if delete reason successfully.', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + const foundReason = { + reason: mockReq.body.reasonData.message, + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + remove: jest.fn((cb) => cb(false)), + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockReturnValueOnce(foundReason); + + await deleteReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + }); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Document deleted', + }), + ); + }); + }); +}); diff --git a/src/controllers/teamController.js b/src/controllers/teamController.js index 41f515e99..f1fd2241d 100644 --- a/src/controllers/teamController.js +++ b/src/controllers/teamController.js @@ -110,14 +110,15 @@ const teamcontroller = function (Team) { return; } - const canEditTeamCode = - req.body.requestor.role === 'Owner' || - req.body.requestor.permissions?.frontPermissions.includes('editTeamCode'); + // Removed the permission check as the permission check if done in earlier + // const canEditTeamCode = + // req.body.requestor.role === 'Owner' || + // req.body.requestor.permissions?.frontPermissions.includes('editTeamCode'); - if (!canEditTeamCode) { - res.status(403).send('You are not authorized to edit team code.'); - return; - } + // if (!canEditTeamCode) { + // res.status(403).send('You are not authorized to edit team code.'); + // return; + // } record.teamName = req.body.teamName; record.isActive = req.body.isActive; @@ -229,52 +230,53 @@ const teamcontroller = function (Team) { }); }; const updateTeamVisibility = async (req, res) => { - console.log("==============> 9 "); + console.log('==============> 9 '); const { visibility, teamId, userId } = req.body; - + try { Team.findById(teamId, (error, team) => { if (error || team === null) { res.status(400).send('No valid records found'); return; } - - const memberIndex = team.members.findIndex(member => member.userId.toString() === userId); + + const memberIndex = team.members.findIndex((member) => member.userId.toString() === userId); if (memberIndex === -1) { res.status(400).send('Member not found in the team.'); return; } - + team.members[memberIndex].visible = visibility; team.modifiedDatetime = Date.now(); - - team.save() - .then(updatedTeam => { - // Additional operations after team.save() + + team + .save() + .then((updatedTeam) => { + // Additional operations after team.save() const assignlist = []; const unassignlist = []; - team.members.forEach(member => { + team.members.forEach((member) => { if (member.userId.toString() === userId) { // Current user, no need to process further return; } - + if (visibility) { assignlist.push(member.userId); } else { - console.log("Visiblity set to false so removing it"); + console.log('Visiblity set to false so removing it'); unassignlist.push(member.userId); } }); - + const addTeamToUserProfile = userProfile .updateMany({ _id: { $in: assignlist } }, { $addToSet: { teams: teamId } }) .exec(); const removeTeamFromUserProfile = userProfile .updateMany({ _id: { $in: unassignlist } }, { $pull: { teams: teamId } }) .exec(); - + Promise.all([addTeamToUserProfile, removeTeamFromUserProfile]) .then(() => { res.status(200).send({ result: 'Done' }); @@ -283,18 +285,17 @@ const teamcontroller = function (Team) { res.status(500).send({ error }); }); }) - .catch(errors => { + .catch((errors) => { console.error('Error saving team:', errors); res.status(400).send(errors); }); - }); } catch (error) { - res.status(500).send(`Error updating team visibility: ${ error.message}`); + res.status(500).send(`Error updating team visibility: ${error.message}`); } }; - /** + /** * Leaner version of the teamcontroller.getAllTeams * Remove redundant data: members, isActive, createdDatetime, modifiedDatetime. */ @@ -308,7 +309,7 @@ const teamcontroller = function (Team) { res.status(500).send('Fetch team code failed.'); }); }; - + return { getAllTeams, getAllTeamCode, diff --git a/src/controllers/titleController.js b/src/controllers/titleController.js index 786f51631..1240d3373 100644 --- a/src/controllers/titleController.js +++ b/src/controllers/titleController.js @@ -4,6 +4,7 @@ const cacheClosure = require('../utilities/nodeCache'); const userProfileController = require("./userProfileController"); const userProfile = require('../models/userProfile'); const project = require('../models/project'); + const controller = userProfileController(userProfile, project); const getAllTeamCodeHelper = controller.getAllTeamCodeHelper; diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index a34956e16..15f36f751 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -1171,12 +1171,15 @@ const userProfileController = function (UserProfile, Project) { const { endDate } = req.body; const isSet = req.body.isSet === 'FinalDay'; let activeStatus = status; + let emailThreeWeeksSent = false; if (endDate && status) { const dateObject = new Date(endDate); dateObject.setHours(dateObject.getHours() + 7); const setEndDate = dateObject; if (moment().isAfter(moment(setEndDate).add(1, 'days'))) { activeStatus = false; + }else if(moment().isBefore(moment(endDate).subtract(3, 'weeks'))){ + emailThreeWeeksSent = true; } } if (!mongoose.Types.ObjectId.isValid(userId)) { @@ -1224,13 +1227,14 @@ const userProfileController = function (UserProfile, Project) { logger.logException(err, 'Unexpected error in finding menagement team'); } - UserProfile.findById(userId, 'isActive email firstName lastName') + UserProfile.findById(userId, 'isActive email firstName lastName finalEmailThreeWeeksSent') .then((user) => { user.set({ isActive: activeStatus, reactivationDate: activationDate, endDate, isSet, + finalEmailThreeWeeksSent: emailThreeWeeksSent, }); user .save() @@ -1255,6 +1259,7 @@ const userProfileController = function (UserProfile, Project) { recipients, isSet, activationDate, + emailThreeWeeksSent, ); auditIfProtectedAccountUpdated( req.body.requestor.requestorId, diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index ba7d64bc5..f0c9e9a03 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -2038,49 +2038,77 @@ const userHelper = function () { recipients, isSet, reactivationDate, + sendThreeWeeks, + followup, ) { + let subject; + let emailBody; + recipients.push('onecommunityglobal@gmail.com'); + recipients = recipients.toString(); if (reactivationDate) { - const subject = `IMPORTANT: ${firstName} ${lastName} has been PAUSED in the Highest Good Network`; - const emailBody = `
Management,
+ subject = `IMPORTANT: ${firstName} ${lastName} has been PAUSED in the Highest Good Network`; + emailBody = `Management,
-Please note that ${firstName} ${lastName} has been PAUSED in the Highest Good Network as of ${moment(endDate).format('M-D-YYYY')}.
+Please note that ${firstName} ${lastName} has been PAUSED in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.
For a smooth transition, Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part until they return on ${moment(reactivationDate).format('M-D-YYYY')}.
With Gratitude,
One Community
`; - recipients.push('onecommunityglobal@gmail.com'); - recipients = recipients.toString(); - emailSender(recipients, subject, emailBody, null, null, email); - } else if (endDate && isSet) { + emailSender(email, subject, emailBody, null, recipients, email); + } else if (endDate && isSet && sendThreeWeeks) { const subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; const emailBody = `Management,
-Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as of ${moment(endDate).format('M-D-YYYY')}.
-For a smooth transition, please confirm all your work is being wrapped up with this individual and nothing further will be needed on their part after this date.
+Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.
+This is more than 3 weeks from now, but you should still start confirming all your work is being wrapped up with this individual and nothing further will be needed on their part after this date.
+ +An additional reminder email will be sent in their final 2 weeks.
With Gratitude,
One Community
`; - recipients.push('onecommunityglobal@gmail.com'); - recipients = recipients.toString(); - emailSender(recipients, subject, emailBody, null, null, email); - } else { - const subject = `IMPORTANT: ${firstName} ${lastName} has been deactivated in the Highest Good Network`; - const emailBody = `Management,
+ emailSender(email, subject, emailBody, null, recipients, email); + + } else if (endDate && isSet && followup) { + subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; + emailBody = `Management,
-Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as of ${moment(endDate).format('M-D-YYYY')}.
+Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.
+This is coming up soon. For a smooth transition, please confirm all your work is wrapped up with this individual and nothing further will be needed on their part after this date.
+ +With Gratitude,
+ +One Community
`; + emailSender(email, subject, emailBody, null, recipients, email); + + } else if (endDate && isSet ) { + subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; + emailBody = `Management,
+ +Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.
+For a smooth transition, Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.
+ +With Gratitude,
+ +One Community
`; + emailSender(email, subject, emailBody, null, recipients, email); + + } else if(endDate){ + subject = `IMPORTANT: ${firstName} ${lastName} has been deactivated in the Highest Good Network`; + emailBody = `Management,
+ +Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.
For a smooth transition, Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.
With Gratitude,
One Community
`; - recipients.push('onecommunityglobal@gmail.com'); - recipients = recipients.toString(); - emailSender(recipients, subject, emailBody, null, null, email); + emailSender(email, subject, emailBody, null, recipients, email); + }; + } - }; - + const deActivateUser = async () => { try { const emailReceivers = await userProfile.find( @@ -2090,13 +2118,38 @@ const userHelper = function () { const recipients = emailReceivers.map((receiver) => receiver.email); const users = await userProfile.find( { isActive: true, endDate: { $exists: true } }, - '_id isActive endDate isSet reactivationDate', + '_id isActive endDate isSet finalEmailThreeWeeksSent reactivationDate', ); for (let i = 0; i < users.length; i += 1) { const user = users[i]; - const { endDate } = user; + const { endDate, finalEmailThreeWeeksSent } = user; endDate.setHours(endDate.getHours() + 7); - if (moment().isAfter(moment(endDate).add(1, 'days'))) { + // notify reminder set final day before 2 weeks + if(finalEmailThreeWeeksSent && moment().isBefore(moment(endDate).subtract(2, 'weeks')) && moment().isAfter(moment(endDate).subtract(3, 'weeks'))){ + const id = user._id; + const person = await userProfile.findById(id); + const lastDay = moment(person.endDate).format('YYYY-MM-DD'); + logger.logInfo(`User with id: ${user._id}'s final Day is set at ${moment().format()}.`); + person.teams.map(async (teamId) => { + const managementEmails = await userHelper.getTeamManagementEmail(teamId); + if (Array.isArray(managementEmails) && managementEmails.length > 0) { + managementEmails.forEach((management) => { + recipients.push(management.email); + }); + } + }); + sendDeactivateEmailBody( + person.firstName, + person.lastName, + lastDay, + person.email, + recipients, + person.isSet, + person.reactivationDate, + false, + true, + ); + } else if (moment().isAfter(moment(endDate).add(1, 'days'))) { try { await userProfile.findByIdAndUpdate( user._id, @@ -2130,6 +2183,7 @@ const userHelper = function () { recipients, person.isSet, person.reactivationDate, + undefined, ); } } diff --git a/src/models/userProfile.js b/src/models/userProfile.js index cc7136f54..3a529294a 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -27,6 +27,7 @@ const userProfileSchema = new Schema({ isActive: { type: Boolean, required: true, default: true }, isRehireable: { type: Boolean, default: true }, isSet: { type: Boolean, required: true, default: false }, + finalEmailThreeWeeksSent: { type: Boolean, required: true, default: false }, role: { type: String, required: true, diff --git a/src/routes/informationRouter.test.js b/src/routes/informationRouter.test.js new file mode 100644 index 000000000..12a600723 --- /dev/null +++ b/src/routes/informationRouter.test.js @@ -0,0 +1,145 @@ +const request = require('supertest'); +const { jwtPayload } = require('../test'); +const cache = require('../utilities/nodeCache')(); +const { app } = require('../app'); +const { + mockReq, + createUser, + mongoHelper: { dbConnect, dbDisconnect, dbClearCollections, dbClearAll }, +} = require('../test'); + +const agent = request.agent(app); + +describe('information routes', () => { + let user; + let token; + let reqBody = { + ...mockReq.body, + }; + beforeAll(async () => { + await dbConnect(); + user = await createUser(); + token = jwtPayload(user); + reqBody = { + ...reqBody, + infoName: 'some infoName', + infoContent: 'some infoContent', + visibility: '1', + }; + }); + beforeEach(async () => { + await dbClearCollections('informations'); + }); + + afterAll(async () => { + await dbClearAll(); + await dbDisconnect(); + }); + describe('informationRoutes', () => { + it('should return 401 if authorization header is not present', async () => { + await agent.post('/api/informations').send(reqBody).expect(401); + await agent.get('/api/informations/randomID').send(reqBody).expect(401); + }); + }); + describe('Post Information route', () => { + it('Should return 201 if the information is successfully added', async () => { + const response = await agent + .post('/api/informations') + .send(reqBody) + .set('Authorization', token) + .expect(201); + + expect(response.body).toEqual({ + _id: expect.anything(), + __v: expect.anything(), + infoName: reqBody.infoName, + infoContent: reqBody.infoContent, + visibility: reqBody.visibility, + }); + }); + }); + describe('Get Information route', () => { + it('Should return 201 if the information is successfully added', async () => { + const informations = [ + { + _id: '6605f860f948db61dab6f27m', + infoName: 'get info', + infoContent: 'get infoConten', + visibility: '1', + }, + ]; + cache.setCache('informations', JSON.stringify(informations)); + const response = await agent + .get('/api/informations') + .send(reqBody) + .set('Authorization', token) + .expect(200); + expect(response.body).toEqual({}); + }); + }); + describe('Delete Information route', () => { + it('Should return 400 if the route does not exist', async () => { + await agent + .delete('/api/informations/random123') + .send(reqBody) + .set('Authorization', token) + .expect(400); + }); + // thrown: "Exceeded timeout of 5000 ms for a test. + // Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout." + // it('Should return 200 if deleting successfully', async () => { + // const _info = new Information(); + // _info.infoName = reqBody.infoName; + // _info.infoContent = reqBody.infoContent; + // _info.visibility = reqBody.visibility; + // const info = await _info.save(); + // const response = await agent + // .delete(`/api/informations/${info._id}`) + // .set('Authorization', token) + // .send(reqBody) + // .expect(200); + + // expect(response.body).toEqual( + // { + // _id: expect.anything(), + // __v: expect.anything(), + // infoName: info.infoName, + // infoContent: info.infoContent, + // visibility: info.visibility, + // }); + // }); + }); + describe('Update Information route', () => { + it('Should return 400 if the route does not exist', async () => { + await agent + .put('/api/informations/random123') + .send(reqBody) + .set('Authorization', token) + .expect(400); + }); + // thrown: "Exceeded timeout of 5000 ms for a test. + // Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout." + // it('Should return 200 if udapted successfully', async () => { + // const _info = new Information(); + // _info.infoName = reqBody.infoName; + // _info.infoContent = reqBody.infoContent; + // _info.visibility = reqBody.visibility; + // const info = await _info.save(); + + // const response = await agent + // .put(`/api/informations/${info.id}`) + // .send(reqBody) + // .set('Authorization', token) + // .expect(200); + // expect(response.body).toEqual( + // { + // _id: expect.anything(), + // __v: expect.anything(), + // infoName: info.infoName, + // infoContent: info.infoContent, + // visibility: info.visibility, + // }); + + // }); + }); +}); diff --git a/src/routes/reasonRouter.test.js b/src/routes/reasonRouter.test.js new file mode 100644 index 000000000..a1f1ab6dc --- /dev/null +++ b/src/routes/reasonRouter.test.js @@ -0,0 +1,338 @@ +const request = require('supertest'); +const moment = require('moment-timezone'); +const { jwtPayload } = require('../test'); +const cache = require('../utilities/nodeCache')(); +const { app } = require('../app'); +const { + mockReq, + mockUser, + createUser, + createTestPermissions, + mongoHelper: { dbConnect, dbDisconnect, dbClearCollections, dbClearAll }, +} = require('../test'); +// const Reason = require('../models/reason'); + +function mockDay(dayIdx, past = false) { + const date = moment().tz('America/Los_Angeles').startOf('day'); + while (date.day() !== dayIdx) { + date.add(past ? -1 : 1, 'days'); + } + return date; +} +const agent = request.agent(app); +describe('reason routers', () => { + let adminUser; + let adminToken; + let reqBody = { + body: { + ...mockReq.body, + ...mockUser(), + }, + }; + beforeAll(async () => { + await dbConnect(); + await createTestPermissions(); + adminUser = await createUser(); + adminToken = jwtPayload(adminUser); + }); + beforeEach(async () => { + await dbClearCollections('reason'); + await dbClearCollections('userProfiles'); + cache.setCache('allusers', '[]'); + reqBody = { + body: { + ...mockReq.body, + ...mockUser(), + reasonData: { + date: mockDay(0), + message: 'some reason', + }, + currentDate: moment.tz('America/Los_Angeles').startOf('day'), + }, + }; + }); + afterAll(async () => { + await dbClearAll(); + await dbDisconnect(); + }); + describe('reasonRouters', () => { + it('should return 401 if authorization header is not present', async () => { + await agent.post('/api/reason/').send(reqBody.body).expect(401); + await agent.get('/api/reason/randomId').send(reqBody.body).expect(401); + await agent.get('/api/reason/single/randomId').send(reqBody.body).expect(401); + await agent.patch('/api/reason/randomId/').send(reqBody.body).expect(401); + await agent.delete('/api/reason/randomId').send(reqBody.body).expect(401); + }); + }); + describe('Post reason route', () => { + it('Should return 400 if user did not choose SUNDAY', async () => { + reqBody.body.reasonData.date = mockDay(1, true); + const response = await agent + .post('/api/reason/') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(400); + expect(response.body).toEqual({ + message: + "You must choose the Sunday YOU'LL RETURN as your date. This is so your reason ends up as a note on that blue square.", + errorCode: 0, + }); + }); + it('Should return 400 if warning to choose a future date', async () => { + reqBody.body.reasonData.date = mockDay(0, true); + const response = await agent + .post('/api/reason/') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(400); + expect(response.body).toEqual({ + message: 'You should select a date that is yet to come', + errorCode: 7, + }); + }); + it('Should return 400 if not providing reason', async () => { + reqBody.body.reasonData.message = null; + const response = await agent + .post('/api/reason/') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(400); + expect(response.body).toEqual({ + message: 'You must provide a reason.', + errorCode: 6, + }); + }); + it('Should return 404 if error in finding user Id', async () => { + reqBody.body.userId = null; + const response = await agent + .post('/api/reason/') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(404); + expect(response.body).toEqual({ + message: 'User not found', + errorCode: 2, + }); + }); + it('Should return 403 if duplicate resonse', async () => { + // const userProfile = new userPro + let response = await agent + .post('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toBeTruthy(); + response = await agent + .get('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + const userId = response.body[0]._id; + reqBody.body.userId = userId; + response = await agent + .post('/api/reason/') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + expect(response.body).toBeTruthy(); + response = await agent + .post('/api/reason/') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(403); + }); + it('Should return 200 if post successfully', async () => { + let response = await agent + .post('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toBeTruthy(); + response = await agent + .get('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + const userId = response.body[0]._id; + reqBody.body.userId = userId; + response = await agent + .post('/api/reason/') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + }); + }); + describe('Get AllReason route', () => { + it('Should return 400 if route does not exist', async () => { + const response = await agent + .get(`/api/reason/random123`) + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(400); + expect(response.body).toEqual({ + errMessage: 'Something went wrong while fetching the user', + }); + }); + it('Should return 200 if get all reasons', async () => { + let response = await agent + .post('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toBeTruthy(); + response = await agent + .get('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + const userId = response.body[0]._id; + reqBody.body.userId = userId; + response = await agent + .get(`/api/reason/${userId}`) + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + }); + }); + describe('Get Single Reason route', () => { + it('Should return 400 if route does not exist', async () => { + reqBody.query = { + queryDate: mockDay(1, true), + }; + const response = await agent + .get(`/api/reason/single/5a7e21f00317bc1538def4b9`) + .set('Authorization', adminToken) + .expect(404); + expect(response.body).toEqual({ + message: 'User not found', + errorCode: 2, + }); + }); + it('Should return 200 if get all reasons', async () => { + let response = await agent + .post('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toBeTruthy(); + response = await agent.get('/api/userProfile').set('Authorization', adminToken).expect(200); + const userId = response.body[0]._id; + reqBody.body.userId = userId; + reqBody.query = { + queryDate: mockDay(1, true), + }; + response = await agent + .get(`/api/reason/single/${userId}`) + .set('Authorization', adminToken) + .expect(200); + }); + }); + describe('Patch reason route', () => { + it('Should return 404 if error in finding user Id', async () => { + reqBody.body.userId = null; + const response = await agent + .patch('/api/reason/5a7e21f00317bc1538def4b9/') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(404); + expect(response.body).toEqual({ + message: 'User not found', + errorCode: 2, + }); + }); + it('Should return 404 if duplicate reasons', async () => { + let response = await agent + .post('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toBeTruthy(); + response = await agent.get('/api/userProfile').set('Authorization', adminToken).expect(200); + const userId = response.body[0]._id; + reqBody.body.userId = userId; + response = await agent + .patch(`/api/reason/${userId}/`) + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(404); + expect(response.body).toEqual({ + message: 'Reason not found', + errorCode: 4, + }); + }); + it('Should return 200 if patch successfully', async () => { + let response = await agent + .post('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toBeTruthy(); + response = await agent.get('/api/userProfile').set('Authorization', adminToken).expect(200); + const userId = response.body[0]._id; + reqBody.body.userId = userId; + response = await agent + .post(`/api/reason/`) + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + expect(response.body).toBeTruthy(); + response = await agent + .patch(`/api/reason/${userId}/`) + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + expect(response.body).toEqual({ + message: 'Reason Updated!', + }); + }); + }); + describe('Delete reason route', () => { + it('Should return 404 if route does not exist', async () => { + const response = await agent + .delete(`/api/reason/5a7e21f00317bc1538def4b9`) + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(404); + expect(response.body).toEqual({ + message: 'User not found', + errorCode: 2, + }); + }); + it('Should return 200 if deleting successfully', async () => { + let response = await agent + .post('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toBeTruthy(); + response = await agent + .get('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + const userId = response.body[0]._id; + reqBody.body.userId = userId; + response = await agent + .post(`/api/reason/`) + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + expect(response.body).toBeTruthy(); + response = await agent + .delete(`/api/reason/${userId}`) + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + expect(response.body).toEqual({ + message: 'Document deleted', + }); + }); + }); +});