diff --git a/requirements/inventoryController/getAllInvInProject.md b/requirements/inventoryController/getAllInvInProject.md new file mode 100644 index 000000000..9a331a9ab --- /dev/null +++ b/requirements/inventoryController/getAllInvInProject.md @@ -0,0 +1,17 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ❌ Receives a POST request in the **/api/userProfile** route +2. ❌ Returns 200 if successfully fetch inventory data + + > ## Negative case + +3. ❌ Returns error 404 if the API does not exist +4. ❌ Returns error code 403 if the user is not authorized to view the inventory data +5. ❌ Returns error code 404 if an error occurs when populating or saving. + +> ## Edge case \ No newline at end of file diff --git a/requirements/inventoryController/getAllInvInProjectWBS.md b/requirements/inventoryController/getAllInvInProjectWBS.md new file mode 100644 index 000000000..f2d7a004d --- /dev/null +++ b/requirements/inventoryController/getAllInvInProjectWBS.md @@ -0,0 +1,17 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ❌ Receives a POST request in the **/api/userProfile** route +2. ✅ Returns 200 if successfully found data + +> ## Negative case + +1. ❌ Returns error 404 if the API does not exist +2. ✅ Returns 403 if user is not authorized to view inventory data +3. ✅ Returns 404 if an error occurs while fetching data + +> ## Edge case \ No newline at end of file diff --git a/requirements/inventoryController/postInvInProjectWBS.md b/requirements/inventoryController/postInvInProjectWBS.md new file mode 100644 index 000000000..3ac80540b --- /dev/null +++ b/requirements/inventoryController/postInvInProjectWBS.md @@ -0,0 +1,19 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ❌ Receives a POST request in the **/api/userProfile** route +2. ✅ Returns status code 201, if the inventory was successfully created and saved +3. ✅ Returns status code 201, if the inventory item was succesfully updated and saved. + +> ## Negative case + +1. ❌ Returns error 404 if the API does not exist +2. ✅ Returns error 403 if the user is not authorized to view data +3. ✅ Returns error 500 if an error occurs when saving +4. ✅ Returns error 400 if a valid project was found but quantity and type id were missing + +> ## Edge case \ No newline at end of file diff --git a/src/controllers/inventoryController.js b/src/controllers/inventoryController.js index d126cc7e4..76b4c899f 100644 --- a/src/controllers/inventoryController.js +++ b/src/controllers/inventoryController.js @@ -13,7 +13,7 @@ const inventoryController = function (Item, ItemType) { // use req.params.projectId and wbsId // Run a mongo query on the Item model to find all items with both the project and wbs // sort the mongo query so that the Wasted false items are listed first - return Item.find({ + await Item.find({ project: mongoose.Types.ObjectId(req.params.projectId), wbs: req.params.wbsId && req.params.wbsId !== 'Unassigned' @@ -283,9 +283,9 @@ const inventoryController = function (Item, ItemType) { } // update the original item by decreasing by the quantity and adding a note - if (req.body.quantity && req.params.invId && projectExists && wbsExists) { + if (req.body.quantity && req.param.invId && projectExists && wbsExists) { return Item.findByIdAndUpdate( - req.params.invId, + req.param.invId, { $decr: { quantity: req.body.quantity }, $push: { @@ -797,4 +797,4 @@ const inventoryController = function (Item, ItemType) { }; }; -module.exports = inventoryController; +module.exports = inventoryController; \ No newline at end of file diff --git a/src/controllers/inventoryController.spec.js b/src/controllers/inventoryController.spec.js new file mode 100644 index 000000000..bc4c4277a --- /dev/null +++ b/src/controllers/inventoryController.spec.js @@ -0,0 +1,334 @@ +/* eslint-disable new-cap */ + +jest.mock('../utilities/permissions', () => ({ + hasPermission: jest.fn(), // Mocking the hasPermission function + })); + const { mockReq, mockRes, assertResMock } = require('../test'); + + const inventoryItem = require('../models/inventoryItem'); + const inventoryItemType = require('../models/inventoryItemType'); + const inventoryController = require('./inventoryController'); + const projects = require('../models/project'); + const wbs = require('../models/wbs'); + + const { hasPermission } = require('../utilities/permissions'); + + const makeSut = () => { + const { getAllInvInProjectWBS, postInvInProjectWBS, getAllInvInProject } = inventoryController( + inventoryItem, + inventoryItemType, + ); + return { getAllInvInProjectWBS, postInvInProjectWBS, getAllInvInProject }; + }; + + const flushPromises = () => new Promise(setImmediate); + + describe('Unit test for inventoryController', () => { + beforeAll(() => { + jest.clearAllMocks(); + }); + beforeEach(() => { + mockReq.params.userid = '5a7e21f00317bc1538def4b7'; + mockReq.params.userId = '5a7e21f00317bc1538def4b7'; + mockReq.params.wbsId = '5a7e21f00317bc1538def4b7'; + mockReq.params.projectId = '5a7e21f00317bc1538def4b7'; + mockReq.body = { + project: '5a7e21f00317bc1538def4b7', + wbs: '5a7e21f00317bc1538def4b7', + itemType: '5a7e21f00317bc1538def4b7', + item: '5a7e21f00317bc1538def4b7', + quantity: 1, + typeId: '5a7e21f00317bc1538def4b7', + cost: 20, + poNum: '123', + }; + }); + afterEach(() => { + jest.clearAllMocks(); + }); + describe('getAllInvInProjectWBS', () => { + test('Returns 403 if user is not authorized to view inventory data', async () => { + const { getAllInvInProjectWBS } = makeSut(); + hasPermission.mockResolvedValue(false); + const response = await getAllInvInProjectWBS(mockReq, mockRes); + assertResMock(403, 'You are not authorized to view inventory data.', response, mockRes); + expect(hasPermission).toHaveBeenCalledTimes(1); + }); + + test('Returns 404 if an error occurs while fetching inventory data', async () => { + const { getAllInvInProjectWBS } = makeSut(); + // Mocking hasPermission function + hasPermission.mockResolvedValue(true); + + // Mock error + const error = new Error('Error fetching inventory data'); + + // Mock chainable methods: populate, sort, then, catch + const mockInventoryItem = { + populate: jest.fn().mockReturnThis(), + sort: jest.fn().mockReturnThis(), + then: jest.fn().mockImplementationOnce(() => Promise.reject(error)), + catch: jest.fn().mockReturnThis(), + }; + + // Mock the inventoryItem.find method + jest.spyOn(inventoryItem, 'find').mockImplementationOnce(() => mockInventoryItem); + + // Call the function + const response = await getAllInvInProjectWBS(mockReq, mockRes); + await flushPromises(); + + // Assertions + expect(hasPermission).toHaveBeenCalledTimes(1); + assertResMock(404, error, response, mockRes); + }); + + test('Returns 200 if successfully found data', async () => { + const { getAllInvInProjectWBS } = makeSut(); + hasPermission.mockResolvedValue(true); + + const mockData = [ + { + _id: '123', + project: '123', + wbs: '123', + itemType: '123', + item: '123', + quantity: 1, + date: new Date().toISOString(), + }, + ]; + + const mockInventoryItem = { + populate: jest.fn().mockReturnThis(), + sort: jest.fn().mockResolvedValue(mockData), + then: jest.fn().mockResolvedValue(() => {}), + catch: jest.fn().mockReturnThis(), + }; + + // Mock the inventoryItem.find method + jest.spyOn(inventoryItem, 'find').mockImplementation(() => mockInventoryItem); + + // Call the function + const response = await getAllInvInProjectWBS(mockReq, mockRes); + await flushPromises(); + + // Assertions + expect(hasPermission).toHaveBeenCalledTimes(1); + assertResMock(200, mockData, response, mockRes); + }); + }); + describe('postInvInProjectWBS', () => { + test('Returns error 403 if the user is not authorized to view data', async () => { + const { getAllInvInProjectWBS } = makeSut(); + hasPermission.mockReturnValue(false); + const response = await getAllInvInProjectWBS(mockReq, mockRes); + assertResMock(403, 'You are not authorized to view inventory data.', response, mockRes); + expect(hasPermission).toHaveBeenCalledTimes(1); + }); + + test('Returns error 400 if an error occurs while fetching an item', async () => { + mockReq.params.wbsId = 'Unassigned'; + const { postInvInProjectWBS } = makeSut(); + hasPermission.mockReturnValue(true); + // look up difference betewewen mockimplmenonce and mockimplementation + // how to incorpoate into the test + // and how to setup mocking variables as well + const mockProjectExists = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnValue(null), + }; + + jest.spyOn(projects, 'findOne').mockImplementationOnce(() => mockProjectExists); + + const response = await postInvInProjectWBS(mockReq, mockRes); + await flushPromises(); + + expect(hasPermission).toHaveBeenCalledTimes(1); + assertResMock( + 400, + 'Valid Project, Quantity and Type Id are necessary as well as valid wbs if sent in and not Unassigned', + response, + mockRes, + ); + }); + test('Returns error 500 if an error occurs when saving', async () => { + const mockProjectExists = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + }; + const mockWbsExists = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + }; + const mockInventoryItem = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnValue(null), + }; + const { postInvInProjectWBS } = makeSut(); + // const hasPermissionSpy = mockHasPermission(true); + hasPermission.mockReturnValue(true); + + jest.spyOn(projects, 'findOne').mockImplementationOnce(() => mockProjectExists); + jest.spyOn(wbs, 'findOne').mockImplementationOnce(() => mockWbsExists); + jest.spyOn(inventoryItem, 'findOne').mockImplementationOnce(() => mockInventoryItem); + + jest.spyOn(inventoryItem.prototype, 'save').mockRejectedValueOnce(new Error('Error saving')); + const response = await postInvInProjectWBS(mockReq, mockRes); + await flushPromises(); + expect(hasPermission).toHaveBeenCalledTimes(1); + assertResMock(500, new Error('Error saving'), response, mockRes); + }); + + test('Receives a 201 success if the inventory was successfully created and saved', async () => { + const resolvedInventoryItem = new inventoryItem({ + project: mockReq.body.projectId, + wbs: mockReq.body.wbsId, + type: mockReq.body.typeId, + quantity: mockReq.body.quantity, + cost: mockReq.body.cost, + poNum: mockReq.body.poNum, + }); + const mockProjectExists = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + }; + const mockWbsExists = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + }; + const mockInventoryItem = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnValue(null), + }; + const { postInvInProjectWBS } = makeSut(); + + hasPermission.mockReturnValue(true); + jest.spyOn(projects, 'findOne').mockImplementationOnce(() => mockProjectExists); + jest.spyOn(wbs, 'findOne').mockImplementationOnce(() => mockWbsExists); + jest.spyOn(inventoryItem, 'findOne').mockImplementationOnce(() => mockInventoryItem); + jest + .spyOn(inventoryItem.prototype, 'save') + .mockImplementationOnce(() => Promise.resolve(resolvedInventoryItem)); + + const response = await postInvInProjectWBS(mockReq, mockRes); + await flushPromises(); + expect(hasPermission).toHaveBeenCalledTimes(1); + assertResMock(201, resolvedInventoryItem, response, mockRes); + }); + + test('Returns a 201, if the inventory item was succesfully updated and saved.', async () => { + const resolvedInventoryItem = { + project: mockReq.body.projectId, + wbs: mockReq.body.wbsId, + type: mockReq.body.typeId, + quantity: mockReq.body.quantity, + cost: mockReq.body.cost, + poNum: mockReq.body.poNum, + }; + + const updatedResolvedInventoryItem = { + project: mockReq.body.projectId, + wbs: mockReq.body.wbsId, + type: mockReq.body.typeId, + quantity: mockReq.body.quantity + 1, + costPer: 200, + }; + + const mockProjectExists = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + }; + const mockWbsExists = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + }; + const mockInventoryExists = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + }; + + const { postInvInProjectWBS } = makeSut(); + hasPermission.mockReturnValue(true); + + jest.spyOn(projects, 'findOne').mockImplementationOnce(() => mockProjectExists); + jest.spyOn(wbs, 'findOne').mockImplementationOnce(() => mockWbsExists); + jest.spyOn(inventoryItem, 'findOne').mockImplementationOnce(() => mockInventoryExists); + jest + .spyOn(inventoryItem, 'findOneAndUpdate') + .mockImplementationOnce(() => Promise.resolve(resolvedInventoryItem)); + + jest + .spyOn(inventoryItem, 'findByIdAndUpdate') + .mockImplementationOnce(() => Promise.resolve(updatedResolvedInventoryItem)); + + const response = await postInvInProjectWBS(mockReq, mockRes); + await flushPromises(); + expect(hasPermission).toHaveBeenCalledTimes(1); + assertResMock(201, updatedResolvedInventoryItem, response, mockRes); + }); + }); + + describe('getAllInvInProject', () => { + test('Returns 403 if user is not authorized to view inventory data', async () => { + const { getAllInvInProject } = makeSut(); + hasPermission.mockResolvedValue(false); + const response = await getAllInvInProject(mockReq, mockRes); + assertResMock(403, 'You are not authorized to view inventory data.', response, mockRes); + expect(hasPermission).toHaveBeenCalledTimes(1); + }); + + test('Returns 404 if an error occurs while fetching inventory data', async () => { + const { getAllInvInProject } = makeSut(); + hasPermission.mockResolvedValue(true); + + const error = new Error('Error fetching inventory data'); + + const mockInventoryItem = { + populate: jest.fn().mockReturnThis(), + sort: jest.fn().mockReturnThis(), + then: jest.fn().mockImplementationOnce(() => Promise.reject(error)), + catch: jest.fn().mockReturnThis(), + }; + + jest.spyOn(inventoryItem, 'find').mockImplementationOnce(() => mockInventoryItem); + + const response = await getAllInvInProject(mockReq, mockRes); + await flushPromises(); + + expect(hasPermission).toHaveBeenCalledTimes(1); + assertResMock(404, error, response, mockRes); + }); + + test('Returns 200 if successfully found data', async () => { + const { getAllInvInProject } = makeSut(); + hasPermission.mockResolvedValue(true); + + const mockData = [ + { + _id: '123', + project: '123', + wbs: '123', + itemType: '123', + item: '123', + quantity: 1, + date: new Date().toISOString(), + }, + ]; + + const mockInventoryItem = { + populate: jest.fn().mockReturnThis(), + sort: jest.fn().mockResolvedValue(mockData), + catch: jest.fn().mockReturnThis(), + }; + + jest.spyOn(inventoryItem, 'find').mockImplementation(() => mockInventoryItem); + + const response = await getAllInvInProject(mockReq, mockRes); + await flushPromises(); + + expect(hasPermission).toHaveBeenCalledTimes(1); + assertResMock(200, mockData, response, mockRes); + }); + }); + }); \ No newline at end of file diff --git a/src/controllers/mapLocationsController.js b/src/controllers/mapLocationsController.js index 9ba230e21..48e0e097d 100644 --- a/src/controllers/mapLocationsController.js +++ b/src/controllers/mapLocationsController.js @@ -1,14 +1,14 @@ +/* eslint-disable no-use-before-define */ const UserProfile = require('../models/userProfile'); -const cacheClosure = require('../utilities/nodeCache'); +const cache = require('../utilities/nodeCache')(); const mapLocationsController = function (MapLocation) { - const cache = cacheClosure(); const getAllLocations = async function (req, res) { try { const users = []; const results = await UserProfile.find( {}, - '_id firstName lastName isActive location jobTitle totalTangibleHrs hoursByCategory', + '_id firstName lastName isActive location jobTitle totalTangibleHrs hoursByCategory homeCountry', ); results.forEach((item) => { @@ -16,14 +16,13 @@ const mapLocationsController = function (MapLocation) { (item.location?.coords.lat && item.location?.coords.lng && item.totalTangibleHrs >= 10) || (item.location?.coords.lat && item.location?.coords.lng && - // eslint-disable-next-line no-use-before-define calculateTotalHours(item.hoursByCategory) >= 10) ) { users.push(item); } }); const modifiedUsers = users.map((item) => ({ - location: item.location, + location: item.homeCountry || item.location, isActive: item.isActive, jobTitle: item.jobTitle[0], _id: item._id, @@ -38,7 +37,7 @@ const mapLocationsController = function (MapLocation) { } }; const deleteLocation = async function (req, res) { - if (req.body.requestor.role !== 'Administrator' && req.body.requestor.role !== 'Owner') { + if (!req.body.requestor.role === 'Administrator' || !req.body.requestor.role === 'Owner') { res.status(403).send('You are not authorized to make changes in the teams.'); return; } @@ -49,11 +48,10 @@ const mapLocationsController = function (MapLocation) { .catch((error) => res.status(500).send({ message: error || "Couldn't remove the location" })); }; const putUserLocation = async function (req, res) { - if (req.body.requestor.role !== 'Owner') { + if (!req.body.requestor.role === 'Owner') { res.status(403).send('You are not authorized to make changes in the teams.'); return; } - const locationData = { firstName: req.body.firstName, lastName: req.body.lastName, @@ -70,11 +68,11 @@ const mapLocationsController = function (MapLocation) { res.status(200).send(response); } catch (err) { console.log(err.message); - res.status(500).send({ message: err.message || 'Something went wrong...' }); + res.status(500).json({ message: err.message || 'Something went wrong...' }); } }; const updateUserLocation = async function (req, res) { - if (req.body.requestor.role !== 'Owner') { + if (!req.body.requestor.role === 'Owner') { res.status(403).send('You are not authorized to make changes in the teams.'); return; } @@ -126,7 +124,7 @@ const mapLocationsController = function (MapLocation) { res.status(200).send(newData); } catch (err) { console.log(err.message); - res.status(500).send({ message: err.message || 'Something went wrong...' }); + res.status(500).json({ message: err.message || 'Something went wrong...' }); } }; diff --git a/src/controllers/mapLocationsController.spec.js b/src/controllers/mapLocationsController.spec.js deleted file mode 100644 index facb9cba2..000000000 --- a/src/controllers/mapLocationsController.spec.js +++ /dev/null @@ -1,367 +0,0 @@ -/// mock the cache function before importing so we can manipulate the implementation - -jest.mock('../utilities/nodeCache'); -const cache = require('../utilities/nodeCache'); -const MapLocation = require('../models/mapLocation'); -const UserProfile = require('../models/userProfile'); -const { mockReq, mockRes, assertResMock } = require('../test'); -const mapLocationsController = require('./mapLocationsController'); - -const makeSut = () => { - const { getAllLocations, deleteLocation, putUserLocation, updateUserLocation } = - mapLocationsController(MapLocation); - - return { getAllLocations, deleteLocation, putUserLocation, updateUserLocation }; -}; - -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('Map Locations Controller', () => { - beforeEach(() => { - mockReq.params.locationId = 'randomId'; - mockReq.body.firstName = 'Bob'; - mockReq.body.lastName = 'Bobberson'; - mockReq.body.jobTitle = 'Software Engineer'; - mockReq.body.location = { - userProvided: 'New York', - coords: { - lat: 12, - lng: 12, - }, - country: 'USA', - city: 'New York City', - }; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('getAllLocations method', () => { - test('Returns 404 if an error occurs when finding all users.', async () => { - const { getAllLocations } = makeSut(); - - const errMsg = 'Failed to find users!'; - const findSpy = jest.spyOn(UserProfile, 'find').mockRejectedValueOnce(new Error(errMsg)); - - const res = await getAllLocations(mockReq, mockRes); - - assertResMock(404, new Error(errMsg), res, mockRes); - expect(findSpy).toHaveBeenCalledWith( - {}, - '_id firstName lastName isActive location jobTitle totalTangibleHrs hoursByCategory', - ); - }); - - test('Returns 404 if an error occurs when finding all map locations.', async () => { - const { getAllLocations } = makeSut(); - - const errMsg = 'Failed to find locations!'; - const findSpy = jest.spyOn(UserProfile, 'find').mockResolvedValueOnce([]); - const findLocationSpy = jest - .spyOn(MapLocation, 'find') - .mockRejectedValueOnce(new Error(errMsg)); - - const res = await getAllLocations(mockReq, mockRes); - - assertResMock(404, new Error(errMsg), res, mockRes); - expect(findSpy).toHaveBeenCalledWith( - {}, - '_id firstName lastName isActive location jobTitle totalTangibleHrs hoursByCategory', - ); - expect(findLocationSpy).toHaveBeenCalledWith({}); - }); - - test('Returns 200 if all is successful', async () => { - const { getAllLocations } = makeSut(); - - const findRes = [ - { - _id: 1, - firstName: 'bob', - lastName: 'marley', - isActive: true, - location: { - coords: { - lat: 12, - lng: 12, - }, - country: 'USA', - city: 'NYC', - }, - jobTitle: ['software engineer'], - totalTangibleHrs: 11, - }, - ]; - const findSpy = jest.spyOn(UserProfile, 'find').mockResolvedValueOnce(findRes); - const findLocationSpy = jest.spyOn(MapLocation, 'find').mockResolvedValueOnce([]); - - const modifiedUsers = { - location: findRes[0].location, - isActive: findRes[0].isActive, - jobTitle: findRes[0].jobTitle[0], - _id: findRes[0]._id, - firstName: findRes[0].firstName, - lastName: findRes[0].lastName, - }; - const res = await getAllLocations(mockReq, mockRes); - - assertResMock(200, { users: [modifiedUsers], mUsers: [] }, res, mockRes); - expect(findSpy).toHaveBeenCalledWith( - {}, - '_id firstName lastName isActive location jobTitle totalTangibleHrs hoursByCategory', - ); - expect(findLocationSpy).toHaveBeenCalledWith({}); - }); - }); - - describe('deleteLocation method', () => { - test('Returns 403 if user is not authorized.', async () => { - mockReq.body.requestor.role = 'Volunteer'; - const { deleteLocation } = makeSut(); - const res = await deleteLocation(mockReq, mockRes); - assertResMock(403, 'You are not authorized to make changes in the teams.', res, mockRes); - }); - - test('Returns 500 if an error occurs when deleting the map location.', async () => { - mockReq.body.requestor.role = 'Owner'; - - const { deleteLocation } = makeSut(); - - const err = new Error('Failed to delete!'); - const deleteSpy = jest.spyOn(MapLocation, 'findOneAndDelete').mockRejectedValueOnce(err); - - const res = await deleteLocation(mockReq, mockRes); - await flushPromises(); - - assertResMock(500, { message: err }, res, mockRes); - expect(deleteSpy).toHaveBeenCalledWith({ _id: mockReq.params.locationId }); - }); - - test('Returns 200 if all is successful', async () => { - mockReq.body.requestor.role = 'Owner'; - const { deleteLocation } = makeSut(); - - const deleteSpy = jest.spyOn(MapLocation, 'findOneAndDelete').mockResolvedValueOnce(true); - - const res = await deleteLocation(mockReq, mockRes); - await flushPromises(); - - assertResMock(200, { message: 'The location was successfully removed!' }, res, mockRes); - expect(deleteSpy).toHaveBeenCalledWith({ _id: mockReq.params.locationId }); - }); - }); - - describe('putUserLocation method', () => { - test('Returns 403 if user is not authorized.', async () => { - mockReq.body.requestor.role = 'Volunteer'; - const { putUserLocation } = makeSut(); - - const res = await putUserLocation(mockReq, mockRes); - assertResMock(403, 'You are not authorized to make changes in the teams.', res, mockRes); - }); - - test('Returns 500 if an error occurs when saving the map location.', async () => { - const { putUserLocation } = makeSut(); - - mockReq.body.requestor.role = 'Owner'; - - const err = new Error('Saving failed!'); - - jest.spyOn(MapLocation.prototype, 'save').mockImplementationOnce(() => Promise.reject(err)); - - const res = await putUserLocation(mockReq, mockRes); - - assertResMock(500, { message: err.message }, res, mockRes); - }); - - test('Returns 200 if all is successful.', async () => { - const { putUserLocation } = makeSut(); - - mockReq.body.requestor.role = 'Owner'; - - const savedLocationData = { - _id: 1, - firstName: mockReq.body.firstName, - lastName: mockReq.body.lastName, - jobTitle: mockReq.body.jobTitle, - location: mockReq.body.location, - }; - - jest - .spyOn(MapLocation.prototype, 'save') - .mockImplementationOnce(() => Promise.resolve(savedLocationData)); - - const res = await putUserLocation(mockReq, mockRes); - - assertResMock(200, savedLocationData, res, mockRes); - }); - }); - - describe('updateUserLocation method', () => { - test('Returns 403 if user is not authorized.', async () => { - const { updateUserLocation } = makeSut(); - - mockReq.body.requestor.role = 'Volunteer'; - - const res = await updateUserLocation(mockReq, mockRes); - - assertResMock(403, 'You are not authorized to make changes in the teams.', res, mockRes); - }); - - // Returns 500 if an error occurs when updating the user location. - test('Returns 500 if an error occurs when updating the user location', async () => { - const { updateUserLocation } = makeSut(); - mockReq.body.requestor.role = 'Owner'; - mockReq.body.type = 'user'; - mockReq.body._id = '60d5f60c2f9b9c3b8a1e4a2f'; - const updateData = { - firstName: mockReq.body.firstName, - lastName: mockReq.body.lastName, - jobTitle: mockReq.body.jobTitle, - location: mockReq.body.location, - }; - - const errMsg = 'Failed to update user profile!'; - const findAndUpdateSpy = jest - .spyOn(UserProfile, 'findOneAndUpdate') - .mockImplementationOnce(() => Promise.reject(new Error(errMsg))); - - const res = await updateUserLocation(mockReq, mockRes); - - assertResMock(500, { message: new Error(errMsg).message }, res, mockRes); - expect(findAndUpdateSpy).toHaveBeenCalledWith( - { _id: mockReq.body._id }, - { $set: { ...updateData, jobTitle: [updateData.jobTitle] } }, - { new: true }, - ); - }); - - test('returns 500 if an error occurs when updating map location', async () => { - const { updateUserLocation } = makeSut(); - mockReq.body.requestor.role = 'Owner'; - mockReq.body.type = 'non-user'; - mockReq.body._id = '60d5f60c2f9b9c3b8a1e4a2f'; - const updateData = { - firstName: mockReq.body.firstName, - lastName: mockReq.body.lastName, - jobTitle: mockReq.body.jobTitle, - location: mockReq.body.location, - }; - - const errMsg = 'failed to update map locations!'; - const findAndUpdateSpy = jest - .spyOn(MapLocation, 'findOneAndUpdate') - .mockImplementationOnce(() => Promise.reject(new Error(errMsg))); - - const res = await updateUserLocation(mockReq, mockRes); - assertResMock(500, { message: new Error(errMsg).message }, res, mockRes); - expect(findAndUpdateSpy).toHaveBeenCalledWith( - { _id: mockReq.body._id }, - { $set: updateData }, - { new: true }, - ); - }); - - test('Returns 200 if all is successful when userType is user and clears and resets cache.', async () => { - mockReq.body.requestor.role = 'Owner'; - mockReq.body.type = 'user'; - mockReq.body._id = '60d5f60c2f9b9c3b8a1e4a2f'; - - const { mockCache: removeAllUsersMock, cacheObject } = makeMockCache('removeCache', true); - const removeUserCacheSpy = jest - .spyOn(cacheObject, 'removeCache') - .mockImplementationOnce(() => true); - - const setCacheSpy = jest.spyOn(cacheObject, 'setCache').mockImplementationOnce(() => true); - - const { updateUserLocation } = makeSut(); - - const updateData = { - firstName: mockReq.body.firstName, - lastName: mockReq.body.lastName, - jobTitle: mockReq.body.jobTitle, - location: mockReq.body.location, - }; - - const queryResponse = { - firstName: mockReq.body.firstName, - lastName: mockReq.body.lastName, - jobTitle: mockReq.body.jobTitle, - location: mockReq.body.location, - _id: mockReq.body._id, - }; - - const findOneAndUpdateSpy = jest - .spyOn(UserProfile, 'findOneAndUpdate') - .mockImplementationOnce(() => Promise.resolve(queryResponse)); - - const res = await updateUserLocation(mockReq, mockRes); - - assertResMock(200, { ...queryResponse, type: mockReq.body.type }, res, mockRes); - expect(findOneAndUpdateSpy).toHaveBeenCalledWith( - { _id: mockReq.body._id }, - { $set: { ...updateData, jobTitle: [updateData.jobTitle] } }, - { new: true }, - ); - - expect(removeAllUsersMock).toHaveBeenCalledWith('allusers'); - expect(removeUserCacheSpy).toHaveBeenCalledWith(`user-${mockReq.body._id}`); - expect(setCacheSpy).toHaveBeenCalledWith( - `user-${mockReq.body._id}`, - JSON.stringify(queryResponse), - ); - }); - - test('Returns 200 if all is succesful when userType is not user', async () => { - mockReq.body.requestor.role = 'Owner'; - mockReq.body.type = 'not-user'; - mockReq.body._id = '60d5f60c2f9b9c3b8a1e4a2f'; - - const { updateUserLocation } = makeSut(); - - const updateData = { - firstName: mockReq.body.firstName, - lastName: mockReq.body.lastName, - jobTitle: mockReq.body.jobTitle, - location: mockReq.body.location, - }; - - const queryResponse = { - firstName: mockReq.body.firstName, - lastName: mockReq.body.lastName, - jobTitle: mockReq.body.jobTitle, - location: mockReq.body.location, - _id: mockReq.body._id, - }; - - const findOneAndUpdateSpy = jest - .spyOn(MapLocation, 'findOneAndUpdate') - .mockImplementationOnce(() => Promise.resolve(queryResponse)); - - const res = await updateUserLocation(mockReq, mockRes); - - assertResMock(200, { ...queryResponse, type: mockReq.body.type }, res, mockRes); - expect(findOneAndUpdateSpy).toHaveBeenCalledWith( - { _id: mockReq.body._id }, - { $set: updateData }, - { new: true }, - ); - }); - }); -}); diff --git a/src/controllers/timeEntryController.js b/src/controllers/timeEntryController.js index 94ff0061f..06e76b59a 100644 --- a/src/controllers/timeEntryController.js +++ b/src/controllers/timeEntryController.js @@ -699,34 +699,27 @@ const timeEntrycontroller = function (TimeEntry) { const isTimeModified = newTotalSeconds !== timeEntry.totalSeconds; const isDescriptionModified = newNotes !== timeEntry.notes; - const canEditTimeEntryTime = await hasPermission(req.body.requestor, 'editTimeEntryTime'); - const canEditTimeEntryDescription = await hasPermission(req.body.requestor, 'editTimeEntryDescription'); + const canEditTimeEntryDescription = await hasPermission( + req.body.requestor, + 'editTimeEntryDescription', + ); const canEditTimeEntryDate = await hasPermission(req.body.requestor, 'editTimeEntryDate'); - const canEditTimeEntryIsTangible = (isForAuthUser - ? !(await hasPermission(req.body.requestor, 'toggleTangibleTime')) - : !(await hasPermission(req.body.requestor, 'editTimeEntryToggleTangible'))); + const canEditTimeEntryIsTangible = isForAuthUser + ? await hasPermission(req.body.requestor, 'toggleTangibleTime') + : await hasPermission(req.body.requestor, 'editTimeEntryToggleTangible'); const isNotUsingAPermission = - (!canEditTimeEntryTime && isTimeModified) || - (!canEditTimeEntryDate && dateOfWorkChanged); + (!canEditTimeEntryTime && isTimeModified) || (!canEditTimeEntryDate && dateOfWorkChanged); // Time - if ( - !isSameDayAuthUserEdit && - isTimeModified && - !canEditTimeEntryTime - ) { + if (!isSameDayAuthUserEdit && isTimeModified && !canEditTimeEntryTime) { const error = `You do not have permission to edit the time entry time`; return res.status(403).send({ error }); } // Description - if ( - !isSameDayAuthUserEdit && - isDescriptionModified && - !canEditTimeEntryDescription - ) { + if (!isSameDayAuthUserEdit && isDescriptionModified && !canEditTimeEntryDescription) { const error = `You do not have permission to edit the time entry description`; return res.status(403).send({ error }); } @@ -738,10 +731,7 @@ const timeEntrycontroller = function (TimeEntry) { } // Tangible Time - if ( - tangibilityChanged && - canEditTimeEntryIsTangible - ) { + if (tangibilityChanged && !canEditTimeEntryIsTangible) { const error = `You do not have permission to edit the time entry isTangible`; return res.status(403).send({ error }); } diff --git a/src/controllers/titleController.js b/src/controllers/titleController.js index f351c6e77..08751bdef 100644 --- a/src/controllers/titleController.js +++ b/src/controllers/titleController.js @@ -1,7 +1,11 @@ const Team = require('../models/team'); const Project = require('../models/project'); +const cacheClosure = require('../utilities/nodeCache'); +const { getAllTeamCodeHelper } = require("./userProfileController"); const titlecontroller = function (Title) { + const cache = cacheClosure(); + const getAllTitles = function (req, res) { Title.find({}) .then((results) => res.status(200).send(results)) @@ -97,11 +101,15 @@ const titlecontroller = function (Title) { res.status(500).send(error); }); }; - + // Update: Confirmed with Jae. Team code is not related to the Team data model. But the team code field within the UserProfile data model. async function checkTeamCodeExists(teamCode) { try { - const team = await Team.findOne({ teamCode }).exec(); - return !!team; + if (cache.getCache('teamCodes')) { + const teamCodes = JSON.parse(cache.getCache('teamCodes')); + return teamCodes.includes(teamCode); + } + const teamCodes = await getAllTeamCodeHelper(); + return teamCodes.includes(teamCode); } catch (error) { console.error('Error checking if team code exists:', error); throw error; @@ -128,3 +136,4 @@ const titlecontroller = function (Title) { }; module.exports = titlecontroller; + \ No newline at end of file diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index a61cf3c43..47ab44478 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -495,7 +495,6 @@ const userProfileController = function (UserProfile, Project) { 'totalTangibleHrs', 'totalIntangibleHrs', 'isFirstTimelog', - 'teamCode', 'isVisible', 'bioPosted', ]; @@ -506,6 +505,16 @@ const userProfileController = function (UserProfile, Project) { } }); + // Since we leverage cache for all team code retrival (refer func getAllTeamCode()), + // we need to remove the cache when team code is updated in case of new team code generation + if (req.body.teamCode) { + // remove teamCode cache when new team assigned + if (req.body.teamCode !== record.teamCode) { + cache.removeCache('teamCodes'); + } + record.teamCode = req.body.teamCode; + } + record.lastModifiedDate = Date.now(); // find userData in cache @@ -1565,6 +1574,31 @@ const userProfileController = function (UserProfile, Project) { } }; + const getAllTeamCodeHelper = async function () { + try { + if (cache.hasCache('teamCodes')) { + const teamCodes = JSON.parse(cache.getCache('teamCodes')); + return teamCodes; + } + const distinctTeamCodes = await UserProfile.distinct('teamCode', { + teamCode: { $ne: null } + }); + cache.setCache('teamCodes', JSON.stringify(distinctTeamCodes)); + return distinctTeamCodes; + } catch (error) { + throw new Error('Encountered an error to get all team codes, please try again!'); + } + } + + const getAllTeamCode = async function (req, res) { + try { + const distinctTeamCodes = await getAllTeamCodeHelper(); + return res.status(200).send({ message: 'Found', distinctTeamCodes }); + } catch (error) { + return res.status(500).send({ message: 'Encountered an error to get all team codes, please try again!' }); + } + } + return { postUserProfile, getUserProfiles, @@ -1587,6 +1621,8 @@ const userProfileController = function (UserProfile, Project) { changeUserRehireableStatus, authorizeUser, getProjectsByPerson, + getAllTeamCode, + getAllTeamCodeHelper, }; }; diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index c71e22f0d..381844883 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -1,3 +1,4 @@ +/* eslint-disable */ const mongoose = require('mongoose'); const userProfile = require('../models/userProfile'); @@ -30,25 +31,22 @@ const warningsController = function (UserProfile) { try { const { userId } = req.params; - const { - iconId, color, date, description, -} = req.body; + const { iconId, color, date, description } = req.body; const record = await UserProfile.findById(userId); if (!record) { return res.status(400).send({ message: 'No valid records found' }); } - record.warnings = record.warnings.concat({ - userId, - iconId, - color, - date, - description, - }); - await record.save(); + const updatedWarnings = await userProfile.findByIdAndUpdate( + { + _id: userId, + }, + { $push: { warnings: { userId, iconId, color, date, description } } }, + { new: true, upsert: true }, + ); - const completedData = filterWarnings(record.warnings); + const completedData = filterWarnings(updatedWarnings.warnings); res.status(201).send({ message: 'success', warnings: completedData }); } catch (error) { @@ -72,9 +70,7 @@ const warningsController = function (UserProfile) { } const sortedWarnings = filterWarnings(warnings.warnings); - res - .status(201) - .send({ message: 'succesfully deleted', warnings: sortedWarnings }); + res.status(201).send({ message: 'succesfully deleted', warnings: sortedWarnings }); } catch (error) { res.status(401).send({ message: error.message || error }); } diff --git a/src/models/team.js b/src/models/team.js index a92e740b1..4d73615f5 100644 --- a/src/models/team.js +++ b/src/models/team.js @@ -2,6 +2,12 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; +/** + * This schema represents a team in the system. + * + * Deprecated field: teamCode. Team code is no longer associated with a team. + * Team code is used as a text string identifier in the user profile data model. + */ const team = new Schema({ teamName: { type: 'String', required: true }, isActive: { type: 'Boolean', required: true, default: true }, @@ -14,6 +20,7 @@ const team = new Schema({ visible: { type : 'Boolean', default:true}, }, ], + // Deprecated field teamCode: { type: 'String', default: '', diff --git a/src/models/userProfile.js b/src/models/userProfile.js index c3e42b778..cc7136f54 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -75,7 +75,7 @@ const userProfileSchema = new Schema({ startDate: { type: Date, required: true, - default() { + default () { return this.createdDate; }, }, @@ -271,4 +271,7 @@ userProfileSchema.pre('save', function (next) { .catch((error) => next(error)); }); +userProfileSchema.index({ teamCode: 1 }); +userProfileSchema.index({ email: 1 }); + module.exports = mongoose.model('userProfile', userProfileSchema, 'userProfiles'); diff --git a/src/routes/mapLocationsRouter.test.js b/src/routes/mapLocationsRouter.test.js deleted file mode 100644 index 5eb9a87d1..000000000 --- a/src/routes/mapLocationsRouter.test.js +++ /dev/null @@ -1,200 +0,0 @@ -const request = require('supertest'); -const { jwtPayload } = require('../test'); -const { app } = require('../app'); -const { - mockReq, - createUser, - mongoHelper: { dbConnect, dbDisconnect, dbClearAll }, -} = require('../test'); -const MapLocation = require('../models/mapLocation'); - -const agent = request.agent(app); - -describe('mapLocations routes', () => { - let ownerUser; - let volunteerUser; - let ownerToken; - let volunteerToken; - let reqBody = { - ...mockReq.body, - }; - - beforeAll(async () => { - await dbConnect(); - ownerUser = await createUser(); - volunteerUser = await createUser(); - ownerUser.role = 'Owner'; - volunteerUser.role = 'Volunteer'; - ownerToken = jwtPayload(ownerUser); - volunteerToken = jwtPayload(volunteerUser); - reqBody = { - ...reqBody, - firstName: volunteerUser.firstName, - lastName: volunteerUser.lastName, - jobTitle: 'Software Engineer', - location: { - userProvided: 'A', - coords: { - lat: '51', - lng: '110', - }, - country: 'Test', - city: 'Usa', - }, - _id: volunteerUser._id, - type: 'user', - }; - }); - - afterAll(async () => { - await dbClearAll(); - await dbDisconnect(); - }); - - describe('mapLocationRoutes', () => { - it('should return 401 if authorization header is not present', async () => { - await agent.get('/api/mapLocations').send(reqBody).expect(401); - await agent.put('/api/mapLocations').send(reqBody).expect(401); - await agent.patch('/api/mapLocations').send(reqBody).expect(401); - await agent.delete('/api/mapLocations/123').send(reqBody).expect(401); - }); - - it('should return 404 if the route does not exist', async () => { - await agent - .get('/api/mapLocation') - .set('Authorization', volunteerToken) - .send(reqBody) - .expect(404); - await agent - .put('/api/mapLocation') - .set('Authorization', volunteerToken) - .send(reqBody) - .expect(404); - await agent - .patch('/api/mapLocation') - .set('Authorization', volunteerToken) - .send(reqBody) - .expect(404); - await agent - .delete('/api/mapLocation/123') - .set('Authorization', volunteerToken) - .send(reqBody) - .expect(404); - }); - }); - - describe('getMapLocation routes', () => { - it('Should return 200 and the users on success', async () => { - const expected = { - mUsers: [], - users: [ - { - location: { - city: '', - coords: { - lat: 51, - lng: 110, - }, - country: '', - userProvided: '', - }, - isActive: ownerUser.isActive, - jobTitle: ownerUser.jobTitle[0], - _id: ownerUser._id.toString(), - firstName: ownerUser.firstName, - lastName: ownerUser.lastName, - }, - { - location: { - city: '', - coords: { - lat: 51, - lng: 110, - }, - country: '', - userProvided: '', - }, - isActive: volunteerUser.isActive, - jobTitle: volunteerUser.jobTitle[0], - _id: volunteerUser._id.toString(), - firstName: volunteerUser.firstName, - lastName: volunteerUser.lastName, - }, - ], - }; - - const response = await agent - .get('/api/mapLocations') - .set('Authorization', ownerToken) - .send(reqBody) - .expect(200); - - expect(response.body).toEqual(expected); - }); - }); - - describe('putMapLocation route', () => { - it('Should return 200 on success', async () => { - const response = await agent - .put('/api/mapLocations') - .set('Authorization', ownerToken) - .send(reqBody) - .expect(200); - - const expected = { - _id: expect.anything(), - __v: expect.anything(), - firstName: reqBody.firstName, - lastName: reqBody.lastName, - jobTitle: reqBody.jobTitle, - location: reqBody.location, - isActive: false, - title: 'Prior to HGN Data Collection', - }; - - expect(response.body).toEqual(expected); - }); - }); - - describe('patchMapLocation route', () => { - it('Should return 200 on success', async () => { - reqBody.location.coords.lat = 51; - reqBody.location.coords.lng = 110; - const res = await agent - .patch('/api/mapLocations') - .set('Authorization', ownerToken) - .send(reqBody) - .expect(200); - - const expected = { - firstName: reqBody.firstName, - lastName: reqBody.lastName, - jobTitle: [reqBody.jobTitle], - location: reqBody.location, - _id: reqBody._id.toString(), - type: reqBody.type, - }; - - expect(res.body).toEqual(expected); - }); - }); - - describe('Delete map locations route', () => { - it('Should return 200 on success', async () => { - const _map = new MapLocation(); - _map.firstName = reqBody.firstName; - _map.lastName = reqBody.lastName; - _map.location = reqBody.location; - _map.jobTitle = reqBody.jobTitle; - - const map = await _map.save(); - - const res = await agent - .delete(`/api/mapLocations/${map._id}`) - .set('Authorization', ownerToken) - .send(reqBody); - - expect(res.body).toEqual({ message: 'The location was successfully removed!' }); - }); - }); -}); diff --git a/src/routes/userProfileRouter.js b/src/routes/userProfileRouter.js index e9c458b1b..795f6cfa3 100644 --- a/src/routes/userProfileRouter.js +++ b/src/routes/userProfileRouter.js @@ -102,6 +102,8 @@ const routes = function (userProfile, project) { userProfileRouter.route('/userProfile/projects/:name').get(controller.getProjectsByPerson); + userProfileRouter.route('/userProfile/teamCode/list').get(controller.getAllTeamCode); + return userProfileRouter; }; diff --git a/src/test/db/createUser.js b/src/test/db/createUser.js index e7c06aebc..ce487ccd6 100644 --- a/src/test/db/createUser.js +++ b/src/test/db/createUser.js @@ -5,9 +5,9 @@ const createUser = async () => { up.password = 'SuperSecretPassword@'; up.role = 'Administrator'; - up.firstName = 'Requestor_first_name'; - up.lastName = 'Requestor_last_name'; - up.jobTitle = ['Any_job_title']; + up.firstName = 'requestor_first_name'; + up.lastName = 'requestor_last_name'; + up.jobTitle = ['any_job_title']; up.phoneNumber = ['123456789']; up.bio = 'any_bio'; up.weeklycommittedHours = 21; @@ -32,8 +32,8 @@ const createUser = async () => { up.location = { userProvided: '', coords: { - lat: 51, - lng: 110, + lat: null, + lng: null, }, country: '', city: '', @@ -46,12 +46,11 @@ const createUser = async () => { up.isFirstTimelog = true; up.actualEmail = ''; up.isVisible = true; - up.totalTangibleHrs = 10; - /* - remove hard coded _id field to allow MongoDB to + /* + remove hard coded _id field to allow MongoDB to automatically create a unique id for us. - Now this function is more reusable if we + Now this function is more reusable if we need to create more than 1 user. */