diff --git a/constants/progresses.js b/constants/progresses.js index c3170eeb2..278e5bc90 100644 --- a/constants/progresses.js +++ b/constants/progresses.js @@ -3,6 +3,8 @@ const PROGRESS_DOCUMENT_RETRIEVAL_SUCCEEDED = "Progress document retrieved succe const PROGRESS_DOCUMENT_NOT_FOUND = "No progress records found."; const PROGRESS_ALREADY_CREATED = "Progress for the day has already been created."; const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; +const INTERNAL_SERVER_ERROR_MESSAGE = + "The server has encountered an unexpected error. Please contact the administrator for more information."; const RESPONSE_MESSAGES = { PROGRESS_DOCUMENT_CREATED_SUCCEEDED, @@ -11,4 +13,17 @@ const RESPONSE_MESSAGES = { PROGRESS_ALREADY_CREATED, }; -module.exports = { RESPONSE_MESSAGES, MILLISECONDS_IN_DAY }; +const TYPE_MAP = { + user: "userId", + task: "taskId", +}; + +const VALID_PROGRESS_TYPES = ["task", "user"]; + +module.exports = { + RESPONSE_MESSAGES, + MILLISECONDS_IN_DAY, + INTERNAL_SERVER_ERROR_MESSAGE, + TYPE_MAP, + VALID_PROGRESS_TYPES, +}; diff --git a/constants/roles.js b/constants/roles.js index 9f80a4968..d28e484a0 100644 --- a/constants/roles.js +++ b/constants/roles.js @@ -4,6 +4,7 @@ const ROLES = { APPOWNER: "app_owner", MEMBER: "member", ARCHIVED: "archived", + INDISCORD: "in_discord", }; module.exports = ROLES; diff --git a/constants/users.js b/constants/users.js index fea46dd70..bdb5939f9 100644 --- a/constants/users.js +++ b/constants/users.js @@ -13,6 +13,7 @@ const USER_STATUS = { const ALLOWED_FILTER_PARAMS = { ITEM_TAG: ["levelId", "levelName", "levelValue", "tagId"], USER_STATE: ["state"], + ROLE: ["role"], }; module.exports = { profileStatus, USER_STATUS, ALLOWED_FILTER_PARAMS }; diff --git a/controllers/progresses.js b/controllers/progresses.js index ad544514d..e0e466736 100644 --- a/controllers/progresses.js +++ b/controllers/progresses.js @@ -1,6 +1,11 @@ const { Conflict, NotFound } = require("http-errors"); -const { createProgressDocument, getProgressDocument, getRangeProgressData } = require("../models/progresses"); -const { RESPONSE_MESSAGES } = require("../constants/progresses"); +const { + createProgressDocument, + getProgressDocument, + getRangeProgressData, + getProgressByDate, +} = require("../models/progresses"); +const { RESPONSE_MESSAGES, INTERNAL_SERVER_ERROR_MESSAGE } = require("../constants/progresses"); const { PROGRESS_DOCUMENT_RETRIEVAL_SUCCEEDED, PROGRESS_DOCUMENT_CREATED_SUCCEEDED } = RESPONSE_MESSAGES; /** @@ -58,8 +63,9 @@ const createProgress = async (req, res) => { message: error.message, }); } - return res.status(400).json({ - message: error.message, + logger.error(error.message); + return res.status(500).json({ + message: INTERNAL_SERVER_ERROR_MESSAGE, }); } }; @@ -112,8 +118,9 @@ const getProgress = async (req, res) => { message: error.message, }); } - return res.status(400).json({ - message: error.message, + logger.error(error.message); + return res.status(500).json({ + message: INTERNAL_SERVER_ERROR_MESSAGE, }); } }; @@ -165,10 +172,65 @@ const getProgressRangeData = async (req, res) => { message: error.message, }); } - return res.status(400).json({ - message: error.message, + logger.error(error.message); + return res.status(500).json({ + message: INTERNAL_SERVER_ERROR_MESSAGE, }); } }; -module.exports = { createProgress, getProgress, getProgressRangeData }; +/** + * @typedef {Object} progressPathParams + * @property {string} type - The type of progress document user or task. + * @property {string} typeId - The ID of the type. + * @property {string} date - The iso format date of the query. + */ + +/** + * @typedef {Object} ProgressDocument + * @property {string} id - The id of the progress document. + * @property {string} type - The type of progress document. + * @property {string} completed - The completed progress. + * @property {string} planned - The planned progress. + * @property {string} blockers - The blockers. + * @property {string} userId - The User ID + * @property {string} [taskId] - The task ID (optional). + * @property {number} createdAt - The timestamp when the progress document was created. + * @property {number} date - The timestamp for the day the progress document was created. + */ + +/** + * @typedef {Object} GetProgressByDateResponse + * @property {string} message - The success message. + * @property {ProgressDocument} data - An array of progress documents + */ + +/** + * Retrieves the progress documents based on provided query parameters. + * @param {Object} req - The HTTP request object. + * @param {progressPathParams} req.params - The query parameters + * @param {Object} res - The HTTP response object. + * @returns {Promise} A Promise that resolves when the response is sent. + */ + +const getProgressBydDateController = async (req, res) => { + try { + const data = await getProgressByDate(req.params); + return res.json({ + message: PROGRESS_DOCUMENT_RETRIEVAL_SUCCEEDED, + data, + }); + } catch (error) { + if (error instanceof NotFound) { + return res.status(404).json({ + message: error.message, + }); + } + logger.error(error.message); + return res.status(500).json({ + message: INTERNAL_SERVER_ERROR_MESSAGE, + }); + } +}; + +module.exports = { createProgress, getProgress, getProgressRangeData, getProgressBydDateController }; diff --git a/controllers/tasks.js b/controllers/tasks.js index b64414ed9..1bae555c5 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -6,6 +6,7 @@ const { addOrUpdate, getRdsUserInfoByGitHubUsername } = require("../models/users const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING } = TASK_STATUS_OLD; const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, ASSIGNED } = TASK_STATUS; const { INTERNAL_SERVER_ERROR, SOMETHING_WENT_WRONG } = require("../constants/errorMessages"); +const dependencyModel = require("../models/tasks"); /** * Creates new task * @@ -16,17 +17,25 @@ const { INTERNAL_SERVER_ERROR, SOMETHING_WENT_WRONG } = require("../constants/er const addNewTask = async (req, res) => { try { const { id: createdBy } = req.userData; + const dependsOn = req.body.dependsOn; const body = { ...req.body, createdBy, }; - - const task = await tasks.updateTask(body); - + delete body.dependsOn; + const { taskId, taskDetails } = await tasks.updateTask(body); + const data = { + taskId, + dependsOn, + }; + const taskDependency = dependsOn && (await dependencyModel.addDependency(data)); return res.json({ message: "Task created successfully!", - task: task.taskDetails, - id: task.taskId, + task: { + ...taskDetails, + ...(taskDependency && { dependsOn: taskDependency }), + id: taskId, + }, }); } catch (err) { logger.error(`Error while creating new task: ${err}`); diff --git a/controllers/users.js b/controllers/users.js index 0991324cc..dda20032b 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -5,7 +5,7 @@ const logsQuery = require("../models/logs"); const imageService = require("../services/imageService"); const { profileDiffStatus } = require("../constants/profileDiff"); const { logType } = require("../constants/logs"); -const { fetch } = require("../utils/fetch"); + const logger = require("../utils/logger"); const obfuscate = require("../utils/obfuscate"); const { getPaginationLink, getUsernamesFromPRs } = require("../utils/users"); @@ -24,7 +24,11 @@ const verifyUser = async (req, res) => { logger.error(`Error while verifying user: ${error}`); return res.boom.serverUnavailable(SOMETHING_WENT_WRONG); } - fetch(process.env.IDENTITY_SERVICE_URL, "POST", null, { userId }, { "Content-Type": "application/json" }); + fetch(process.env.IDENTITY_SERVICE_URL, { + method: "POST", + body: { userId }, + headers: { "Content-Type": "application/json" }, + }); return res.json({ message: "Your request has been queued successfully", }); diff --git a/middlewares/validators/progresses.js b/middlewares/validators/progresses.js index 326a21329..2cad2c606 100644 --- a/middlewares/validators/progresses.js +++ b/middlewares/validators/progresses.js @@ -1,14 +1,20 @@ const joi = require("joi"); +const { VALID_PROGRESS_TYPES } = require("../../constants/progresses"); const validateCreateProgressRecords = async (req, res, next) => { const baseSchema = joi .object() .strict() .keys({ - type: joi.string().trim().valid("user", "task").required().messages({ - "any.required": "Required field 'type' is missing.", - "any.only": "Type field is restricted to either 'user' or 'task'.", - }), + type: joi + .string() + .trim() + .valid(...VALID_PROGRESS_TYPES) + .required() + .messages({ + "any.required": "Required field 'type' is missing.", + "any.only": "Type field is restricted to either 'user' or 'task'.", + }), completed: joi.string().trim().required().messages({ "any.required": "Required field 'completed' is missing.", "string.trim": "completed must not have leading or trailing whitespace", @@ -44,9 +50,13 @@ const validateCreateProgressRecords = async (req, res, next) => { const validateGetProgressRecordsQuery = async (req, res, next) => { const schema = joi .object({ - type: joi.string().valid("user", "task").optional().messages({ - "any.only": "Type field is restricted to either 'user' or 'task'.", - }), + type: joi + .string() + .valid(...VALID_PROGRESS_TYPES) + .optional() + .messages({ + "any.only": "Type field is restricted to either 'user' or 'task'.", + }), userId: joi.string().optional().allow("").messages({ "string.base": "userId must be a string", }), @@ -92,8 +102,30 @@ const validateGetRangeProgressRecordsParams = async (req, res, next) => { res.boom.badRequest(error.details[0].message); } }; + +const validateGetDayProgressParams = async (req, res, next) => { + const schema = joi.object({ + type: joi + .string() + .valid(...VALID_PROGRESS_TYPES) + .required() + .messages({ + "any.only": "Type field is restricted to either 'user' or 'task'.", + }), + typeId: joi.string().required(), + date: joi.date().iso().required(), + }); + try { + await schema.validateAsync(req.params, { abortEarly: false }); + next(); + } catch (error) { + logger.error(`Error validating payload: ${error}`); + res.boom.badRequest(error.details[0].message); + } +}; module.exports = { validateCreateProgressRecords, validateGetProgressRecordsQuery, validateGetRangeProgressRecordsParams, + validateGetDayProgressParams, }; diff --git a/middlewares/validators/user.js b/middlewares/validators/user.js index 8ead61e64..ac91998d0 100644 --- a/middlewares/validators/user.js +++ b/middlewares/validators/user.js @@ -1,5 +1,6 @@ const joi = require("joi"); const { USER_STATUS } = require("../../constants/users"); +const ROLES = require("../../constants/roles"); const updateUser = async (req, res, next) => { const schema = joi @@ -172,6 +173,8 @@ async function validateUserQueryParams(req, res, next) { joi.array().items(joi.string().valid("IDLE", "OOO", "ACTIVE")) ) .optional(), + role: joi.string().valid(ROLES.MEMBER, ROLES.INDISCORD).optional(), + verified: joi.string().optional(), }) .messages({ "object.min": "Please provide at least one filter criteria", diff --git a/models/progresses.js b/models/progresses.js index 4e7a1e99e..b861da0ab 100644 --- a/models/progresses.js +++ b/models/progresses.js @@ -1,7 +1,7 @@ -const { Conflict } = require("http-errors"); +const { Conflict, NotFound } = require("http-errors"); const fireStore = require("../utils/firestore"); const progressesCollection = fireStore.collection("progresses"); -const { RESPONSE_MESSAGES } = require("../constants/progresses"); +const { RESPONSE_MESSAGES, TYPE_MAP } = require("../constants/progresses"); const { buildQueryToFetchDocs, getProgressDocs, @@ -11,8 +11,9 @@ const { buildQueryForPostingProgress, assertTaskExists, getProgressDateTimestamp, + buildQueryToSearchProgressByDay, } = require("../utils/progresses"); -const { PROGRESS_ALREADY_CREATED } = RESPONSE_MESSAGES; +const { PROGRESS_ALREADY_CREATED, PROGRESS_DOCUMENT_NOT_FOUND } = RESPONSE_MESSAGES; /** * Adds a new progress document for the given user or task, with a limit of one progress document per day. @@ -68,4 +69,22 @@ const getRangeProgressData = async (queryParams) => { }; }; -module.exports = { createProgressDocument, getProgressDocument, getRangeProgressData }; +/** + * This function fetches the progress records for a particular user or task on the specified date. + * @param pathParams {object} This is the data that will be used for querying the db. It should contain type, typeId and date + * @returns {Promise} A Promise that resolves with the progress records of the queried user or task. + * @throws {Error} If the userId or taskId is invalid or does not exist. + **/ +async function getProgressByDate(pathParams) { + const { type, typeId, date } = pathParams; + await assertUserOrTaskExists({ [TYPE_MAP[type]]: typeId }); + const query = buildQueryToSearchProgressByDay({ [TYPE_MAP[type]]: typeId, date }); + const result = await query.get(); + if (!result.size) { + throw new NotFound(PROGRESS_DOCUMENT_NOT_FOUND); + } + const doc = result.docs[0]; + return { id: doc.id, ...doc.data() }; +} + +module.exports = { createProgressDocument, getProgressDocument, getRangeProgressData, getProgressByDate }; diff --git a/models/tasks.js b/models/tasks.js index e333ce93a..1756e0dcc 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -1,6 +1,7 @@ const firestore = require("../utils/firestore"); const tasksModel = firestore.collection("tasks"); const ItemModel = firestore.collection("itemTags"); +const dependencyModel = firestore.collection("taskDependencies"); const userUtils = require("../utils/users"); const { fromFirestoreData, toFirestoreData, buildTasks } = require("../utils/tasks"); const { TASK_TYPE, TASK_STATUS, TASK_STATUS_OLD } = require("../constants/tasks"); @@ -32,13 +33,34 @@ const updateTask = async (taskData, taskId = null) => { taskId: taskInfo.id, taskDetails: await fromFirestoreData(taskData), }; - return result; } catch (err) { logger.error("Error in updating task", err); throw err; } }; +const addDependency = async (data) => { + try { + const { taskId, dependsOn } = data; + const batch = firestore.batch(); + if (dependsOn.length > 500) { + throw new Error("Error cannot add more than 500 taskId"); + } + for (const dependsId of dependsOn) { + const taskDependOn = { + taskId, + dependsId, + }; + const docid = dependencyModel.doc(); + batch.set(docid, taskDependOn); + } + await batch.commit(); + return data.dependsOn; + } catch (err) { + logger.error("Error in creating dependency"); + throw err; + } +}; /** * Fetch all tasks @@ -352,5 +374,6 @@ module.exports = { fetchSelfTask, fetchSkillLevelTask, overdueTasks, + addDependency, fetchTaskByIssueId, }; diff --git a/models/users.js b/models/users.js index 4cf32ff3f..9b640235b 100644 --- a/models/users.js +++ b/models/users.js @@ -366,6 +366,8 @@ const getRdsUserInfoByGitHubUsername = async (githubUsername) => { * @param {Array} query.levelNumber - Array of levelNumbers to filter the users on * @param {Array} query.tagId - Array of tagIds to filter the users on * @param {Array} query.state - Array of states to filter the users on + * @param {String} query.role - filter the users on role + * @param {String} query.verified - filter the users on verified i.e, discordId data * @return {Promise} - Array of user documents that match the filter criteria */ @@ -418,6 +420,33 @@ const getUsersBasedOnFilter = async (query) => { const filteredUserDocs = userDocs.filter((doc) => !doc.roles?.archived); return filteredUserDocs; } + + const { role: roleQuery, verified: verifiedQuery } = query; + + if (roleQuery) { + const filteredUsers = []; + const snapshot = await userModel.where(`roles.${roleQuery}`, "==", true).get(); + snapshot.forEach((doc) => { + filteredUsers.push({ + id: doc.id, + ...doc.data(), + }); + }); + + return filteredUsers.filter((user) => !user.roles?.archived); + } + if (verifiedQuery === "true") { + const filteredUsers = []; + const snapshot = await userModel.where("discordId", "!=", null).get(); + snapshot.forEach((doc) => { + filteredUsers.push({ + id: doc.id, + ...doc.data(), + }); + }); + + return filteredUsers.filter((user) => !user.roles?.archived); + } return []; }; diff --git a/routes/progresses.js b/routes/progresses.js index 89e3d46e1..767559443 100644 --- a/routes/progresses.js +++ b/routes/progresses.js @@ -5,11 +5,18 @@ const { validateCreateProgressRecords, validateGetProgressRecordsQuery, validateGetRangeProgressRecordsParams, + validateGetDayProgressParams, } = require("../middlewares/validators/progresses"); -const { createProgress, getProgress, getProgressRangeData } = require("../controllers/progresses"); +const { + createProgress, + getProgress, + getProgressRangeData, + getProgressBydDateController, +} = require("../controllers/progresses"); router.post("/", authenticate, validateCreateProgressRecords, createProgress); router.get("/", validateGetProgressRecordsQuery, getProgress); +router.get("/:type/:typeId/date/:date", validateGetDayProgressParams, getProgressBydDateController); router.get("/range", validateGetRangeProgressRecordsParams, getProgressRangeData); module.exports = router; diff --git a/test/fixtures/user/user.js b/test/fixtures/user/user.js index 94d9d7f9d..e46c6eafe 100644 --- a/test/fixtures/user/user.js +++ b/test/fixtures/user/user.js @@ -21,8 +21,10 @@ module.exports = () => { isMember: true, phone: "1234567890", email: "abc@gmail.com", + joined_discord: "2023-01-13T18:21:09.278000+00:00", roles: { member: true, + in_discord: true, }, tokens: { githubAccessToken: "githubAccessToken", @@ -105,6 +107,7 @@ module.exports = () => { github_display_name: "Ankush Dharkar", phone: "1234567890", email: "ad@amazon.com", + joined_discord: "2023-01-13T18:21:09.278000+00:00", status: "idle", tokens: { githubAccessToken: "githubAccessToken", @@ -112,6 +115,7 @@ module.exports = () => { roles: { super_user: true, archived: false, + in_discord: true, }, picture: { publicId: "profile/mtS4DhUvNYsKqI7oCWVB/aenklfhtjldc5ytei3ar", diff --git a/test/integration/progressesTasks.test.js b/test/integration/progressesTasks.test.js index 5a4f303d7..e73f9dae9 100644 --- a/test/integration/progressesTasks.test.js +++ b/test/integration/progressesTasks.test.js @@ -20,8 +20,7 @@ const taskData = require("../fixtures/tasks/tasks")(); const cookieName = config.get("userToken.cookieName"); const { expect } = chai; -// eslint-disable-next-line mocha/no-skipped-tests -describe.skip("Test Progress Updates API for Tasks", function () { +describe("Test Progress Updates API for Tasks", function () { afterEach(async function () { await cleanDb(); }); @@ -34,7 +33,7 @@ describe.skip("Test Progress Updates API for Tasks", function () { let taskId2; beforeEach(async function () { clock = sinon.useFakeTimers({ - now: new Date(2023, 4, 2, 5, 55).getTime(), + now: new Date(Date.UTC(2023, 4, 2, 0, 25)).getTime(), // UTC time equivalent to 5:55 AM IST toFake: ["Date"], }); userId = await addUser(userData[1]); @@ -52,7 +51,7 @@ describe.skip("Test Progress Updates API for Tasks", function () { clock.restore(); }); - it("Stores the progress entry for the task", function (done) { + it("Stores the task progress entry", function (done) { chai .request(app) .post(`/progresses`) @@ -80,6 +79,36 @@ describe.skip("Test Progress Updates API for Tasks", function () { }); }); + it("stores the user progress document for the previous day if the update is sent before 6am IST", function (done) { + clock.setSystemTime(new Date(Date.UTC(2023, 4, 2, 0, 29)).getTime()); // 2nd May 2023 05:59 am IST + chai + .request(app) + .post(`/progresses`) + .set("cookie", `${cookieName}=${userToken}`) + .send(taskProgressDay1(taskId2)) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(201); + expect(res.body.data.date).to.be.equal(1682899200000); // 1st May 2023 + return done(); + }); + }); + + it("stores the user progress document for the current day if the update is sent after 6am IST", function (done) { + clock.setSystemTime(new Date(Date.UTC(2023, 4, 2, 0, 31)).getTime()); // 2nd May 2023 06:01 am IST + chai + .request(app) + .post(`/progresses`) + .set("cookie", `${cookieName}=${userToken}`) + .send(taskProgressDay1(taskId2)) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(201); + expect(res.body.data.date).to.be.equal(1682985600000); // 2nd May 2023 + return done(); + }); + }); + it("throws Conflict Error 409 if the task progress is updated multiple times in a day", function (done) { chai .request(app) @@ -319,4 +348,95 @@ describe.skip("Test Progress Updates API for Tasks", function () { }); }); }); + + describe("Verify the GET endpoint for retrieving progress document for the user on a particular date", function () { + let userId; + let taskId; + let anotherTaskId; + + beforeEach(async function () { + userId = await addUser(userData[0]); + const taskObject = await tasks.updateTask(taskData[0]); + taskId = taskObject.taskId; + const anotherTaskObject = await tasks.updateTask(taskData[0]); + anotherTaskId = anotherTaskObject.taskId; + const progressData = stubbedModelTaskProgressData(userId, taskId, 1683072000000, 1682985600000); + await firestore.collection("progresses").doc("progressDoc").set(progressData); + }); + + it("Returns the progress data for a specific task", function (done) { + chai + .request(app) + .get(`/progresses/task/${taskId}/date/2023-05-02`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(200); + expect(res.body).to.have.keys(["message", "data"]); + expect(res.body.data).to.be.an("object"); + expect(res.body.message).to.be.equal("Progress document retrieved successfully."); + expect(res.body.data).to.have.keys([ + "id", + "type", + "completed", + "planned", + "blockers", + "userId", + "taskId", + "createdAt", + "date", + ]); + return done(); + }); + }); + + it("Should return 404 No progress records found if the document doesn't exist", function (done) { + chai + .request(app) + .get(`/progresses/task/${taskId}/date/2023-05-03`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(404); + expect(res.body).to.be.an("object"); + expect(res.body).to.have.key("message"); + expect(res.body.message).to.be.equal("No progress records found."); + return done(); + }); + }); + + it("Returns 400 for bad request", function (done) { + chai + .request(app) + .get(`/progresses/task/${taskId}/date/2023-05-33`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(400); + expect(res.body.message).to.be.equal('"date" must be in ISO 8601 date format'); + return done(); + }); + }); + + it("Returns 404 for invalid task id", function (done) { + chai + .request(app) + .get(`/progresses/task/invalidTaskId/date/2023-05-02`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(404); + expect(res.body.message).to.be.equal("Task with id invalidTaskId does not exist."); + return done(); + }); + }); + + it("Returns 404 if the progress document doesn't exist for the task", function (done) { + chai + .request(app) + .get(`/progresses/task/${anotherTaskId}/date/2023-05-02`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(404); + expect(res.body.message).to.be.equal("No progress records found."); + return done(); + }); + }); + }); }); diff --git a/test/integration/progressesUsers.test.js b/test/integration/progressesUsers.test.js index ecac4a121..27a6d382b 100644 --- a/test/integration/progressesUsers.test.js +++ b/test/integration/progressesUsers.test.js @@ -18,8 +18,7 @@ const userData = require("../fixtures/user/user")(); const cookieName = config.get("userToken.cookieName"); const { expect } = chai; -// eslint-disable-next-line mocha/no-skipped-tests -describe.skip("Test Progress Updates API for Users", function () { +describe("Test Progress Updates API for Users", function () { afterEach(async function () { await cleanDb(); }); @@ -32,7 +31,7 @@ describe.skip("Test Progress Updates API for Users", function () { let anotherUserToken; beforeEach(async function () { clock = sinon.useFakeTimers({ - now: new Date(2023, 4, 2, 5, 55).getTime(), + now: new Date(Date.UTC(2023, 4, 2, 0, 25)).getTime(), // UTC time equivalent to 5:55 AM IST toFake: ["Date"], }); userId = await addUser(userData[1]); @@ -47,7 +46,7 @@ describe.skip("Test Progress Updates API for Users", function () { clock.restore(); }); - it("stores the user progress document for the first time", function (done) { + it("stores the user progress document", function (done) { chai .request(app) .post(`/progresses`) @@ -73,6 +72,36 @@ describe.skip("Test Progress Updates API for Users", function () { }); }); + it("stores the user progress document for the previous day if the update is sent before 6am IST", function (done) { + clock.setSystemTime(new Date(Date.UTC(2023, 4, 2, 0, 29)).getTime()); // 2nd May 2023 05:59 am IST + chai + .request(app) + .post(`/progresses`) + .set("cookie", `${cookieName}=${userToken}`) + .send(standupProgressDay1) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(201); + expect(res.body.data.date).to.be.equal(1682899200000); // 1st May 2023 + return done(); + }); + }); + + it("stores the user progress document for the current day if the update is sent after 6am IST", function (done) { + clock.setSystemTime(new Date(Date.UTC(2023, 4, 2, 0, 31)).getTime()); // 2nd May 2023 06:01 am IST + chai + .request(app) + .post(`/progresses`) + .set("cookie", `${cookieName}=${userToken}`) + .send(standupProgressDay1) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(201); + expect(res.body.data.date).to.be.equal(1682985600000); // 2nd May 2023 + return done(); + }); + }); + it("throws Conflict Error 409 if the user tries to update progress multiple times in a single day", function (done) { chai .request(app) @@ -294,4 +323,90 @@ describe.skip("Test Progress Updates API for Users", function () { }); }); }); + + describe("Verify the GET endpoint for retrieving progress document for the user on a particular date", function () { + let userId; + let anotherUserId; + + beforeEach(async function () { + userId = await addUser(userData[0]); + anotherUserId = await addUser(userData[1]); + const progressData = stubbedModelProgressData(userId, 1683072000000, 1682985600000); + await firestore.collection("progresses").doc("progressDoc").set(progressData); + }); + + it("Returns the progress data for a specific user", function (done) { + chai + .request(app) + .get(`/progresses/user/${userId}/date/2023-05-02`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(200); + expect(res.body).to.have.keys(["message", "data"]); + expect(res.body.data).to.be.an("object"); + expect(res.body.message).to.be.equal("Progress document retrieved successfully."); + expect(res.body.data).to.have.keys([ + "id", + "type", + "completed", + "planned", + "blockers", + "userId", + "createdAt", + "date", + ]); + return done(); + }); + }); + + it("Should return 404 No progress records found if the document doesn't exist", function (done) { + chai + .request(app) + .get(`/progresses/user/${userId}/date/2023-05-03`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(404); + expect(res.body).to.be.an("object"); + expect(res.body).to.have.key("message"); + expect(res.body.message).to.be.equal("No progress records found."); + return done(); + }); + }); + + it("Returns 400 for bad request", function (done) { + chai + .request(app) + .get(`/progresses/user/${userId}/date/2023-05-33`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(400); + expect(res.body.message).to.be.equal('"date" must be in ISO 8601 date format'); + return done(); + }); + }); + + it("Returns 404 for invalid user id", function (done) { + chai + .request(app) + .get(`/progresses/user/invalidUserId/date/2023-05-02`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(404); + expect(res.body.message).to.be.equal("User with id invalidUserId does not exist."); + return done(); + }); + }); + + it("Returns 404 if the progress document doesn't exist for the user", function (done) { + chai + .request(app) + .get(`/progresses/user/${anotherUserId}/date/2023-05-02`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(404); + expect(res.body.message).to.be.equal("No progress records found."); + return done(); + }); + }); + }); }); diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index df1b5cdb2..dc21ff41e 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -98,6 +98,7 @@ describe("Tasks", function () { lossRate: { [DINERO]: 1 }, assignee: appOwner.username, participants: [], + dependsOn: [], }) .end((err, res) => { if (err) { @@ -106,11 +107,12 @@ describe("Tasks", function () { expect(res).to.have.status(200); expect(res.body).to.be.a("object"); expect(res.body.message).to.equal("Task created successfully!"); - expect(res.body.id).to.be.a("string"); expect(res.body.task).to.be.a("object"); + expect(res.body.task.id).to.be.a("string"); expect(res.body.task.createdBy).to.equal(appOwner.username); expect(res.body.task.assignee).to.equal(appOwner.username); expect(res.body.task.participants).to.be.a("array"); + expect(res.body.task.dependsOn).to.be.a("array"); return done(); }); }); diff --git a/test/unit/models/tasks.test.js b/test/unit/models/tasks.test.js index ac88b8d42..0069920ee 100644 --- a/test/unit/models/tasks.test.js +++ b/test/unit/models/tasks.test.js @@ -10,6 +10,9 @@ const { expect } = chai; const cleanDb = require("../../utils/cleanDb"); const tasksData = require("../../fixtures/tasks/tasks")(); const tasks = require("../../../models/tasks"); +const { addDependency } = require("../../../models/tasks"); +const firestore = require("../../../utils/firestore"); +const dependencyModel = firestore.collection("TaskDependencies"); describe("tasks", function () { afterEach(async function () { @@ -40,4 +43,31 @@ describe("tasks", function () { }); }); }); + + describe("addDependency", function () { + it("should add dependencies to firestore and return dependsOn array", async function () { + const data = { + taskId: "taskId1", + dependsOn: ["taskId2", "taskId3"], + }; + const result = await addDependency(data); + expect(result).to.deep.equal(data.dependsOn); + }); + + it("should throw an error if there is an error while creating dependencies", async function () { + const data = { + taskId: "taskId1", + dependsOn: ["taskId2", "taskId3"], + }; + const expectedError = new Error("test error"); + dependencyModel.doc = () => { + throw expectedError; + }; + try { + await addDependency(data); + } catch (err) { + expect(err).to.deep.equal(expectedError); + } + }); + }); }); diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index 728f267c9..747015fed 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -88,4 +88,16 @@ describe("users", function () { expect(userExists).to.equal(true); }); }); + + describe(" search users API: getUsersBasedOnFilter", function () { + it("should return an empty array if no query is provided", async function () { + const result = await users.getUsersBasedOnFilter({}); + expect(result).to.deep.equal([]); + }); + + it("should return an array of verified users", async function () { + const result = await users.getUsersBasedOnFilter({ verified: "true" }); + expect(result).to.deep.equal(userDataArray.filter((user) => user.discordId)); + }); + }); }); diff --git a/utils/progresses.js b/utils/progresses.js index e56e6ab74..1eddc8462 100644 --- a/utils/progresses.js +++ b/utils/progresses.js @@ -16,7 +16,7 @@ const progressesCollection = fireStore.collection("progresses"); const getProgressDateTimestamp = () => { // Currently, we are primarily catering to Indian users for our apps, which is why we have implemented support for the IST (Indian Standard Time) timezone for progress updates. const currentHourIST = new Date().getUTCHours() + 5.5; // IST offset is UTC+5:30; - const isBefore6amIST = currentHourIST < 6; + const isBefore6amIST = currentHourIST === 5.5 && new Date().getUTCMinutes() <= 30; return isBefore6amIST ? new Date().setUTCHours(0, 0, 0, 0) - MILLISECONDS_IN_DAY : new Date().setUTCHours(0, 0, 0, 0); }; @@ -178,6 +178,27 @@ const getProgressRecords = async (query, queryParams) => { return progressRecords; }; +/** + * Retrieves progress records for a given date range. + * @param {Object} pathParamsObject - An object containing the type , typeId and date. + * @param {string} pathParamsObject.type - The type of the record i.e user or task. + * @param {string} pathParamsObject.typeId - The id of the type i.e user or task. + * @param {string} pathParamsObject.date - The date of the record + * @returns {Query} A Firestore query object that filters progress documents based on the given parameters. + * + */ +const buildQueryToSearchProgressByDay = (pathParams) => { + const { userId, taskId, date } = pathParams; + let query = progressesCollection; + if (userId) { + query = query.where("userId", "==", userId); + } else { + query = query.where("taskId", "==", taskId); + } + const dateTimeStamp = new Date(date).setUTCHours(0, 0, 0, 0); + query = query.where("date", "==", dateTimeStamp).limit(1); + return query; +}; module.exports = { getProgressDateTimestamp, buildQueryForPostingProgress, @@ -188,4 +209,5 @@ module.exports = { getProgressDocs, buildRangeProgressQuery, getProgressRecords, + buildQueryToSearchProgressByDay, };