From 6a88a00d299dbcbb1a7a08ec3d21ce15f79df8f3 Mon Sep 17 00:00:00 2001 From: Soumava Das Date: Tue, 16 Jun 2026 20:15:42 +0530 Subject: [PATCH 01/10] perf(user-status): delegate scheduled status filtering to Firestore Optimizes the database query in updateAllUserStatus by performing range filtering on the database level instead of in-memory application logic. Key Changes: - Added filter on futureStatus.from directly to the query. - Replaced the internal snapshot property _size with public size property. - Instantiated today once at the top of the updateAllUserStatus model. --- models/userStatus.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/models/userStatus.js b/models/userStatus.js index 9a868791b..bb50c1478 100644 --- a/models/userStatus.js +++ b/models/userStatus.js @@ -297,10 +297,13 @@ const updateAllUserStatus = async () => { nonOooUsersUnaltered: 0, }; try { - const userStatusDocs = await userStatusModel.where("futureStatus.state", "in", ["ACTIVE", "IDLE", "OOO"]).get(); - summary.usersCount = userStatusDocs._size; + const today = Date.now(); + const userStatusDocs = await userStatusModel + .where("futureStatus.state", "in", ["ACTIVE", "IDLE", "OOO"]) + .where("futureStatus.from", "<=", today) + .get(); + summary.usersCount = userStatusDocs.size; const batch = firestore.batch(); - const today = new Date().getTime(); for (const document of userStatusDocs.docs) { const doc = document.data(); const docRef = document.ref; From d02b3faf6b03f9c83ba53ab7ca3ebfe980655d76 Mon Sep 17 00:00:00 2001 From: Soumava Das Date: Thu, 2 Jul 2026 21:37:56 +0530 Subject: [PATCH 02/10] refactor: remove redundant in-code date checks in updateAllUserStatus Removes obsolete date-comparison filters and dead code branches in models/userStatus.js. Since the query already filters by 'futureStatus.from <= today' (via commit 6a88a00d), the corresponding nested checks are always true and have been simplified. --- models/userStatus.js | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/models/userStatus.js b/models/userStatus.js index bb50c1478..4103c0acb 100644 --- a/models/userStatus.js +++ b/models/userStatus.js @@ -315,24 +315,20 @@ const updateAllUserStatus = async () => { const currentState = currentStatus?.state; const currentUntil = currentStatus?.until; if (futureState === "ACTIVE" || futureState === "IDLE") { - if (today >= futureStatus.from) { - // OOO period is over and we need to update their current status - newStatusData.currentStatus = { ...futureStatus, until: "", updatedAt: today }; - delete newStatusData.futureStatus; - const lastOooUntilUpdate = resolveLastOooUntil({ - previousState: currentState, - previousUntil: currentUntil, - nextState: futureState, - fallbackTimestamp: today, - }); - if (lastOooUntilUpdate !== undefined) { - newStatusData.lastOooUntil = lastOooUntilUpdate; - } - toUpdate = !toUpdate; - summary.oooUsersAltered++; - } else { - summary.oooUsersUnaltered++; + // OOO period is over and we need to update their current status + newStatusData.currentStatus = { ...futureStatus, until: "", updatedAt: today }; + delete newStatusData.futureStatus; + const lastOooUntilUpdate = resolveLastOooUntil({ + previousState: currentState, + previousUntil: currentUntil, + nextState: futureState, + fallbackTimestamp: today, + }); + if (lastOooUntilUpdate !== undefined) { + newStatusData.lastOooUntil = lastOooUntilUpdate; } + toUpdate = !toUpdate; + summary.oooUsersAltered++; } else { // futureState is OOO if (today > futureStatus.until) { @@ -340,7 +336,7 @@ const updateAllUserStatus = async () => { delete newStatusData.futureStatus; toUpdate = !toUpdate; summary.nonOooUsersAltered++; - } else if (today <= doc.futureStatus.until && today >= doc.futureStatus.from) { + } else { // the current date i.e today lies in between the from and until so we need to swap the status let newCurrentStatus = {}; let newFutureStatus = {}; @@ -353,8 +349,6 @@ const updateAllUserStatus = async () => { newStatusData.lastOooUntil = null; toUpdate = !toUpdate; summary.nonOooUsersAltered++; - } else { - summary.nonOooUsersUnaltered++; } } if (toUpdate) { From e6151c4b6b4c70e6fb19873b7d11eafd8683740a Mon Sep 17 00:00:00 2001 From: Soumava Das Date: Sun, 12 Jul 2026 03:17:50 +0530 Subject: [PATCH 03/10] fixed missing test cases --- test/unit/models/userStatus.js | 890 +++++++++++++++++++++++++++++++-- 1 file changed, 839 insertions(+), 51 deletions(-) diff --git a/test/unit/models/userStatus.js b/test/unit/models/userStatus.js index d537ce829..8271cf647 100644 --- a/test/unit/models/userStatus.js +++ b/test/unit/models/userStatus.js @@ -1,89 +1,877 @@ -import { userFutureStatusData } from "../../fixtures/userFutureStatus/userFutureStatusData"; +const { userFutureStatusData } = require("../../fixtures/userFutureStatus/userFutureStatusData"); const chai = require("chai"); const sinon = require("sinon"); +const admin = require("firebase-admin"); const { NotFound, Forbidden } = require("http-errors"); const { expect } = chai; const firestore = require("../../../utils/firestore"); +const logger = require("../../../utils/logger"); +global.logger = logger; + const userStatusModel = firestore.collection("usersStatus"); const tasksModel = firestore.collection("tasks"); -const { cancelOooStatus, addFutureStatus } = require("../../../models/userStatus"); +const discordRoleModel = firestore.collection("discord-roles"); +const memberRoleModel = firestore.collection("member-group-roles"); +const { + deleteUserStatus, + getUserStatus, + getAllUserStatus, + updateUserStatus, + updateAllUserStatus, + getGroupRole, + cancelOooStatus, + addFutureStatus, +} = require("../../../models/userStatus"); const cleanDb = require("../../utils/cleanDb"); const addUser = require("../../utils/addUser"); +const userData = require("../../fixtures/user/user"); +const allTasks = require("../../fixtures/tasks/tasks"); const { userState } = require("../../../constants/userStatus"); +const { TASK_STATUS } = require("../../../constants/tasks"); +const { ONE_DAY_IN_MS } = require("../../../constants/users"); const { generateStatusDataForCancelOOO, generateDefaultFutureStatus } = require("../../fixtures/userStatus/userStatus"); -describe("tasks", function () { - let userId; - let docRefUser0; +const now = () => Date.now(); + +const buildCurrentStatus = (state, overrides = {}) => ({ + message: "", + from: now() - ONE_DAY_IN_MS, + until: "", + updatedAt: now() - ONE_DAY_IN_MS, + state, + ...overrides, +}); + +const buildUserStatus = (userId, state = userState.ACTIVE, overrides = {}) => { + const { currentStatus, ...statusOverrides } = overrides; + return { + userId, + currentStatus: buildCurrentStatus(state, currentStatus), + monthlyHours: { + committed: 40, + updatedAt: now() - ONE_DAY_IN_MS, + }, + ...statusOverrides, + }; +}; + +const seedGroupIdleRole = async () => { + await discordRoleModel.doc("group-idle-doc").set({ + rolename: "group-idle", + roleid: "group-idle-role-id", + }); +}; + +const getGroupIdleMemberRolesForUser = (discordId) => { + return memberRoleModel.where("roleid", "==", "group-idle-role-id").where("userid", "==", discordId).get(); +}; - beforeEach(async function () { - userId = await addUser(); - docRefUser0 = userStatusModel.doc(); - const data = generateStatusDataForCancelOOO(userId, userState.OOO); - await docRefUser0.set(data); +const addLiveTaskForUser = async (userId) => { + const [task] = allTasks(); + await tasksModel.doc().set({ + ...task, + assignee: userId, + status: TASK_STATUS.ASSIGNED, }); +}; + +describe("User Status Model", function () { + let fetchStub; afterEach(async function () { sinon.restore(); + if (fetchStub) { + fetchStub = null; + } await cleanDb(); }); - it("Should cancel the OOO Status of the User", async function () { - const response = await cancelOooStatus(userId); - expect(response.userStatusExists).to.equal(true); - expect(response.data.userId).to.equal(userId); - expect(response.data.currentStatus).to.not.equal(userState.OOO); - expect(response.data.futureStatus?.state).to.equal(undefined); + describe("deleteUserStatus", function () { + it("should delete an existing user status document", async function () { + const userId = "delete-existing-user"; + await userStatusModel.doc("status-to-delete").set(buildUserStatus(userId)); + + const response = await deleteUserStatus(userId); + const deletedDoc = await userStatusModel.doc("status-to-delete").get(); + + expect(response).to.deep.equal({ + id: "status-to-delete", + userStatusExisted: true, + userStatusDeleted: true, + }); + expect(deletedDoc.exists).to.equal(false); + }); + + it("should return a not found response when the user status document does not exist", async function () { + const response = await deleteUserStatus("missing-user"); + + expect(response).to.deep.equal({ + id: null, + userStatusExisted: false, + userStatusDeleted: false, + }); + }); + + it("should throw an error when the database query fails", async function () { + sinon.stub(admin.firestore.Query.prototype, "where").throws(new Error("Unable to delete user status")); + + try { + await deleteUserStatus("user-id"); + expect.fail("Should have thrown"); + } catch (err) { + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.equal("Unable to delete user status"); + } + }); }); - it("Should clear the future Status if the User cancels OOO", async function () { - const data = generateStatusDataForCancelOOO(userId, userState.OOO); - const from = new Date().getTime() + 24 * 60 * 60 * 1000; // 1 day offset from current time - data.futureStatus = generateDefaultFutureStatus(userState.IDLE, from, ""); - await docRefUser0.set(data); - const response = await cancelOooStatus(userId); - expect(response.userStatusExists).to.equal(true); - expect(response.data.userId).to.equal(userId); - expect(response.data.futureStatus.state).to.equal(undefined); + describe("getUserStatus", function () { + it("should fetch an existing user status document", async function () { + const userId = "get-existing-user"; + const statusData = buildUserStatus(userId, userState.IDLE); + await userStatusModel.doc("existing-status").set(statusData); + + const response = await getUserStatus(userId); + + expect(response.id).to.equal("existing-status"); + expect(response.userStatusExists).to.equal(true); + expect(response.data).to.deep.include({ + userId, + }); + expect(response.data.currentStatus.state).to.equal(userState.IDLE); + }); + + it("should return a not found response when the user status document does not exist", async function () { + const response = await getUserStatus("missing-user"); + + expect(response).to.deep.equal({ + id: null, + data: null, + userStatusExists: false, + }); + }); + + it("should throw an error when the database query fails", async function () { + sinon.stub(admin.firestore.Query.prototype, "where").throws(new Error("Unable to fetch user status")); + + try { + await getUserStatus("user-id"); + expect.fail("Should have thrown"); + } catch (err) { + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.equal("Unable to fetch user status"); + } + }); }); - it("should throw an error if unable to fetch the user status document", async function () { - sinon.stub(userStatusModel, "where").throws(new Error("Unable to fetch user status document")); - await cancelOooStatus(userId).catch((err) => { - expect(err).to.be.an.instanceOf(Error); - expect(err.message).to.be.equal("Unable to fetch user status document"); + describe("getAllUserStatus", function () { + it("should retrieve all user status documents when no state filter is provided", async function () { + const activeStatus = buildUserStatus("active-user", userState.ACTIVE); + await userStatusModel.doc("active-status").set(activeStatus); + await userStatusModel.doc("idle-status").set(buildUserStatus("idle-user", userState.IDLE, { idleFrom: 123 })); + + const response = await getAllUserStatus({}); + + expect(response.allUserStatus).to.have.length(2); + expect(response.allUserStatus).to.deep.include({ + id: "active-status", + userId: "active-user", + currentStatus: activeStatus.currentStatus, + monthlyHours: activeStatus.monthlyHours, + idleFrom: null, + }); + expect(response.allUserStatus.find((status) => status.id === "idle-status").idleFrom).to.equal(123); + }); + + it("should filter user status documents by current state", async function () { + await userStatusModel.doc("active-status").set( + buildUserStatus("active-user", userState.ACTIVE, { + currentStatus: { from: now() - ONE_DAY_IN_MS * 2 }, + }) + ); + await userStatusModel.doc("idle-status").set(buildUserStatus("idle-user", userState.IDLE)); + + const response = await getAllUserStatus({ state: userState.ACTIVE }); + + expect(response.allUserStatus).to.have.length(1); + expect(response.allUserStatus[0].id).to.equal("active-status"); + expect(response.allUserStatus[0].currentStatus.state).to.equal(userState.ACTIVE); + }); + + it("should throw an error when the database query fails", async function () { + sinon.stub(admin.firestore.Query.prototype, "get").throws(new Error("Unable to fetch all user status")); + + try { + await getAllUserStatus({}); + expect.fail("Should have thrown"); + } catch (err) { + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.equal("Unable to fetch all user status"); + } }); }); - it("Should throw error when no User status document found", async function () { - await cancelOooStatus("randomUserId").catch((err) => { - expect(err).to.be.an.instanceOf(Error); - expect(err).to.be.an.instanceOf(NotFound); - expect(err.message).to.be.equal("No User status document found"); + describe("updateUserStatus", function () { + it("should update an existing user status document", async function () { + const userId = "update-existing-user"; + await userStatusModel.doc("status-to-update").set(buildUserStatus(userId, userState.ACTIVE)); + + const updatedStatusData = { + currentStatus: buildCurrentStatus(userState.IDLE, { + message: "Wrapping up", + from: now(), + updatedAt: now(), + }), + }; + + const response = await updateUserStatus(userId, updatedStatusData); + const updatedDoc = await userStatusModel.doc("status-to-update").get(); + + expect(response.id).to.equal("status-to-update"); + expect(response.userStatusExists).to.equal(true); + expect(response.data.currentStatus.state).to.equal(userState.IDLE); + expect(updatedDoc.data().currentStatus.state).to.equal(userState.IDLE); + }); + + it("should move tomorrow's OOO status into futureStatus", async function () { + const userId = "future-ooo-user"; + await userStatusModel.doc("future-ooo-status").set(buildUserStatus(userId, userState.ACTIVE)); + + const response = await updateUserStatus(userId, { + currentStatus: buildCurrentStatus(userState.OOO, { + from: now() + ONE_DAY_IN_MS, + until: now() + ONE_DAY_IN_MS * 3, + message: "OOO tomorrow", + updatedAt: now(), + }), + }); + const updatedDoc = await userStatusModel.doc("future-ooo-status").get(); + + expect(response.data.currentStatus).to.equal(undefined); + expect(response.data.futureStatus.state).to.equal(userState.OOO); + expect(updatedDoc.data().futureStatus.state).to.equal(userState.OOO); + expect(updatedDoc.data().currentStatus.state).to.equal(userState.ACTIVE); + }); + + it("should clear futureStatus when OOO starts today", async function () { + const userId = "current-ooo-user"; + await userStatusModel.doc("current-ooo-status").set(buildUserStatus(userId, userState.ACTIVE)); + + const response = await updateUserStatus(userId, { + currentStatus: buildCurrentStatus(userState.OOO, { + from: now(), + until: now() + ONE_DAY_IN_MS, + message: "OOO today", + updatedAt: now(), + }), + }); + const updatedDoc = await userStatusModel.doc("current-ooo-status").get(); + + expect(response.data.currentStatus.state).to.equal(userState.OOO); + expect(response.data.futureStatus).to.deep.equal({}); + expect(updatedDoc.data().currentStatus.state).to.equal(userState.OOO); + expect(updatedDoc.data().futureStatus).to.deep.equal({}); + expect(updatedDoc.data().lastOooUntil).to.equal(null); + }); + + it("should clear stale futureStatus and persist lastOooUntil when an OOO user moves to ACTIVE", async function () { + const userId = "ooo-to-active-user"; + const oooUntil = now() + ONE_DAY_IN_MS; + await userStatusModel.doc("ooo-to-active-status").set( + buildUserStatus(userId, userState.OOO, { + currentStatus: { + until: oooUntil, + }, + futureStatus: buildCurrentStatus(userState.IDLE, { + from: oooUntil, + }), + }) + ); + + const response = await updateUserStatus(userId, { + currentStatus: buildCurrentStatus(userState.ACTIVE, { + from: now(), + updatedAt: now(), + }), + }); + const updatedDoc = await userStatusModel.doc("ooo-to-active-status").get(); + + expect(response.data.currentStatus.state).to.equal(userState.ACTIVE); + expect(response.data.futureStatus).to.deep.equal({}); + expect(response.data.lastOooUntil).to.equal(oooUntil); + expect(updatedDoc.data().futureStatus).to.deep.equal({}); + expect(updatedDoc.data().lastOooUntil).to.equal(oooUntil); + }); + + it("should remove the group idle Discord role when a user transitions out of IDLE", async function () { + fetchStub = sinon.stub(global, "fetch").resolves({ ok: true }); + const userId = await addUser(userData()[0]); + await seedGroupIdleRole(); + await memberRoleModel.doc("member-idle-role").set({ + roleid: "group-idle-role-id", + userid: userData()[0].discordId, + }); + await userStatusModel.doc("idle-status").set(buildUserStatus(userId, userState.IDLE)); + + const response = await updateUserStatus(userId, { + currentStatus: buildCurrentStatus(userState.ACTIVE, { + from: now(), + updatedAt: now(), + }), + }); + const memberRole = await memberRoleModel.doc("member-idle-role").get(); + + expect(response.data.currentStatus.state).to.equal(userState.ACTIVE); + expect(memberRole.exists).to.equal(false); + expect(fetchStub.calledOnce).to.equal(true); + expect(fetchStub.firstCall.args[1].method).to.equal("DELETE"); + }); + + it("should not call Discord when the group idle role does not exist", async function () { + fetchStub = sinon.stub(global, "fetch").resolves({ ok: true }); + const userId = await addUser(userData()[0]); + await userStatusModel.doc("idle-status-no-role").set(buildUserStatus(userId, userState.IDLE)); + + const response = await updateUserStatus(userId, { + currentStatus: buildCurrentStatus(userState.ACTIVE, { + from: now(), + updatedAt: now(), + }), + }); + + expect(response.data.currentStatus.state).to.equal(userState.ACTIVE); + expect(fetchStub.notCalled).to.equal(true); + }); + + it("should not call Discord when the user has no discordId", async function () { + fetchStub = sinon.stub(global, "fetch").resolves({ ok: true }); + const userId = await addUser(userData()[3]); + await seedGroupIdleRole(); + await userStatusModel.doc("idle-status-no-discord").set(buildUserStatus(userId, userState.IDLE)); + + const response = await updateUserStatus(userId, { + currentStatus: buildCurrentStatus(userState.ACTIVE, { + from: now(), + updatedAt: now(), + }), + }); + + expect(response.data.currentStatus.state).to.equal(userState.ACTIVE); + expect(fetchStub.notCalled).to.equal(true); + }); + + it("should call Discord DELETE even when the member idle role is not present in Firestore", async function () { + fetchStub = sinon.stub(global, "fetch").resolves({ ok: true }); + const userId = await addUser(userData()[0]); + await seedGroupIdleRole(); + await userStatusModel.doc("idle-status-no-member-role").set(buildUserStatus(userId, userState.IDLE)); + + const response = await updateUserStatus(userId, { + currentStatus: buildCurrentStatus(userState.ACTIVE, { + from: now(), + updatedAt: now(), + }), + }); + + expect(response.data.currentStatus.state).to.equal(userState.ACTIVE); + expect(fetchStub.calledOnce).to.equal(true); + expect(fetchStub.firstCall.args[1].method).to.equal("DELETE"); + }); + + it("should throw when Discord role removal fails", async function () { + fetchStub = sinon.stub(global, "fetch").rejects(new Error("Discord remove failed")); + const userId = await addUser(userData()[0]); + await seedGroupIdleRole(); + await userStatusModel.doc("idle-status-discord-failure").set(buildUserStatus(userId, userState.IDLE)); + + return updateUserStatus(userId, { + currentStatus: buildCurrentStatus(userState.ACTIVE, { + from: now(), + updatedAt: now(), + }), + }).catch((err) => { + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.equal("Discord remove failed"); + }); + }); + + it("should create a new user status document when one does not exist", async function () { + const userId = "new-status-user"; + + const response = await updateUserStatus(userId, { + currentStatus: buildCurrentStatus(userState.ACTIVE, { + from: now(), + updatedAt: now(), + }), + }); + const persistedStatus = await getUserStatus(userId); + + expect(response.userStatusExists).to.equal(false); + expect(response.id).to.be.a("string"); + expect(response.data.currentStatus.state).to.equal(userState.ACTIVE); + expect(persistedStatus.userStatusExists).to.equal(true); + expect(persistedStatus.data.lastOooUntil).to.equal(null); + }); + + it("should create a new user status document with futureStatus for future OOO", async function () { + const userId = "new-future-ooo-status-user"; + + const response = await updateUserStatus(userId, { + currentStatus: buildCurrentStatus(userState.OOO, { + from: now() + ONE_DAY_IN_MS, + until: now() + ONE_DAY_IN_MS * 3, + message: "Future OOO", + updatedAt: now(), + }), + }); + const persistedStatus = await getUserStatus(userId); + + expect(response.userStatusExists).to.equal(false); + expect(response.data.currentStatus).to.equal(undefined); + expect(response.data.futureStatus.state).to.equal(userState.OOO); + expect(persistedStatus.userStatusExists).to.equal(true); + expect(persistedStatus.data.currentStatus).to.equal(undefined); + expect(persistedStatus.data.futureStatus.state).to.equal(userState.OOO); + expect(persistedStatus.data.lastOooUntil).to.equal(null); + }); + + it("should throw an error when the database update fails", async function () { + const userId = "update-failure-user"; + await userStatusModel.doc("status-update-failure").set(buildUserStatus(userId, userState.ACTIVE)); + sinon + .stub(admin.firestore.DocumentReference.prototype, "update") + .rejects(new Error("Unable to update user status")); + + try { + await updateUserStatus(userId, { + currentStatus: buildCurrentStatus(userState.IDLE, { + from: now(), + updatedAt: now(), + }), + }); + expect.fail("Should have thrown"); + } catch (err) { + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.equal("Unable to update user status"); + } }); }); - it("Should throw an error if the status is not OOO", async function () { - const data = generateStatusDataForCancelOOO(userId, userState.ACTIVE); - await docRefUser0.set(data); - await cancelOooStatus(userId).catch((err) => { - expect(err).to.be.an.instanceOf(Error); - expect(err).to.be.an.instanceOf(Forbidden); - expect(err.message).to.be.equal("The OOO Status cannot be canceled because the current status is ACTIVE."); + describe("updateAllUserStatus", function () { + it("should transition a user's future status to current status after the futureStatus from date has passed", async function () { + const userId = "ooo-to-active-user"; + const oooUntil = now() - ONE_DAY_IN_MS; + await userStatusModel.doc("ooo-to-active-status").set( + buildUserStatus(userId, userState.OOO, { + currentStatus: { + until: oooUntil, + }, + futureStatus: buildCurrentStatus(userState.ACTIVE, { + from: now() - ONE_DAY_IN_MS, + }), + }) + ); + + const summary = await updateAllUserStatus(); + const updatedDoc = await userStatusModel.doc("ooo-to-active-status").get(); + + expect(summary).to.deep.include({ + usersCount: 1, + oooUsersAltered: 1, + }); + expect(updatedDoc.data().currentStatus.state).to.equal(userState.ACTIVE); + expect(updatedDoc.data().futureStatus).to.equal(undefined); + expect(updatedDoc.data().lastOooUntil).to.equal(oooUntil); + }); + + it("should add the group idle Discord role when a processed transition moves a user into IDLE", async function () { + fetchStub = sinon.stub(global, "fetch").resolves({ ok: true }); + const userId = await addUser(userData()[0]); + await seedGroupIdleRole(); + await userStatusModel.doc("ooo-to-idle-status").set( + buildUserStatus(userId, userState.OOO, { + currentStatus: { + until: now() - ONE_DAY_IN_MS, + }, + futureStatus: buildCurrentStatus(userState.IDLE, { + from: now() - ONE_DAY_IN_MS, + }), + }) + ); + + await updateAllUserStatus(); + const memberRoles = await memberRoleModel + .where("roleid", "==", "group-idle-role-id") + .where("userid", "==", userData()[0].discordId) + .get(); + + expect(memberRoles.empty).to.equal(false); + expect(fetchStub.calledOnce).to.equal(true); + expect(fetchStub.firstCall.args[1].method).to.equal("PUT"); + }); + + it("should not duplicate the group idle Firestore role when the user already has it", async function () { + fetchStub = sinon.stub(global, "fetch").resolves({ ok: true }); + const userId = await addUser(userData()[0]); + await seedGroupIdleRole(); + await memberRoleModel.doc("existing-member-idle-role").set({ + roleid: "group-idle-role-id", + userid: userData()[0].discordId, + }); + await userStatusModel.doc("ooo-to-idle-existing-member-role").set( + buildUserStatus(userId, userState.OOO, { + currentStatus: { + until: now() - ONE_DAY_IN_MS, + }, + futureStatus: buildCurrentStatus(userState.IDLE, { + from: now() - ONE_DAY_IN_MS, + }), + }) + ); + + await updateAllUserStatus(); + const memberRoles = await getGroupIdleMemberRolesForUser(userData()[0].discordId); + + expect(memberRoles.size).to.equal(1); + expect(fetchStub.calledOnce).to.equal(true); + expect(fetchStub.firstCall.args[1].method).to.equal("PUT"); + }); + + it("should remove the group idle Discord role when a processed transition moves a user out of IDLE", async function () { + fetchStub = sinon.stub(global, "fetch").resolves({ ok: true }); + const userId = await addUser(userData()[0]); + await seedGroupIdleRole(); + await memberRoleModel.doc("member-idle-role").set({ + roleid: "group-idle-role-id", + userid: userData()[0].discordId, + }); + await userStatusModel.doc("idle-to-active-status").set( + buildUserStatus(userId, userState.IDLE, { + futureStatus: buildCurrentStatus(userState.ACTIVE, { + from: now() - ONE_DAY_IN_MS, + }), + }) + ); + + await updateAllUserStatus(); + const memberRole = await memberRoleModel.doc("member-idle-role").get(); + + expect(memberRole.exists).to.equal(false); + expect(fetchStub.calledOnce).to.equal(true); + expect(fetchStub.firstCall.args[1].method).to.equal("DELETE"); + }); + + it("should remove an expired future OOO status", async function () { + await userStatusModel.doc("expired-future-ooo-status").set( + buildUserStatus("expired-future-ooo-user", userState.ACTIVE, { + futureStatus: buildCurrentStatus(userState.OOO, { + from: now() - ONE_DAY_IN_MS * 3, + until: now() - ONE_DAY_IN_MS, + }), + }) + ); + + const summary = await updateAllUserStatus(); + const updatedDoc = await userStatusModel.doc("expired-future-ooo-status").get(); + + expect(summary.nonOooUsersAltered).to.equal(1); + expect(updatedDoc.data().currentStatus.state).to.equal(userState.ACTIVE); + expect(updatedDoc.data().futureStatus).to.equal(undefined); + }); + + it("should swap current and future statuses when today lies within a future OOO range", async function () { + const until = now() + ONE_DAY_IN_MS; + await userStatusModel.doc("active-to-ooo-status").set( + buildUserStatus("active-to-ooo-user", userState.ACTIVE, { + futureStatus: buildCurrentStatus(userState.OOO, { + from: now() - ONE_DAY_IN_MS, + until, + }), + }) + ); + + const summary = await updateAllUserStatus(); + const updatedDoc = await userStatusModel.doc("active-to-ooo-status").get(); + + expect(summary.nonOooUsersAltered).to.equal(1); + expect(updatedDoc.data().currentStatus.state).to.equal(userState.OOO); + expect(updatedDoc.data().futureStatus.state).to.equal(userState.ACTIVE); + expect(updatedDoc.data().futureStatus.from).to.equal(until); + expect(updatedDoc.data().lastOooUntil).to.equal(null); + }); + + it("should swap future OOO into currentStatus and set an empty futureStatus when no current state exists", async function () { + const until = now() + ONE_DAY_IN_MS; + await userStatusModel.doc("missing-current-to-ooo-status").set({ + userId: "missing-current-to-ooo-user", + futureStatus: buildCurrentStatus(userState.OOO, { + from: now() - ONE_DAY_IN_MS, + until, + }), + }); + + const summary = await updateAllUserStatus(); + const updatedDoc = await userStatusModel.doc("missing-current-to-ooo-status").get(); + + expect(summary.nonOooUsersAltered).to.equal(1); + expect(updatedDoc.data().currentStatus.state).to.equal(userState.OOO); + expect(updatedDoc.data().futureStatus).to.deep.equal({}); + expect(updatedDoc.data().lastOooUntil).to.equal(null); + }); + + it("should return correct summary statistics for altered and unaltered statuses", async function () { + await userStatusModel.doc("ooo-altered").set( + buildUserStatus("ooo-altered-user", userState.OOO, { + futureStatus: buildCurrentStatus(userState.ACTIVE, { + from: now() - ONE_DAY_IN_MS, + }), + }) + ); + await userStatusModel.doc("ooo-unaltered").set( + buildUserStatus("ooo-unaltered-user", userState.OOO, { + futureStatus: buildCurrentStatus(userState.IDLE, { + from: now() + ONE_DAY_IN_MS, + }), + }) + ); + await userStatusModel.doc("non-ooo-altered").set( + buildUserStatus("non-ooo-altered-user", userState.ACTIVE, { + futureStatus: buildCurrentStatus(userState.OOO, { + from: now() - ONE_DAY_IN_MS, + until: now() + ONE_DAY_IN_MS, + }), + }) + ); + await userStatusModel.doc("non-ooo-unaltered").set( + buildUserStatus("non-ooo-unaltered-user", userState.ACTIVE, { + futureStatus: buildCurrentStatus(userState.OOO, { + from: now() + ONE_DAY_IN_MS, + until: now() + ONE_DAY_IN_MS * 3, + }), + }) + ); + + const summary = await updateAllUserStatus(); + + expect(summary).to.deep.equal({ + usersCount: 2, + oooUsersAltered: 1, + oooUsersUnaltered: 0, + nonOooUsersAltered: 1, + nonOooUsersUnaltered: 0, + }); + }); + + it("should log a warning when more than 100 user status documents are updated", async function () { + const loggerInfoStub = sinon.stub(logger, "info"); + const statusPromises = []; + for (let index = 0; index < 101; index++) { + statusPromises.push( + userStatusModel.doc(`status-${index}`).set( + buildUserStatus(`user-${index}`, userState.ACTIVE, { + futureStatus: buildCurrentStatus(userState.OOO, { + from: now() - ONE_DAY_IN_MS, + until: now() + ONE_DAY_IN_MS, + }), + }) + ) + ); + } + await Promise.all(statusPromises); + + await updateAllUserStatus(); + + expect(loggerInfoStub.calledOnce).to.equal(true); + expect(loggerInfoStub.firstCall.args[0]).to.include("Warning: More than 100 User Status documents to update"); + }); + + it("should return an error response when the user status query fails", async function () { + sinon.stub(admin.firestore.Query.prototype, "where").throws(new Error("Unable to query future statuses")); + + const response = await updateAllUserStatus(); + + expect(response).to.deep.equal({ + status: 500, + message: "User Status couldn't be updated Successfully.", + }); + }); + + it("should return an error response when the batch commit fails", async function () { + await userStatusModel.doc("batch-failure-status").set( + buildUserStatus("batch-failure-user", userState.ACTIVE, { + futureStatus: buildCurrentStatus(userState.OOO, { + from: now() - ONE_DAY_IN_MS, + until: now() + ONE_DAY_IN_MS, + }), + }) + ); + sinon.stub(firestore, "batch").returns({ + _ops: [], + set: sinon.stub(), + commit: sinon.stub().rejects(new Error("Batch operation failed")), + }); + + const response = await updateAllUserStatus(); + + expect(response).to.deep.equal({ + status: 500, + message: "User Status couldn't be updated Successfully.", + }); }); }); - it("should throw an error if unable to fetch task assigned to user.", async function () { - sinon.stub(tasksModel, "where").throws(new Error("Task not found")); - await cancelOooStatus(userId).catch((err) => { - expect(err).to.be.an.instanceOf(Error); - expect(err.message).to.be.equal("Task not found"); + describe("getGroupRole", function () { + it("should return roleExists false when role name is empty or missing", async function () { + expect(await getGroupRole()).to.deep.equal({ roleExists: false }); + expect(await getGroupRole("")).to.deep.equal({ roleExists: false }); + }); + + it("should return roleExists false when the role does not exist", async function () { + const response = await getGroupRole("group-idle"); + + expect(response).to.deep.equal({ roleExists: false }); + }); + + it("should return the role when it exists", async function () { + await seedGroupIdleRole(); + + const response = await getGroupRole("group-idle"); + + expect(response.roleExists).to.equal(true); + expect(response.role).to.deep.equal({ + id: "group-idle-doc", + rolename: "group-idle", + roleid: "group-idle-role-id", + }); + }); + + it("should throw an error when the database fetch fails", async function () { + sinon.stub(admin.firestore.Query.prototype, "where").throws(new Error("Unable to fetch role")); + + try { + await getGroupRole("group-idle"); + expect.fail("Should have thrown"); + } catch (err) { + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.equal("Unable to fetch role"); + } }); }); - it("Should add future status to the User", async function () { - const response = await addFutureStatus(userFutureStatusData); - expect(response.userStatusExists).to.equal(true); - expect(response.data.futureStatus.state).to.equal("UPCOMING"); + describe("cancelOooStatus and addFutureStatus coverage", function () { + let userId; + let docRefUser0; + + beforeEach(async function () { + userId = await addUser(); + docRefUser0 = userStatusModel.doc(); + const data = generateStatusDataForCancelOOO(userId, userState.OOO); + await docRefUser0.set(data); + }); + + it("Should cancel the OOO Status of the User", async function () { + const response = await cancelOooStatus(userId); + expect(response.userStatusExists).to.equal(true); + expect(response.data.userId).to.equal(userId); + expect(response.data.currentStatus).to.not.equal(userState.OOO); + expect(response.data.futureStatus?.state).to.equal(undefined); + }); + + it("Should clear the future Status if the User cancels OOO", async function () { + const data = generateStatusDataForCancelOOO(userId, userState.OOO); + const from = now() + ONE_DAY_IN_MS; + data.futureStatus = generateDefaultFutureStatus(userState.IDLE, from, ""); + await docRefUser0.set(data); + const response = await cancelOooStatus(userId); + expect(response.userStatusExists).to.equal(true); + expect(response.data.userId).to.equal(userId); + expect(response.data.futureStatus.state).to.equal(undefined); + }); + + it("should transition to ACTIVE when the OOO user has active tasks", async function () { + await addLiveTaskForUser(userId); + + const response = await cancelOooStatus(userId); + + expect(response.userStatusExists).to.equal(true); + expect(response.data.currentStatus.state).to.equal(userState.ACTIVE); + }); + + it("should add the group idle Discord role and persist lastOooUntil when OOO is canceled without active tasks", async function () { + fetchStub = sinon.stub(global, "fetch").resolves({ ok: true }); + await seedGroupIdleRole(); + const oooUntil = now() + ONE_DAY_IN_MS; + const data = generateStatusDataForCancelOOO(userId, userState.OOO); + data.currentStatus.until = oooUntil; + await docRefUser0.set(data); + + const response = await cancelOooStatus(userId); + const memberRoles = await getGroupIdleMemberRolesForUser(userData()[0].discordId); + + expect(response.data.currentStatus.state).to.equal(userState.IDLE); + expect(response.data.lastOooUntil).to.equal(oooUntil); + expect(memberRoles.size).to.equal(1); + expect(fetchStub.calledOnce).to.equal(true); + expect(fetchStub.firstCall.args[1].method).to.equal("PUT"); + }); + + it("should throw an error if unable to fetch the user status document", async function () { + sinon.stub(admin.firestore.Query.prototype, "where").throws(new Error("Unable to fetch user status document")); + try { + await cancelOooStatus(userId); + expect.fail("Should have thrown"); + } catch (err) { + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.be.equal("Unable to fetch user status document"); + } + }); + + it("Should throw error when no User status document found", async function () { + await cancelOooStatus("randomUserId").catch((err) => { + expect(err).to.be.an.instanceOf(Error); + expect(err).to.be.an.instanceOf(NotFound); + expect(err.message).to.be.equal("No User status document found"); + }); + }); + + it("Should throw an error if the status is not OOO", async function () { + const data = generateStatusDataForCancelOOO(userId, userState.ACTIVE); + await docRefUser0.set(data); + await cancelOooStatus(userId).catch((err) => { + expect(err).to.be.an.instanceOf(Error); + expect(err).to.be.an.instanceOf(Forbidden); + expect(err.message).to.be.equal("The OOO Status cannot be canceled because the current status is ACTIVE."); + }); + }); + + it("should throw an error if unable to fetch task assigned to user.", async function () { + sinon.stub(tasksModel, "where").throws(new Error("Task not found")); + await cancelOooStatus(userId).catch((err) => { + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.be.equal("Task not found"); + }); + }); + + it("Should add future status to the User", async function () { + const response = await addFutureStatus({ ...userFutureStatusData }); + expect(response.userStatusExists).to.equal(true); + expect(response.data.futureStatus.state).to.equal("UPCOMING"); + }); + + it("should create a user status document when adding future status for a new user", async function () { + const futureStatusData = { + ...userFutureStatusData, + userId: "new-future-status-user", + }; + + const response = await addFutureStatus(futureStatusData); + const persistedStatus = await getUserStatus("new-future-status-user"); + + expect(response.userStatusExists).to.equal(true); + expect(response.data.userId).to.equal("new-future-status-user"); + expect(response.data.futureStatus.state).to.equal("UPCOMING"); + expect(persistedStatus.userStatusExists).to.equal(true); + expect(persistedStatus.data.futureStatus.state).to.equal("UPCOMING"); + }); }); }); From 973eca144dafbde7b741c5481b24f4af8863846b Mon Sep 17 00:00:00 2001 From: Soumava Das Date: Tue, 14 Jul 2026 01:37:26 +0530 Subject: [PATCH 04/10] Revert "fixed missing test cases" This reverts commit e6151c4b6b4c70e6fb19873b7d11eafd8683740a. --- test/unit/models/userStatus.js | 890 ++------------------------------- 1 file changed, 51 insertions(+), 839 deletions(-) diff --git a/test/unit/models/userStatus.js b/test/unit/models/userStatus.js index 8271cf647..d537ce829 100644 --- a/test/unit/models/userStatus.js +++ b/test/unit/models/userStatus.js @@ -1,877 +1,89 @@ -const { userFutureStatusData } = require("../../fixtures/userFutureStatus/userFutureStatusData"); +import { userFutureStatusData } from "../../fixtures/userFutureStatus/userFutureStatusData"; const chai = require("chai"); const sinon = require("sinon"); -const admin = require("firebase-admin"); const { NotFound, Forbidden } = require("http-errors"); const { expect } = chai; const firestore = require("../../../utils/firestore"); -const logger = require("../../../utils/logger"); -global.logger = logger; - const userStatusModel = firestore.collection("usersStatus"); const tasksModel = firestore.collection("tasks"); -const discordRoleModel = firestore.collection("discord-roles"); -const memberRoleModel = firestore.collection("member-group-roles"); -const { - deleteUserStatus, - getUserStatus, - getAllUserStatus, - updateUserStatus, - updateAllUserStatus, - getGroupRole, - cancelOooStatus, - addFutureStatus, -} = require("../../../models/userStatus"); +const { cancelOooStatus, addFutureStatus } = require("../../../models/userStatus"); const cleanDb = require("../../utils/cleanDb"); const addUser = require("../../utils/addUser"); -const userData = require("../../fixtures/user/user"); -const allTasks = require("../../fixtures/tasks/tasks"); const { userState } = require("../../../constants/userStatus"); -const { TASK_STATUS } = require("../../../constants/tasks"); -const { ONE_DAY_IN_MS } = require("../../../constants/users"); const { generateStatusDataForCancelOOO, generateDefaultFutureStatus } = require("../../fixtures/userStatus/userStatus"); -const now = () => Date.now(); - -const buildCurrentStatus = (state, overrides = {}) => ({ - message: "", - from: now() - ONE_DAY_IN_MS, - until: "", - updatedAt: now() - ONE_DAY_IN_MS, - state, - ...overrides, -}); - -const buildUserStatus = (userId, state = userState.ACTIVE, overrides = {}) => { - const { currentStatus, ...statusOverrides } = overrides; - return { - userId, - currentStatus: buildCurrentStatus(state, currentStatus), - monthlyHours: { - committed: 40, - updatedAt: now() - ONE_DAY_IN_MS, - }, - ...statusOverrides, - }; -}; - -const seedGroupIdleRole = async () => { - await discordRoleModel.doc("group-idle-doc").set({ - rolename: "group-idle", - roleid: "group-idle-role-id", - }); -}; - -const getGroupIdleMemberRolesForUser = (discordId) => { - return memberRoleModel.where("roleid", "==", "group-idle-role-id").where("userid", "==", discordId).get(); -}; +describe("tasks", function () { + let userId; + let docRefUser0; -const addLiveTaskForUser = async (userId) => { - const [task] = allTasks(); - await tasksModel.doc().set({ - ...task, - assignee: userId, - status: TASK_STATUS.ASSIGNED, + beforeEach(async function () { + userId = await addUser(); + docRefUser0 = userStatusModel.doc(); + const data = generateStatusDataForCancelOOO(userId, userState.OOO); + await docRefUser0.set(data); }); -}; - -describe("User Status Model", function () { - let fetchStub; afterEach(async function () { sinon.restore(); - if (fetchStub) { - fetchStub = null; - } await cleanDb(); }); - describe("deleteUserStatus", function () { - it("should delete an existing user status document", async function () { - const userId = "delete-existing-user"; - await userStatusModel.doc("status-to-delete").set(buildUserStatus(userId)); - - const response = await deleteUserStatus(userId); - const deletedDoc = await userStatusModel.doc("status-to-delete").get(); - - expect(response).to.deep.equal({ - id: "status-to-delete", - userStatusExisted: true, - userStatusDeleted: true, - }); - expect(deletedDoc.exists).to.equal(false); - }); - - it("should return a not found response when the user status document does not exist", async function () { - const response = await deleteUserStatus("missing-user"); - - expect(response).to.deep.equal({ - id: null, - userStatusExisted: false, - userStatusDeleted: false, - }); - }); - - it("should throw an error when the database query fails", async function () { - sinon.stub(admin.firestore.Query.prototype, "where").throws(new Error("Unable to delete user status")); - - try { - await deleteUserStatus("user-id"); - expect.fail("Should have thrown"); - } catch (err) { - expect(err).to.be.an.instanceOf(Error); - expect(err.message).to.equal("Unable to delete user status"); - } - }); + it("Should cancel the OOO Status of the User", async function () { + const response = await cancelOooStatus(userId); + expect(response.userStatusExists).to.equal(true); + expect(response.data.userId).to.equal(userId); + expect(response.data.currentStatus).to.not.equal(userState.OOO); + expect(response.data.futureStatus?.state).to.equal(undefined); }); - describe("getUserStatus", function () { - it("should fetch an existing user status document", async function () { - const userId = "get-existing-user"; - const statusData = buildUserStatus(userId, userState.IDLE); - await userStatusModel.doc("existing-status").set(statusData); - - const response = await getUserStatus(userId); - - expect(response.id).to.equal("existing-status"); - expect(response.userStatusExists).to.equal(true); - expect(response.data).to.deep.include({ - userId, - }); - expect(response.data.currentStatus.state).to.equal(userState.IDLE); - }); - - it("should return a not found response when the user status document does not exist", async function () { - const response = await getUserStatus("missing-user"); - - expect(response).to.deep.equal({ - id: null, - data: null, - userStatusExists: false, - }); - }); - - it("should throw an error when the database query fails", async function () { - sinon.stub(admin.firestore.Query.prototype, "where").throws(new Error("Unable to fetch user status")); - - try { - await getUserStatus("user-id"); - expect.fail("Should have thrown"); - } catch (err) { - expect(err).to.be.an.instanceOf(Error); - expect(err.message).to.equal("Unable to fetch user status"); - } - }); + it("Should clear the future Status if the User cancels OOO", async function () { + const data = generateStatusDataForCancelOOO(userId, userState.OOO); + const from = new Date().getTime() + 24 * 60 * 60 * 1000; // 1 day offset from current time + data.futureStatus = generateDefaultFutureStatus(userState.IDLE, from, ""); + await docRefUser0.set(data); + const response = await cancelOooStatus(userId); + expect(response.userStatusExists).to.equal(true); + expect(response.data.userId).to.equal(userId); + expect(response.data.futureStatus.state).to.equal(undefined); }); - describe("getAllUserStatus", function () { - it("should retrieve all user status documents when no state filter is provided", async function () { - const activeStatus = buildUserStatus("active-user", userState.ACTIVE); - await userStatusModel.doc("active-status").set(activeStatus); - await userStatusModel.doc("idle-status").set(buildUserStatus("idle-user", userState.IDLE, { idleFrom: 123 })); - - const response = await getAllUserStatus({}); - - expect(response.allUserStatus).to.have.length(2); - expect(response.allUserStatus).to.deep.include({ - id: "active-status", - userId: "active-user", - currentStatus: activeStatus.currentStatus, - monthlyHours: activeStatus.monthlyHours, - idleFrom: null, - }); - expect(response.allUserStatus.find((status) => status.id === "idle-status").idleFrom).to.equal(123); - }); - - it("should filter user status documents by current state", async function () { - await userStatusModel.doc("active-status").set( - buildUserStatus("active-user", userState.ACTIVE, { - currentStatus: { from: now() - ONE_DAY_IN_MS * 2 }, - }) - ); - await userStatusModel.doc("idle-status").set(buildUserStatus("idle-user", userState.IDLE)); - - const response = await getAllUserStatus({ state: userState.ACTIVE }); - - expect(response.allUserStatus).to.have.length(1); - expect(response.allUserStatus[0].id).to.equal("active-status"); - expect(response.allUserStatus[0].currentStatus.state).to.equal(userState.ACTIVE); - }); - - it("should throw an error when the database query fails", async function () { - sinon.stub(admin.firestore.Query.prototype, "get").throws(new Error("Unable to fetch all user status")); - - try { - await getAllUserStatus({}); - expect.fail("Should have thrown"); - } catch (err) { - expect(err).to.be.an.instanceOf(Error); - expect(err.message).to.equal("Unable to fetch all user status"); - } + it("should throw an error if unable to fetch the user status document", async function () { + sinon.stub(userStatusModel, "where").throws(new Error("Unable to fetch user status document")); + await cancelOooStatus(userId).catch((err) => { + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.be.equal("Unable to fetch user status document"); }); }); - describe("updateUserStatus", function () { - it("should update an existing user status document", async function () { - const userId = "update-existing-user"; - await userStatusModel.doc("status-to-update").set(buildUserStatus(userId, userState.ACTIVE)); - - const updatedStatusData = { - currentStatus: buildCurrentStatus(userState.IDLE, { - message: "Wrapping up", - from: now(), - updatedAt: now(), - }), - }; - - const response = await updateUserStatus(userId, updatedStatusData); - const updatedDoc = await userStatusModel.doc("status-to-update").get(); - - expect(response.id).to.equal("status-to-update"); - expect(response.userStatusExists).to.equal(true); - expect(response.data.currentStatus.state).to.equal(userState.IDLE); - expect(updatedDoc.data().currentStatus.state).to.equal(userState.IDLE); - }); - - it("should move tomorrow's OOO status into futureStatus", async function () { - const userId = "future-ooo-user"; - await userStatusModel.doc("future-ooo-status").set(buildUserStatus(userId, userState.ACTIVE)); - - const response = await updateUserStatus(userId, { - currentStatus: buildCurrentStatus(userState.OOO, { - from: now() + ONE_DAY_IN_MS, - until: now() + ONE_DAY_IN_MS * 3, - message: "OOO tomorrow", - updatedAt: now(), - }), - }); - const updatedDoc = await userStatusModel.doc("future-ooo-status").get(); - - expect(response.data.currentStatus).to.equal(undefined); - expect(response.data.futureStatus.state).to.equal(userState.OOO); - expect(updatedDoc.data().futureStatus.state).to.equal(userState.OOO); - expect(updatedDoc.data().currentStatus.state).to.equal(userState.ACTIVE); - }); - - it("should clear futureStatus when OOO starts today", async function () { - const userId = "current-ooo-user"; - await userStatusModel.doc("current-ooo-status").set(buildUserStatus(userId, userState.ACTIVE)); - - const response = await updateUserStatus(userId, { - currentStatus: buildCurrentStatus(userState.OOO, { - from: now(), - until: now() + ONE_DAY_IN_MS, - message: "OOO today", - updatedAt: now(), - }), - }); - const updatedDoc = await userStatusModel.doc("current-ooo-status").get(); - - expect(response.data.currentStatus.state).to.equal(userState.OOO); - expect(response.data.futureStatus).to.deep.equal({}); - expect(updatedDoc.data().currentStatus.state).to.equal(userState.OOO); - expect(updatedDoc.data().futureStatus).to.deep.equal({}); - expect(updatedDoc.data().lastOooUntil).to.equal(null); - }); - - it("should clear stale futureStatus and persist lastOooUntil when an OOO user moves to ACTIVE", async function () { - const userId = "ooo-to-active-user"; - const oooUntil = now() + ONE_DAY_IN_MS; - await userStatusModel.doc("ooo-to-active-status").set( - buildUserStatus(userId, userState.OOO, { - currentStatus: { - until: oooUntil, - }, - futureStatus: buildCurrentStatus(userState.IDLE, { - from: oooUntil, - }), - }) - ); - - const response = await updateUserStatus(userId, { - currentStatus: buildCurrentStatus(userState.ACTIVE, { - from: now(), - updatedAt: now(), - }), - }); - const updatedDoc = await userStatusModel.doc("ooo-to-active-status").get(); - - expect(response.data.currentStatus.state).to.equal(userState.ACTIVE); - expect(response.data.futureStatus).to.deep.equal({}); - expect(response.data.lastOooUntil).to.equal(oooUntil); - expect(updatedDoc.data().futureStatus).to.deep.equal({}); - expect(updatedDoc.data().lastOooUntil).to.equal(oooUntil); - }); - - it("should remove the group idle Discord role when a user transitions out of IDLE", async function () { - fetchStub = sinon.stub(global, "fetch").resolves({ ok: true }); - const userId = await addUser(userData()[0]); - await seedGroupIdleRole(); - await memberRoleModel.doc("member-idle-role").set({ - roleid: "group-idle-role-id", - userid: userData()[0].discordId, - }); - await userStatusModel.doc("idle-status").set(buildUserStatus(userId, userState.IDLE)); - - const response = await updateUserStatus(userId, { - currentStatus: buildCurrentStatus(userState.ACTIVE, { - from: now(), - updatedAt: now(), - }), - }); - const memberRole = await memberRoleModel.doc("member-idle-role").get(); - - expect(response.data.currentStatus.state).to.equal(userState.ACTIVE); - expect(memberRole.exists).to.equal(false); - expect(fetchStub.calledOnce).to.equal(true); - expect(fetchStub.firstCall.args[1].method).to.equal("DELETE"); - }); - - it("should not call Discord when the group idle role does not exist", async function () { - fetchStub = sinon.stub(global, "fetch").resolves({ ok: true }); - const userId = await addUser(userData()[0]); - await userStatusModel.doc("idle-status-no-role").set(buildUserStatus(userId, userState.IDLE)); - - const response = await updateUserStatus(userId, { - currentStatus: buildCurrentStatus(userState.ACTIVE, { - from: now(), - updatedAt: now(), - }), - }); - - expect(response.data.currentStatus.state).to.equal(userState.ACTIVE); - expect(fetchStub.notCalled).to.equal(true); - }); - - it("should not call Discord when the user has no discordId", async function () { - fetchStub = sinon.stub(global, "fetch").resolves({ ok: true }); - const userId = await addUser(userData()[3]); - await seedGroupIdleRole(); - await userStatusModel.doc("idle-status-no-discord").set(buildUserStatus(userId, userState.IDLE)); - - const response = await updateUserStatus(userId, { - currentStatus: buildCurrentStatus(userState.ACTIVE, { - from: now(), - updatedAt: now(), - }), - }); - - expect(response.data.currentStatus.state).to.equal(userState.ACTIVE); - expect(fetchStub.notCalled).to.equal(true); - }); - - it("should call Discord DELETE even when the member idle role is not present in Firestore", async function () { - fetchStub = sinon.stub(global, "fetch").resolves({ ok: true }); - const userId = await addUser(userData()[0]); - await seedGroupIdleRole(); - await userStatusModel.doc("idle-status-no-member-role").set(buildUserStatus(userId, userState.IDLE)); - - const response = await updateUserStatus(userId, { - currentStatus: buildCurrentStatus(userState.ACTIVE, { - from: now(), - updatedAt: now(), - }), - }); - - expect(response.data.currentStatus.state).to.equal(userState.ACTIVE); - expect(fetchStub.calledOnce).to.equal(true); - expect(fetchStub.firstCall.args[1].method).to.equal("DELETE"); - }); - - it("should throw when Discord role removal fails", async function () { - fetchStub = sinon.stub(global, "fetch").rejects(new Error("Discord remove failed")); - const userId = await addUser(userData()[0]); - await seedGroupIdleRole(); - await userStatusModel.doc("idle-status-discord-failure").set(buildUserStatus(userId, userState.IDLE)); - - return updateUserStatus(userId, { - currentStatus: buildCurrentStatus(userState.ACTIVE, { - from: now(), - updatedAt: now(), - }), - }).catch((err) => { - expect(err).to.be.an.instanceOf(Error); - expect(err.message).to.equal("Discord remove failed"); - }); - }); - - it("should create a new user status document when one does not exist", async function () { - const userId = "new-status-user"; - - const response = await updateUserStatus(userId, { - currentStatus: buildCurrentStatus(userState.ACTIVE, { - from: now(), - updatedAt: now(), - }), - }); - const persistedStatus = await getUserStatus(userId); - - expect(response.userStatusExists).to.equal(false); - expect(response.id).to.be.a("string"); - expect(response.data.currentStatus.state).to.equal(userState.ACTIVE); - expect(persistedStatus.userStatusExists).to.equal(true); - expect(persistedStatus.data.lastOooUntil).to.equal(null); - }); - - it("should create a new user status document with futureStatus for future OOO", async function () { - const userId = "new-future-ooo-status-user"; - - const response = await updateUserStatus(userId, { - currentStatus: buildCurrentStatus(userState.OOO, { - from: now() + ONE_DAY_IN_MS, - until: now() + ONE_DAY_IN_MS * 3, - message: "Future OOO", - updatedAt: now(), - }), - }); - const persistedStatus = await getUserStatus(userId); - - expect(response.userStatusExists).to.equal(false); - expect(response.data.currentStatus).to.equal(undefined); - expect(response.data.futureStatus.state).to.equal(userState.OOO); - expect(persistedStatus.userStatusExists).to.equal(true); - expect(persistedStatus.data.currentStatus).to.equal(undefined); - expect(persistedStatus.data.futureStatus.state).to.equal(userState.OOO); - expect(persistedStatus.data.lastOooUntil).to.equal(null); - }); - - it("should throw an error when the database update fails", async function () { - const userId = "update-failure-user"; - await userStatusModel.doc("status-update-failure").set(buildUserStatus(userId, userState.ACTIVE)); - sinon - .stub(admin.firestore.DocumentReference.prototype, "update") - .rejects(new Error("Unable to update user status")); - - try { - await updateUserStatus(userId, { - currentStatus: buildCurrentStatus(userState.IDLE, { - from: now(), - updatedAt: now(), - }), - }); - expect.fail("Should have thrown"); - } catch (err) { - expect(err).to.be.an.instanceOf(Error); - expect(err.message).to.equal("Unable to update user status"); - } + it("Should throw error when no User status document found", async function () { + await cancelOooStatus("randomUserId").catch((err) => { + expect(err).to.be.an.instanceOf(Error); + expect(err).to.be.an.instanceOf(NotFound); + expect(err.message).to.be.equal("No User status document found"); }); }); - describe("updateAllUserStatus", function () { - it("should transition a user's future status to current status after the futureStatus from date has passed", async function () { - const userId = "ooo-to-active-user"; - const oooUntil = now() - ONE_DAY_IN_MS; - await userStatusModel.doc("ooo-to-active-status").set( - buildUserStatus(userId, userState.OOO, { - currentStatus: { - until: oooUntil, - }, - futureStatus: buildCurrentStatus(userState.ACTIVE, { - from: now() - ONE_DAY_IN_MS, - }), - }) - ); - - const summary = await updateAllUserStatus(); - const updatedDoc = await userStatusModel.doc("ooo-to-active-status").get(); - - expect(summary).to.deep.include({ - usersCount: 1, - oooUsersAltered: 1, - }); - expect(updatedDoc.data().currentStatus.state).to.equal(userState.ACTIVE); - expect(updatedDoc.data().futureStatus).to.equal(undefined); - expect(updatedDoc.data().lastOooUntil).to.equal(oooUntil); - }); - - it("should add the group idle Discord role when a processed transition moves a user into IDLE", async function () { - fetchStub = sinon.stub(global, "fetch").resolves({ ok: true }); - const userId = await addUser(userData()[0]); - await seedGroupIdleRole(); - await userStatusModel.doc("ooo-to-idle-status").set( - buildUserStatus(userId, userState.OOO, { - currentStatus: { - until: now() - ONE_DAY_IN_MS, - }, - futureStatus: buildCurrentStatus(userState.IDLE, { - from: now() - ONE_DAY_IN_MS, - }), - }) - ); - - await updateAllUserStatus(); - const memberRoles = await memberRoleModel - .where("roleid", "==", "group-idle-role-id") - .where("userid", "==", userData()[0].discordId) - .get(); - - expect(memberRoles.empty).to.equal(false); - expect(fetchStub.calledOnce).to.equal(true); - expect(fetchStub.firstCall.args[1].method).to.equal("PUT"); - }); - - it("should not duplicate the group idle Firestore role when the user already has it", async function () { - fetchStub = sinon.stub(global, "fetch").resolves({ ok: true }); - const userId = await addUser(userData()[0]); - await seedGroupIdleRole(); - await memberRoleModel.doc("existing-member-idle-role").set({ - roleid: "group-idle-role-id", - userid: userData()[0].discordId, - }); - await userStatusModel.doc("ooo-to-idle-existing-member-role").set( - buildUserStatus(userId, userState.OOO, { - currentStatus: { - until: now() - ONE_DAY_IN_MS, - }, - futureStatus: buildCurrentStatus(userState.IDLE, { - from: now() - ONE_DAY_IN_MS, - }), - }) - ); - - await updateAllUserStatus(); - const memberRoles = await getGroupIdleMemberRolesForUser(userData()[0].discordId); - - expect(memberRoles.size).to.equal(1); - expect(fetchStub.calledOnce).to.equal(true); - expect(fetchStub.firstCall.args[1].method).to.equal("PUT"); - }); - - it("should remove the group idle Discord role when a processed transition moves a user out of IDLE", async function () { - fetchStub = sinon.stub(global, "fetch").resolves({ ok: true }); - const userId = await addUser(userData()[0]); - await seedGroupIdleRole(); - await memberRoleModel.doc("member-idle-role").set({ - roleid: "group-idle-role-id", - userid: userData()[0].discordId, - }); - await userStatusModel.doc("idle-to-active-status").set( - buildUserStatus(userId, userState.IDLE, { - futureStatus: buildCurrentStatus(userState.ACTIVE, { - from: now() - ONE_DAY_IN_MS, - }), - }) - ); - - await updateAllUserStatus(); - const memberRole = await memberRoleModel.doc("member-idle-role").get(); - - expect(memberRole.exists).to.equal(false); - expect(fetchStub.calledOnce).to.equal(true); - expect(fetchStub.firstCall.args[1].method).to.equal("DELETE"); - }); - - it("should remove an expired future OOO status", async function () { - await userStatusModel.doc("expired-future-ooo-status").set( - buildUserStatus("expired-future-ooo-user", userState.ACTIVE, { - futureStatus: buildCurrentStatus(userState.OOO, { - from: now() - ONE_DAY_IN_MS * 3, - until: now() - ONE_DAY_IN_MS, - }), - }) - ); - - const summary = await updateAllUserStatus(); - const updatedDoc = await userStatusModel.doc("expired-future-ooo-status").get(); - - expect(summary.nonOooUsersAltered).to.equal(1); - expect(updatedDoc.data().currentStatus.state).to.equal(userState.ACTIVE); - expect(updatedDoc.data().futureStatus).to.equal(undefined); - }); - - it("should swap current and future statuses when today lies within a future OOO range", async function () { - const until = now() + ONE_DAY_IN_MS; - await userStatusModel.doc("active-to-ooo-status").set( - buildUserStatus("active-to-ooo-user", userState.ACTIVE, { - futureStatus: buildCurrentStatus(userState.OOO, { - from: now() - ONE_DAY_IN_MS, - until, - }), - }) - ); - - const summary = await updateAllUserStatus(); - const updatedDoc = await userStatusModel.doc("active-to-ooo-status").get(); - - expect(summary.nonOooUsersAltered).to.equal(1); - expect(updatedDoc.data().currentStatus.state).to.equal(userState.OOO); - expect(updatedDoc.data().futureStatus.state).to.equal(userState.ACTIVE); - expect(updatedDoc.data().futureStatus.from).to.equal(until); - expect(updatedDoc.data().lastOooUntil).to.equal(null); - }); - - it("should swap future OOO into currentStatus and set an empty futureStatus when no current state exists", async function () { - const until = now() + ONE_DAY_IN_MS; - await userStatusModel.doc("missing-current-to-ooo-status").set({ - userId: "missing-current-to-ooo-user", - futureStatus: buildCurrentStatus(userState.OOO, { - from: now() - ONE_DAY_IN_MS, - until, - }), - }); - - const summary = await updateAllUserStatus(); - const updatedDoc = await userStatusModel.doc("missing-current-to-ooo-status").get(); - - expect(summary.nonOooUsersAltered).to.equal(1); - expect(updatedDoc.data().currentStatus.state).to.equal(userState.OOO); - expect(updatedDoc.data().futureStatus).to.deep.equal({}); - expect(updatedDoc.data().lastOooUntil).to.equal(null); - }); - - it("should return correct summary statistics for altered and unaltered statuses", async function () { - await userStatusModel.doc("ooo-altered").set( - buildUserStatus("ooo-altered-user", userState.OOO, { - futureStatus: buildCurrentStatus(userState.ACTIVE, { - from: now() - ONE_DAY_IN_MS, - }), - }) - ); - await userStatusModel.doc("ooo-unaltered").set( - buildUserStatus("ooo-unaltered-user", userState.OOO, { - futureStatus: buildCurrentStatus(userState.IDLE, { - from: now() + ONE_DAY_IN_MS, - }), - }) - ); - await userStatusModel.doc("non-ooo-altered").set( - buildUserStatus("non-ooo-altered-user", userState.ACTIVE, { - futureStatus: buildCurrentStatus(userState.OOO, { - from: now() - ONE_DAY_IN_MS, - until: now() + ONE_DAY_IN_MS, - }), - }) - ); - await userStatusModel.doc("non-ooo-unaltered").set( - buildUserStatus("non-ooo-unaltered-user", userState.ACTIVE, { - futureStatus: buildCurrentStatus(userState.OOO, { - from: now() + ONE_DAY_IN_MS, - until: now() + ONE_DAY_IN_MS * 3, - }), - }) - ); - - const summary = await updateAllUserStatus(); - - expect(summary).to.deep.equal({ - usersCount: 2, - oooUsersAltered: 1, - oooUsersUnaltered: 0, - nonOooUsersAltered: 1, - nonOooUsersUnaltered: 0, - }); - }); - - it("should log a warning when more than 100 user status documents are updated", async function () { - const loggerInfoStub = sinon.stub(logger, "info"); - const statusPromises = []; - for (let index = 0; index < 101; index++) { - statusPromises.push( - userStatusModel.doc(`status-${index}`).set( - buildUserStatus(`user-${index}`, userState.ACTIVE, { - futureStatus: buildCurrentStatus(userState.OOO, { - from: now() - ONE_DAY_IN_MS, - until: now() + ONE_DAY_IN_MS, - }), - }) - ) - ); - } - await Promise.all(statusPromises); - - await updateAllUserStatus(); - - expect(loggerInfoStub.calledOnce).to.equal(true); - expect(loggerInfoStub.firstCall.args[0]).to.include("Warning: More than 100 User Status documents to update"); - }); - - it("should return an error response when the user status query fails", async function () { - sinon.stub(admin.firestore.Query.prototype, "where").throws(new Error("Unable to query future statuses")); - - const response = await updateAllUserStatus(); - - expect(response).to.deep.equal({ - status: 500, - message: "User Status couldn't be updated Successfully.", - }); - }); - - it("should return an error response when the batch commit fails", async function () { - await userStatusModel.doc("batch-failure-status").set( - buildUserStatus("batch-failure-user", userState.ACTIVE, { - futureStatus: buildCurrentStatus(userState.OOO, { - from: now() - ONE_DAY_IN_MS, - until: now() + ONE_DAY_IN_MS, - }), - }) - ); - sinon.stub(firestore, "batch").returns({ - _ops: [], - set: sinon.stub(), - commit: sinon.stub().rejects(new Error("Batch operation failed")), - }); - - const response = await updateAllUserStatus(); - - expect(response).to.deep.equal({ - status: 500, - message: "User Status couldn't be updated Successfully.", - }); + it("Should throw an error if the status is not OOO", async function () { + const data = generateStatusDataForCancelOOO(userId, userState.ACTIVE); + await docRefUser0.set(data); + await cancelOooStatus(userId).catch((err) => { + expect(err).to.be.an.instanceOf(Error); + expect(err).to.be.an.instanceOf(Forbidden); + expect(err.message).to.be.equal("The OOO Status cannot be canceled because the current status is ACTIVE."); }); }); - describe("getGroupRole", function () { - it("should return roleExists false when role name is empty or missing", async function () { - expect(await getGroupRole()).to.deep.equal({ roleExists: false }); - expect(await getGroupRole("")).to.deep.equal({ roleExists: false }); - }); - - it("should return roleExists false when the role does not exist", async function () { - const response = await getGroupRole("group-idle"); - - expect(response).to.deep.equal({ roleExists: false }); - }); - - it("should return the role when it exists", async function () { - await seedGroupIdleRole(); - - const response = await getGroupRole("group-idle"); - - expect(response.roleExists).to.equal(true); - expect(response.role).to.deep.equal({ - id: "group-idle-doc", - rolename: "group-idle", - roleid: "group-idle-role-id", - }); - }); - - it("should throw an error when the database fetch fails", async function () { - sinon.stub(admin.firestore.Query.prototype, "where").throws(new Error("Unable to fetch role")); - - try { - await getGroupRole("group-idle"); - expect.fail("Should have thrown"); - } catch (err) { - expect(err).to.be.an.instanceOf(Error); - expect(err.message).to.equal("Unable to fetch role"); - } + it("should throw an error if unable to fetch task assigned to user.", async function () { + sinon.stub(tasksModel, "where").throws(new Error("Task not found")); + await cancelOooStatus(userId).catch((err) => { + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.be.equal("Task not found"); }); }); - describe("cancelOooStatus and addFutureStatus coverage", function () { - let userId; - let docRefUser0; - - beforeEach(async function () { - userId = await addUser(); - docRefUser0 = userStatusModel.doc(); - const data = generateStatusDataForCancelOOO(userId, userState.OOO); - await docRefUser0.set(data); - }); - - it("Should cancel the OOO Status of the User", async function () { - const response = await cancelOooStatus(userId); - expect(response.userStatusExists).to.equal(true); - expect(response.data.userId).to.equal(userId); - expect(response.data.currentStatus).to.not.equal(userState.OOO); - expect(response.data.futureStatus?.state).to.equal(undefined); - }); - - it("Should clear the future Status if the User cancels OOO", async function () { - const data = generateStatusDataForCancelOOO(userId, userState.OOO); - const from = now() + ONE_DAY_IN_MS; - data.futureStatus = generateDefaultFutureStatus(userState.IDLE, from, ""); - await docRefUser0.set(data); - const response = await cancelOooStatus(userId); - expect(response.userStatusExists).to.equal(true); - expect(response.data.userId).to.equal(userId); - expect(response.data.futureStatus.state).to.equal(undefined); - }); - - it("should transition to ACTIVE when the OOO user has active tasks", async function () { - await addLiveTaskForUser(userId); - - const response = await cancelOooStatus(userId); - - expect(response.userStatusExists).to.equal(true); - expect(response.data.currentStatus.state).to.equal(userState.ACTIVE); - }); - - it("should add the group idle Discord role and persist lastOooUntil when OOO is canceled without active tasks", async function () { - fetchStub = sinon.stub(global, "fetch").resolves({ ok: true }); - await seedGroupIdleRole(); - const oooUntil = now() + ONE_DAY_IN_MS; - const data = generateStatusDataForCancelOOO(userId, userState.OOO); - data.currentStatus.until = oooUntil; - await docRefUser0.set(data); - - const response = await cancelOooStatus(userId); - const memberRoles = await getGroupIdleMemberRolesForUser(userData()[0].discordId); - - expect(response.data.currentStatus.state).to.equal(userState.IDLE); - expect(response.data.lastOooUntil).to.equal(oooUntil); - expect(memberRoles.size).to.equal(1); - expect(fetchStub.calledOnce).to.equal(true); - expect(fetchStub.firstCall.args[1].method).to.equal("PUT"); - }); - - it("should throw an error if unable to fetch the user status document", async function () { - sinon.stub(admin.firestore.Query.prototype, "where").throws(new Error("Unable to fetch user status document")); - try { - await cancelOooStatus(userId); - expect.fail("Should have thrown"); - } catch (err) { - expect(err).to.be.an.instanceOf(Error); - expect(err.message).to.be.equal("Unable to fetch user status document"); - } - }); - - it("Should throw error when no User status document found", async function () { - await cancelOooStatus("randomUserId").catch((err) => { - expect(err).to.be.an.instanceOf(Error); - expect(err).to.be.an.instanceOf(NotFound); - expect(err.message).to.be.equal("No User status document found"); - }); - }); - - it("Should throw an error if the status is not OOO", async function () { - const data = generateStatusDataForCancelOOO(userId, userState.ACTIVE); - await docRefUser0.set(data); - await cancelOooStatus(userId).catch((err) => { - expect(err).to.be.an.instanceOf(Error); - expect(err).to.be.an.instanceOf(Forbidden); - expect(err.message).to.be.equal("The OOO Status cannot be canceled because the current status is ACTIVE."); - }); - }); - - it("should throw an error if unable to fetch task assigned to user.", async function () { - sinon.stub(tasksModel, "where").throws(new Error("Task not found")); - await cancelOooStatus(userId).catch((err) => { - expect(err).to.be.an.instanceOf(Error); - expect(err.message).to.be.equal("Task not found"); - }); - }); - - it("Should add future status to the User", async function () { - const response = await addFutureStatus({ ...userFutureStatusData }); - expect(response.userStatusExists).to.equal(true); - expect(response.data.futureStatus.state).to.equal("UPCOMING"); - }); - - it("should create a user status document when adding future status for a new user", async function () { - const futureStatusData = { - ...userFutureStatusData, - userId: "new-future-status-user", - }; - - const response = await addFutureStatus(futureStatusData); - const persistedStatus = await getUserStatus("new-future-status-user"); - - expect(response.userStatusExists).to.equal(true); - expect(response.data.userId).to.equal("new-future-status-user"); - expect(response.data.futureStatus.state).to.equal("UPCOMING"); - expect(persistedStatus.userStatusExists).to.equal(true); - expect(persistedStatus.data.futureStatus.state).to.equal("UPCOMING"); - }); + it("Should add future status to the User", async function () { + const response = await addFutureStatus(userFutureStatusData); + expect(response.userStatusExists).to.equal(true); + expect(response.data.futureStatus.state).to.equal("UPCOMING"); }); }); From 4e013fae22c1c7cafbb661fbc318da8464dd5132 Mon Sep 17 00:00:00 2001 From: Soumava Das Date: Tue, 14 Jul 2026 01:42:53 +0530 Subject: [PATCH 05/10] fixed the unnecessary test file code changes --- models/userStatus.js | 1 + test/unit/models/userStatus.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/models/userStatus.js b/models/userStatus.js index 4103c0acb..963573034 100644 --- a/models/userStatus.js +++ b/models/userStatus.js @@ -27,6 +27,7 @@ const usersCollection = firestore.collection("users"); const config = require("config"); const DISCORD_BASE_URL = config.get("services.discordBot.baseUrl"); const { generateAuthTokenForCloudflare } = require("../utils/discord-actions"); +const logger = require("../utils/logger"); // added this function here to avoid circular dependency /** diff --git a/test/unit/models/userStatus.js b/test/unit/models/userStatus.js index d537ce829..ef496e111 100644 --- a/test/unit/models/userStatus.js +++ b/test/unit/models/userStatus.js @@ -1,4 +1,4 @@ -import { userFutureStatusData } from "../../fixtures/userFutureStatus/userFutureStatusData"; +const { userFutureStatusData } = require("../../fixtures/userFutureStatus/userFutureStatusData"); const chai = require("chai"); const sinon = require("sinon"); const { NotFound, Forbidden } = require("http-errors"); From 9d0c873c6dfea32a38804d53076a6c8638f41123 Mon Sep 17 00:00:00 2001 From: Soumava Das Date: Tue, 14 Jul 2026 09:16:41 +0530 Subject: [PATCH 06/10] test(user-status): add unit and integration tests for bulk user status updates Introduce comprehensive tests to verify the automatic updating of user status from Out-of-Office (OOO) to Active based on the scheduled start date. Key Changes: - Unit Tests: Added cases for updateAllUserStatus (past, today boundary, and future scenarios). - Integration Tests: Verified PATCH /users/status/update endpoint under superuser auth and checked DB states. - Clock Mocking: Utilized Sinon fake timers to ensure deterministic date-based testing. --- test/integration/userStatus.test.js | 116 +++++++++++++++++++++++ test/unit/models/userStatus.js | 139 +++++++++++++++++++++++++++- 2 files changed, 254 insertions(+), 1 deletion(-) diff --git a/test/integration/userStatus.test.js b/test/integration/userStatus.test.js index 58b2e8cb9..4fc31d89d 100644 --- a/test/integration/userStatus.test.js +++ b/test/integration/userStatus.test.js @@ -139,6 +139,14 @@ describe("UserStatus", function () { }); describe("PATCH /users/status/update", function () { + let clock; + + afterEach(function () { + if (clock) { + clock.restore(); + } + }); + it("Should return 401 for unauthorized request", async function () { const response = await chai.request(app).patch("/users/status/update"); expect(response).to.have.status(401); @@ -148,6 +156,114 @@ describe("UserStatus", function () { const response = await chai.request(app).patch("/users/status/update").set("cookie", `${cookieName}=${jwt}`); expect(response).to.have.status(401); }); + + it("Should update user status and return 200 for a superuser request", async function () { + clock = sinon.useFakeTimers({ + now: new Date("2026-07-14T02:00:00.000Z").getTime(), + toFake: ["Date"], + }); + const today = Date.now(); + + const userToUpdateId = await addUser(userData[1]); + const userNotToUpdateId = await addUser(userData[2]); + const userBoundaryUpdateId = await addUser(userData[3]); + + const userToUpdateStatusRef = firestore.collection("usersStatus").doc(); + await userToUpdateStatusRef.set({ + userId: userToUpdateId, + currentStatus: { + state: userState.OOO, + from: today - 2 * 24 * 60 * 60 * 1000, + until: today + 2 * 24 * 60 * 60 * 1000, + message: "On leave", + updatedAt: today - 2 * 24 * 60 * 60 * 1000, + }, + futureStatus: { + state: userState.ACTIVE, + from: today - 24 * 60 * 60 * 1000, // yesterday + until: "", + message: "", + updatedAt: today - 24 * 60 * 60 * 1000, + }, + monthlyHours: { + committed: 40, + updatedAt: today - 2 * 24 * 60 * 60 * 1000, + }, + }); + + const userNotToUpdateStatusRef = firestore.collection("usersStatus").doc(); + await userNotToUpdateStatusRef.set({ + userId: userNotToUpdateId, + currentStatus: { + state: userState.OOO, + from: today - 24 * 60 * 60 * 1000, + until: today + 2 * 24 * 60 * 60 * 1000, + message: "On leave", + updatedAt: today - 24 * 60 * 60 * 1000, + }, + futureStatus: { + state: userState.ACTIVE, + from: today + 24 * 60 * 60 * 1000, // tomorrow + until: "", + message: "", + updatedAt: today - 24 * 60 * 60 * 1000, + }, + monthlyHours: { + committed: 40, + updatedAt: today - 24 * 60 * 60 * 1000, + }, + }); + + const userBoundaryUpdateStatusRef = firestore.collection("usersStatus").doc(); + await userBoundaryUpdateStatusRef.set({ + userId: userBoundaryUpdateId, + currentStatus: { + state: userState.OOO, + from: today - 24 * 60 * 60 * 1000, + until: today + 24 * 60 * 60 * 1000, + message: "On leave", + updatedAt: today - 24 * 60 * 60 * 1000, + }, + futureStatus: { + state: userState.ACTIVE, + from: today, // exactly today + until: "", + message: "", + updatedAt: today - 24 * 60 * 60 * 1000, + }, + monthlyHours: { + committed: 40, + updatedAt: today - 24 * 60 * 60 * 1000, + }, + }); + + const response = await chai + .request(app) + .patch("/users/status/update") + .set("cookie", `${cookieName}=${superUserAuthToken}`); + + expect(response).to.have.status(200); + expect(response.body.message).to.equal("All User Status updated successfully."); + + expect(response.body.data.usersCount).to.equal(2); + expect(response.body.data.oooUsersAltered).to.equal(2); + + const updatedDoc = await userToUpdateStatusRef.get(); + expect(updatedDoc.data().currentStatus.state).to.equal(userState.ACTIVE); + expect(updatedDoc.data().futureStatus).to.equal(undefined); + + const nonUpdatedDoc = await userNotToUpdateStatusRef.get(); + expect(nonUpdatedDoc.data().currentStatus.state).to.equal(userState.OOO); + expect(nonUpdatedDoc.data().futureStatus.state).to.equal(userState.ACTIVE); + + const boundaryUpdatedDoc = await userBoundaryUpdateStatusRef.get(); + expect(boundaryUpdatedDoc.data().currentStatus.state).to.equal(userState.ACTIVE); + expect(boundaryUpdatedDoc.data().futureStatus).to.equal(undefined); + + await userToUpdateStatusRef.delete(); + await userNotToUpdateStatusRef.delete(); + await userBoundaryUpdateStatusRef.delete(); + }); }); describe("PATCH /users/status/:userid", function () { diff --git a/test/unit/models/userStatus.js b/test/unit/models/userStatus.js index ef496e111..e275450a4 100644 --- a/test/unit/models/userStatus.js +++ b/test/unit/models/userStatus.js @@ -6,7 +6,7 @@ const { expect } = chai; const firestore = require("../../../utils/firestore"); const userStatusModel = firestore.collection("usersStatus"); const tasksModel = firestore.collection("tasks"); -const { cancelOooStatus, addFutureStatus } = require("../../../models/userStatus"); +const { cancelOooStatus, addFutureStatus, updateAllUserStatus } = require("../../../models/userStatus"); const cleanDb = require("../../utils/cleanDb"); const addUser = require("../../utils/addUser"); const { userState } = require("../../../constants/userStatus"); @@ -86,4 +86,141 @@ describe("tasks", function () { expect(response.userStatusExists).to.equal(true); expect(response.data.futureStatus.state).to.equal("UPCOMING"); }); + + describe("updateAllUserStatus", function () { + let clock; + + beforeEach(async function () { + clock = sinon.useFakeTimers({ + now: new Date("2026-07-14T02:00:00.000Z").getTime(), + toFake: ["Date"], + }); + }); + + afterEach(async function () { + clock.restore(); + await cleanDb(); + }); + + it("Should update user status when futureStatus.from <= today (e.g. from is in the past)", async function () { + const today = Date.now(); + const docRef = userStatusModel.doc(); + + // futureStatus.from = today - 1 day (in the past) + const userStatusData = { + userId, + currentStatus: { + state: userState.OOO, + from: today - 2 * 24 * 60 * 60 * 1000, + until: today + 2 * 24 * 60 * 60 * 1000, + message: "On leave", + updatedAt: today - 2 * 24 * 60 * 60 * 1000, + }, + futureStatus: { + state: userState.ACTIVE, + from: today - 24 * 60 * 60 * 1000, // yesterday + until: "", + message: "", + updatedAt: today - 24 * 60 * 60 * 1000, + }, + monthlyHours: { + committed: 40, + updatedAt: today - 2 * 24 * 60 * 60 * 1000, + }, + }; + await docRef.set(userStatusData); + + const summary = await updateAllUserStatus(); + expect(summary.usersCount).to.equal(1); + expect(summary.oooUsersAltered).to.equal(1); + + const doc = await docRef.get(); + const data = doc.data(); + + // Verify status has updated to ACTIVE + expect(data.currentStatus.state).to.equal(userState.ACTIVE); + expect(data.currentStatus.from).to.equal(today - 24 * 60 * 60 * 1000); + expect(data.futureStatus).to.equal(undefined); + }); + + it("Should update user status when futureStatus.from === today (boundary case)", async function () { + const today = Date.now(); + const docRef = userStatusModel.doc(); + + const userStatusData = { + userId, + currentStatus: { + state: userState.OOO, + from: today - 24 * 60 * 60 * 1000, + until: today + 24 * 60 * 60 * 1000, + message: "On leave", + updatedAt: today - 24 * 60 * 60 * 1000, + }, + futureStatus: { + state: userState.ACTIVE, + from: today, // exactly today + until: "", + message: "", + updatedAt: today - 24 * 60 * 60 * 1000, + }, + monthlyHours: { + committed: 40, + updatedAt: today - 24 * 60 * 60 * 1000, + }, + }; + await docRef.set(userStatusData); + + const summary = await updateAllUserStatus(); + expect(summary.usersCount).to.equal(1); + expect(summary.oooUsersAltered).to.equal(1); + + const doc = await docRef.get(); + const data = doc.data(); + + // Verify status has updated to ACTIVE + expect(data.currentStatus.state).to.equal(userState.ACTIVE); + expect(data.currentStatus.from).to.equal(today); + expect(data.futureStatus).to.equal(undefined); + }); + + it("Should not update user status when futureStatus.from > today (e.g. from is in the future)", async function () { + const today = Date.now(); + const docRef = userStatusModel.doc(); + + // futureStatus.from = today + 1 day (in the future) + const userStatusData = { + userId, + currentStatus: { + state: userState.OOO, + from: today - 24 * 60 * 60 * 1000, + until: today + 2 * 24 * 60 * 60 * 1000, + message: "On leave", + updatedAt: today - 24 * 60 * 60 * 1000, + }, + futureStatus: { + state: userState.ACTIVE, + from: today + 24 * 60 * 60 * 1000, // tomorrow + until: "", + message: "", + updatedAt: today - 24 * 60 * 60 * 1000, + }, + monthlyHours: { + committed: 40, + updatedAt: today - 24 * 60 * 60 * 1000, + }, + }; + await docRef.set(userStatusData); + + const summary = await updateAllUserStatus(); + expect(summary.usersCount).to.equal(0); + expect(summary.oooUsersAltered).to.equal(0); + + const doc = await docRef.get(); + const data = doc.data(); + + // Verify status remains OOO and futureStatus is not touched + expect(data.currentStatus.state).to.equal(userState.OOO); + expect(data.futureStatus.state).to.equal(userState.ACTIVE); + }); + }); }); From 961731befa107aadccc33812d8387d05bd5a2f50 Mon Sep 17 00:00:00 2001 From: Soumava Das Date: Wed, 15 Jul 2026 19:24:54 +0530 Subject: [PATCH 07/10] updated the test files --- test/integration/userStatus.test.js | 8 +-- test/unit/models/userStatus.js | 92 +++++++++++------------------ 2 files changed, 37 insertions(+), 63 deletions(-) diff --git a/test/integration/userStatus.test.js b/test/integration/userStatus.test.js index 4fc31d89d..24b0b3d35 100644 --- a/test/integration/userStatus.test.js +++ b/test/integration/userStatus.test.js @@ -8,7 +8,6 @@ const app = require("../../server"); const authService = require("../../services/authService"); const addUser = require("../utils/addUser"); const cleanDb = require("../utils/cleanDb"); -// Import fixtures const userData = require("../fixtures/user/user")(); const superUser = userData[4]; const { @@ -180,7 +179,7 @@ describe("UserStatus", function () { }, futureStatus: { state: userState.ACTIVE, - from: today - 24 * 60 * 60 * 1000, // yesterday + from: today - 24 * 60 * 60 * 1000, until: "", message: "", updatedAt: today - 24 * 60 * 60 * 1000, @@ -203,7 +202,7 @@ describe("UserStatus", function () { }, futureStatus: { state: userState.ACTIVE, - from: today + 24 * 60 * 60 * 1000, // tomorrow + from: today + 24 * 60 * 60 * 1000, until: "", message: "", updatedAt: today - 24 * 60 * 60 * 1000, @@ -226,7 +225,7 @@ describe("UserStatus", function () { }, futureStatus: { state: userState.ACTIVE, - from: today, // exactly today + from: today, until: "", message: "", updatedAt: today - 24 * 60 * 60 * 1000, @@ -337,7 +336,6 @@ describe("UserStatus", function () { }); it("Should return 401 for unauthorized request for user and superuser", function (done) { - // Using ONBOARDING state since OOO is now blocked by the validator chai .request(app) .patch(`/users/status/${testUserId}`) diff --git a/test/unit/models/userStatus.js b/test/unit/models/userStatus.js index e275450a4..5ed1d6955 100644 --- a/test/unit/models/userStatus.js +++ b/test/unit/models/userStatus.js @@ -38,7 +38,7 @@ describe("tasks", function () { it("Should clear the future Status if the User cancels OOO", async function () { const data = generateStatusDataForCancelOOO(userId, userState.OOO); - const from = new Date().getTime() + 24 * 60 * 60 * 1000; // 1 day offset from current time + const from = new Date().getTime() + 24 * 60 * 60 * 1000; data.futureStatus = generateDefaultFutureStatus(userState.IDLE, from, ""); await docRefUser0.set(data); const response = await cancelOooStatus(userId); @@ -102,32 +102,47 @@ describe("tasks", function () { await cleanDb(); }); - it("Should update user status when futureStatus.from <= today (e.g. from is in the past)", async function () { - const today = Date.now(); - const docRef = userStatusModel.doc(); - - // futureStatus.from = today - 1 day (in the past) - const userStatusData = { + function buildUserStatusMock( + userId, + today, + { + currentStatusFromOffset = -24 * 60 * 60 * 1000, + currentStatusUntilOffset = 2 * 24 * 60 * 60 * 1000, + futureStatusFromOffset, + } = {} + ) { + const currentFrom = today + currentStatusFromOffset; + return { userId, currentStatus: { state: userState.OOO, - from: today - 2 * 24 * 60 * 60 * 1000, - until: today + 2 * 24 * 60 * 60 * 1000, + from: currentFrom, + until: today + currentStatusUntilOffset, message: "On leave", - updatedAt: today - 2 * 24 * 60 * 60 * 1000, + updatedAt: currentFrom, }, futureStatus: { state: userState.ACTIVE, - from: today - 24 * 60 * 60 * 1000, // yesterday + from: today + futureStatusFromOffset, until: "", message: "", updatedAt: today - 24 * 60 * 60 * 1000, }, monthlyHours: { committed: 40, - updatedAt: today - 2 * 24 * 60 * 60 * 1000, + updatedAt: currentFrom, }, }; + } + + it("Should update user status when futureStatus.from <= today (e.g. from is in the past)", async function () { + const today = Date.now(); + const docRef = userStatusModel.doc(); + + const userStatusData = buildUserStatusMock(userId, today, { + currentStatusFromOffset: -2 * 24 * 60 * 60 * 1000, + futureStatusFromOffset: -24 * 60 * 60 * 1000, + }); await docRef.set(userStatusData); const summary = await updateAllUserStatus(); @@ -137,7 +152,6 @@ describe("tasks", function () { const doc = await docRef.get(); const data = doc.data(); - // Verify status has updated to ACTIVE expect(data.currentStatus.state).to.equal(userState.ACTIVE); expect(data.currentStatus.from).to.equal(today - 24 * 60 * 60 * 1000); expect(data.futureStatus).to.equal(undefined); @@ -147,27 +161,10 @@ describe("tasks", function () { const today = Date.now(); const docRef = userStatusModel.doc(); - const userStatusData = { - userId, - currentStatus: { - state: userState.OOO, - from: today - 24 * 60 * 60 * 1000, - until: today + 24 * 60 * 60 * 1000, - message: "On leave", - updatedAt: today - 24 * 60 * 60 * 1000, - }, - futureStatus: { - state: userState.ACTIVE, - from: today, // exactly today - until: "", - message: "", - updatedAt: today - 24 * 60 * 60 * 1000, - }, - monthlyHours: { - committed: 40, - updatedAt: today - 24 * 60 * 60 * 1000, - }, - }; + const userStatusData = buildUserStatusMock(userId, today, { + currentStatusUntilOffset: 24 * 60 * 60 * 1000, + futureStatusFromOffset: 0, + }); await docRef.set(userStatusData); const summary = await updateAllUserStatus(); @@ -177,7 +174,6 @@ describe("tasks", function () { const doc = await docRef.get(); const data = doc.data(); - // Verify status has updated to ACTIVE expect(data.currentStatus.state).to.equal(userState.ACTIVE); expect(data.currentStatus.from).to.equal(today); expect(data.futureStatus).to.equal(undefined); @@ -187,28 +183,9 @@ describe("tasks", function () { const today = Date.now(); const docRef = userStatusModel.doc(); - // futureStatus.from = today + 1 day (in the future) - const userStatusData = { - userId, - currentStatus: { - state: userState.OOO, - from: today - 24 * 60 * 60 * 1000, - until: today + 2 * 24 * 60 * 60 * 1000, - message: "On leave", - updatedAt: today - 24 * 60 * 60 * 1000, - }, - futureStatus: { - state: userState.ACTIVE, - from: today + 24 * 60 * 60 * 1000, // tomorrow - until: "", - message: "", - updatedAt: today - 24 * 60 * 60 * 1000, - }, - monthlyHours: { - committed: 40, - updatedAt: today - 24 * 60 * 60 * 1000, - }, - }; + const userStatusData = buildUserStatusMock(userId, today, { + futureStatusFromOffset: 24 * 60 * 60 * 1000, + }); await docRef.set(userStatusData); const summary = await updateAllUserStatus(); @@ -218,7 +195,6 @@ describe("tasks", function () { const doc = await docRef.get(); const data = doc.data(); - // Verify status remains OOO and futureStatus is not touched expect(data.currentStatus.state).to.equal(userState.OOO); expect(data.futureStatus.state).to.equal(userState.ACTIVE); }); From 25c35aee2f5674f132ac3aae2760796226071587 Mon Sep 17 00:00:00 2001 From: Soumava Das Date: Thu, 16 Jul 2026 19:48:46 +0530 Subject: [PATCH 08/10] test(userStatus): refactor updateAllUserStatus tests and extract shared fixture Split the single combined PATCH /users/status/update integration test into three focused, scenario-specific tests: - OOO -> ACTIVE when futureStatus.from is in the past - No update when futureStatus.from is in the future - OOO -> ACTIVE when futureStatus.from === today (boundary case) Each test now seeds only the data it needs, uses scoped assertions (usersCount: 1 or 0 instead of a combined count), and relies on the existing cleanDb() in afterEach for cleanup instead of manual ref.delete() calls. The fake clock setup is moved to beforeEach for consistency. Extracted the shared OOO status document builder into a reusable generateOooUserStatusDoc() in test/fixtures/userStatus/userStatus.js, replacing the duplicate local helpers buildUserStatusMock (unit test) and buildOooStatusDoc (integration test) that built the same document shape independently. --- test/fixtures/userStatus/userStatus.js | 34 ++++++ test/integration/userStatus.test.js | 161 +++++++++++-------------- test/unit/models/userStatus.js | 45 ++----- 3 files changed, 112 insertions(+), 128 deletions(-) diff --git a/test/fixtures/userStatus/userStatus.js b/test/fixtures/userStatus/userStatus.js index 1401f2fbc..23b5eef42 100644 --- a/test/fixtures/userStatus/userStatus.js +++ b/test/fixtures/userStatus/userStatus.js @@ -224,6 +224,39 @@ const generateDefaultFutureStatus = (state, from, until) => { return futureStatusData; }; +const generateOooUserStatusDoc = ( + userId, + today, + { + currentStatusFromOffset = -24 * 60 * 60 * 1000, + currentStatusUntilOffset = 2 * 24 * 60 * 60 * 1000, + futureStatusFromOffset, + } = {} +) => { + const currentFrom = today + currentStatusFromOffset; + return { + userId, + currentStatus: { + state: userState.OOO, + from: currentFrom, + until: today + currentStatusUntilOffset, + message: "On leave", + updatedAt: currentFrom, + }, + futureStatus: { + state: userState.ACTIVE, + from: today + futureStatusFromOffset, + until: "", + message: "", + updatedAt: today - 24 * 60 * 60 * 1000, + }, + monthlyHours: { + committed: 40, + updatedAt: currentFrom, + }, + }; +}; + module.exports = { userStatusDataForNewUser, userStatusDataAfterSignup, @@ -239,4 +272,5 @@ module.exports = { inputFixtureForFnConvertTimestampsToUTC, OutputFixtureForFnConvertTimestampsToUTC, generateDefaultFutureStatus, + generateOooUserStatusDoc, }; diff --git a/test/integration/userStatus.test.js b/test/integration/userStatus.test.js index 24b0b3d35..d567a3826 100644 --- a/test/integration/userStatus.test.js +++ b/test/integration/userStatus.test.js @@ -14,6 +14,7 @@ const { userStatusDataForNewUser, userStatusDataForOooState, generateUserStatusData, + generateOooUserStatusDoc, } = require("../fixtures/userStatus/userStatus"); const config = require("config"); @@ -140,10 +141,15 @@ describe("UserStatus", function () { describe("PATCH /users/status/update", function () { let clock; + beforeEach(function () { + clock = sinon.useFakeTimers({ + now: new Date("2026-07-14T02:00:00.000Z").getTime(), + toFake: ["Date"], + }); + }); + afterEach(function () { - if (clock) { - clock.restore(); - } + clock.restore(); }); it("Should return 401 for unauthorized request", async function () { @@ -156,85 +162,44 @@ describe("UserStatus", function () { expect(response).to.have.status(401); }); - it("Should update user status and return 200 for a superuser request", async function () { - clock = sinon.useFakeTimers({ - now: new Date("2026-07-14T02:00:00.000Z").getTime(), - toFake: ["Date"], - }); + it("Should transition OOO → ACTIVE when futureStatus.from is in the past", async function () { const today = Date.now(); - const userToUpdateId = await addUser(userData[1]); - const userNotToUpdateId = await addUser(userData[2]); - const userBoundaryUpdateId = await addUser(userData[3]); + const docRef = firestore.collection("usersStatus").doc(); + await docRef.set( + generateOooUserStatusDoc(userToUpdateId, today, { + currentStatusFromOffset: -2 * 24 * 60 * 60 * 1000, + currentStatusUntilOffset: 2 * 24 * 60 * 60 * 1000, + futureStatusFromOffset: -24 * 60 * 60 * 1000, + }) + ); - const userToUpdateStatusRef = firestore.collection("usersStatus").doc(); - await userToUpdateStatusRef.set({ - userId: userToUpdateId, - currentStatus: { - state: userState.OOO, - from: today - 2 * 24 * 60 * 60 * 1000, - until: today + 2 * 24 * 60 * 60 * 1000, - message: "On leave", - updatedAt: today - 2 * 24 * 60 * 60 * 1000, - }, - futureStatus: { - state: userState.ACTIVE, - from: today - 24 * 60 * 60 * 1000, - until: "", - message: "", - updatedAt: today - 24 * 60 * 60 * 1000, - }, - monthlyHours: { - committed: 40, - updatedAt: today - 2 * 24 * 60 * 60 * 1000, - }, - }); + const response = await chai + .request(app) + .patch("/users/status/update") + .set("cookie", `${cookieName}=${superUserAuthToken}`); - const userNotToUpdateStatusRef = firestore.collection("usersStatus").doc(); - await userNotToUpdateStatusRef.set({ - userId: userNotToUpdateId, - currentStatus: { - state: userState.OOO, - from: today - 24 * 60 * 60 * 1000, - until: today + 2 * 24 * 60 * 60 * 1000, - message: "On leave", - updatedAt: today - 24 * 60 * 60 * 1000, - }, - futureStatus: { - state: userState.ACTIVE, - from: today + 24 * 60 * 60 * 1000, - until: "", - message: "", - updatedAt: today - 24 * 60 * 60 * 1000, - }, - monthlyHours: { - committed: 40, - updatedAt: today - 24 * 60 * 60 * 1000, - }, - }); + expect(response).to.have.status(200); + expect(response.body.message).to.equal("All User Status updated successfully."); + expect(response.body.data.usersCount).to.equal(1); + expect(response.body.data.oooUsersAltered).to.equal(1); - const userBoundaryUpdateStatusRef = firestore.collection("usersStatus").doc(); - await userBoundaryUpdateStatusRef.set({ - userId: userBoundaryUpdateId, - currentStatus: { - state: userState.OOO, - from: today - 24 * 60 * 60 * 1000, - until: today + 24 * 60 * 60 * 1000, - message: "On leave", - updatedAt: today - 24 * 60 * 60 * 1000, - }, - futureStatus: { - state: userState.ACTIVE, - from: today, - until: "", - message: "", - updatedAt: today - 24 * 60 * 60 * 1000, - }, - monthlyHours: { - committed: 40, - updatedAt: today - 24 * 60 * 60 * 1000, - }, - }); + const updatedDoc = await docRef.get(); + expect(updatedDoc.data().currentStatus.state).to.equal(userState.ACTIVE); + expect(updatedDoc.data().futureStatus).to.equal(undefined); + }); + + it("Should NOT update status when futureStatus.from is in the future", async function () { + const today = Date.now(); + const userNotToUpdateId = await addUser(userData[2]); + const docRef = firestore.collection("usersStatus").doc(); + await docRef.set( + generateOooUserStatusDoc(userNotToUpdateId, today, { + currentStatusFromOffset: -24 * 60 * 60 * 1000, + currentStatusUntilOffset: 2 * 24 * 60 * 60 * 1000, + futureStatusFromOffset: 24 * 60 * 60 * 1000, + }) + ); const response = await chai .request(app) @@ -243,25 +208,39 @@ describe("UserStatus", function () { expect(response).to.have.status(200); expect(response.body.message).to.equal("All User Status updated successfully."); + expect(response.body.data.usersCount).to.equal(0); + expect(response.body.data.oooUsersAltered).to.equal(0); - expect(response.body.data.usersCount).to.equal(2); - expect(response.body.data.oooUsersAltered).to.equal(2); + const doc = await docRef.get(); + expect(doc.data().currentStatus.state).to.equal(userState.OOO); + expect(doc.data().futureStatus.state).to.equal(userState.ACTIVE); + }); - const updatedDoc = await userToUpdateStatusRef.get(); - expect(updatedDoc.data().currentStatus.state).to.equal(userState.ACTIVE); - expect(updatedDoc.data().futureStatus).to.equal(undefined); + it("Should transition OOO → ACTIVE when futureStatus.from === today (boundary)", async function () { + const today = Date.now(); + const userBoundaryUpdateId = await addUser(userData[3]); + const docRef = firestore.collection("usersStatus").doc(); + await docRef.set( + generateOooUserStatusDoc(userBoundaryUpdateId, today, { + currentStatusFromOffset: -24 * 60 * 60 * 1000, + currentStatusUntilOffset: 24 * 60 * 60 * 1000, + futureStatusFromOffset: 0, + }) + ); - const nonUpdatedDoc = await userNotToUpdateStatusRef.get(); - expect(nonUpdatedDoc.data().currentStatus.state).to.equal(userState.OOO); - expect(nonUpdatedDoc.data().futureStatus.state).to.equal(userState.ACTIVE); + const response = await chai + .request(app) + .patch("/users/status/update") + .set("cookie", `${cookieName}=${superUserAuthToken}`); - const boundaryUpdatedDoc = await userBoundaryUpdateStatusRef.get(); - expect(boundaryUpdatedDoc.data().currentStatus.state).to.equal(userState.ACTIVE); - expect(boundaryUpdatedDoc.data().futureStatus).to.equal(undefined); + expect(response).to.have.status(200); + expect(response.body.message).to.equal("All User Status updated successfully."); + expect(response.body.data.usersCount).to.equal(1); + expect(response.body.data.oooUsersAltered).to.equal(1); - await userToUpdateStatusRef.delete(); - await userNotToUpdateStatusRef.delete(); - await userBoundaryUpdateStatusRef.delete(); + const updatedDoc = await docRef.get(); + expect(updatedDoc.data().currentStatus.state).to.equal(userState.ACTIVE); + expect(updatedDoc.data().futureStatus).to.equal(undefined); }); }); diff --git a/test/unit/models/userStatus.js b/test/unit/models/userStatus.js index 5ed1d6955..5ede77f22 100644 --- a/test/unit/models/userStatus.js +++ b/test/unit/models/userStatus.js @@ -10,7 +10,11 @@ const { cancelOooStatus, addFutureStatus, updateAllUserStatus } = require("../.. const cleanDb = require("../../utils/cleanDb"); const addUser = require("../../utils/addUser"); const { userState } = require("../../../constants/userStatus"); -const { generateStatusDataForCancelOOO, generateDefaultFutureStatus } = require("../../fixtures/userStatus/userStatus"); +const { + generateStatusDataForCancelOOO, + generateDefaultFutureStatus, + generateOooUserStatusDoc, +} = require("../../fixtures/userStatus/userStatus"); describe("tasks", function () { let userId; @@ -102,44 +106,11 @@ describe("tasks", function () { await cleanDb(); }); - function buildUserStatusMock( - userId, - today, - { - currentStatusFromOffset = -24 * 60 * 60 * 1000, - currentStatusUntilOffset = 2 * 24 * 60 * 60 * 1000, - futureStatusFromOffset, - } = {} - ) { - const currentFrom = today + currentStatusFromOffset; - return { - userId, - currentStatus: { - state: userState.OOO, - from: currentFrom, - until: today + currentStatusUntilOffset, - message: "On leave", - updatedAt: currentFrom, - }, - futureStatus: { - state: userState.ACTIVE, - from: today + futureStatusFromOffset, - until: "", - message: "", - updatedAt: today - 24 * 60 * 60 * 1000, - }, - monthlyHours: { - committed: 40, - updatedAt: currentFrom, - }, - }; - } - it("Should update user status when futureStatus.from <= today (e.g. from is in the past)", async function () { const today = Date.now(); const docRef = userStatusModel.doc(); - const userStatusData = buildUserStatusMock(userId, today, { + const userStatusData = generateOooUserStatusDoc(userId, today, { currentStatusFromOffset: -2 * 24 * 60 * 60 * 1000, futureStatusFromOffset: -24 * 60 * 60 * 1000, }); @@ -161,7 +132,7 @@ describe("tasks", function () { const today = Date.now(); const docRef = userStatusModel.doc(); - const userStatusData = buildUserStatusMock(userId, today, { + const userStatusData = generateOooUserStatusDoc(userId, today, { currentStatusUntilOffset: 24 * 60 * 60 * 1000, futureStatusFromOffset: 0, }); @@ -183,7 +154,7 @@ describe("tasks", function () { const today = Date.now(); const docRef = userStatusModel.doc(); - const userStatusData = buildUserStatusMock(userId, today, { + const userStatusData = generateOooUserStatusDoc(userId, today, { futureStatusFromOffset: 24 * 60 * 60 * 1000, }); await docRef.set(userStatusData); From 6fde85f3381cbc6de619b69be95a14e24569b88a Mon Sep 17 00:00:00 2001 From: Soumava Das Date: Wed, 22 Jul 2026 02:16:02 +0530 Subject: [PATCH 09/10] fixed the error in test file --- test/unit/models/userStatus.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/unit/models/userStatus.js b/test/unit/models/userStatus.js index 32ff7f70b..aba9cc127 100644 --- a/test/unit/models/userStatus.js +++ b/test/unit/models/userStatus.js @@ -6,7 +6,12 @@ const { expect } = chai; const firestore = require("../../../utils/firestore"); const userStatusModel = firestore.collection("usersStatus"); const tasksModel = firestore.collection("tasks"); -const { cancelOooStatus, addFutureStatus, getUserStatusForUserIds } = require("../../../models/userStatus"); +const { + cancelOooStatus, + addFutureStatus, + getUserStatusForUserIds, + updateAllUserStatus, +} = require("../../../models/userStatus"); const cleanDb = require("../../utils/cleanDb"); const addUser = require("../../utils/addUser"); const { userState } = require("../../../constants/userStatus"); @@ -168,6 +173,9 @@ describe("tasks", function () { expect(data.currentStatus.state).to.equal(userState.OOO); expect(data.futureStatus.state).to.equal(userState.ACTIVE); + }); + }); + describe("getUserStatusForUserIds", function () { it("returns statuses keyed by userId for the given ids", async function () { await userStatusModel.add({ userId: "user-idle-1", currentStatus: { state: userState.IDLE } }); From ae35433624d3694213a31ca12b144d377cf2cb6f Mon Sep 17 00:00:00 2001 From: Soumava Das Date: Mon, 27 Jul 2026 11:56:47 +0530 Subject: [PATCH 10/10] refactor: clean up duplicate setup code in user status integration tests --- test/integration/userStatus.test.js | 49 +++++++++++++---------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/test/integration/userStatus.test.js b/test/integration/userStatus.test.js index 9ea51d966..77986d10f 100644 --- a/test/integration/userStatus.test.js +++ b/test/integration/userStatus.test.js @@ -196,6 +196,13 @@ describe("UserStatus", function () { clock.restore(); }); + const setupOooUserStatus = async (userFixture, today, offsets) => { + const userId = await addUser(userFixture); + const docRef = firestore.collection("usersStatus").doc(); + await docRef.set(generateOooUserStatusDoc(userId, today, offsets)); + return { userId, docRef }; + }; + it("Should return 401 for unauthorized request", async function () { const response = await chai.request(app).patch("/users/status/update"); expect(response).to.have.status(401); @@ -208,15 +215,11 @@ describe("UserStatus", function () { it("Should transition OOO → ACTIVE when futureStatus.from is in the past", async function () { const today = Date.now(); - const userToUpdateId = await addUser(userData[1]); - const docRef = firestore.collection("usersStatus").doc(); - await docRef.set( - generateOooUserStatusDoc(userToUpdateId, today, { - currentStatusFromOffset: -2 * 24 * 60 * 60 * 1000, - currentStatusUntilOffset: 2 * 24 * 60 * 60 * 1000, - futureStatusFromOffset: -24 * 60 * 60 * 1000, - }) - ); + const { docRef } = await setupOooUserStatus(userData[1], today, { + currentStatusFromOffset: -2 * 24 * 60 * 60 * 1000, + currentStatusUntilOffset: 2 * 24 * 60 * 60 * 1000, + futureStatusFromOffset: -24 * 60 * 60 * 1000, + }); const response = await chai .request(app) @@ -235,15 +238,11 @@ describe("UserStatus", function () { it("Should NOT update status when futureStatus.from is in the future", async function () { const today = Date.now(); - const userNotToUpdateId = await addUser(userData[2]); - const docRef = firestore.collection("usersStatus").doc(); - await docRef.set( - generateOooUserStatusDoc(userNotToUpdateId, today, { - currentStatusFromOffset: -24 * 60 * 60 * 1000, - currentStatusUntilOffset: 2 * 24 * 60 * 60 * 1000, - futureStatusFromOffset: 24 * 60 * 60 * 1000, - }) - ); + const { docRef } = await setupOooUserStatus(userData[2], today, { + currentStatusFromOffset: -24 * 60 * 60 * 1000, + currentStatusUntilOffset: 2 * 24 * 60 * 60 * 1000, + futureStatusFromOffset: 24 * 60 * 60 * 1000, + }); const response = await chai .request(app) @@ -262,15 +261,11 @@ describe("UserStatus", function () { it("Should transition OOO → ACTIVE when futureStatus.from === today (boundary)", async function () { const today = Date.now(); - const userBoundaryUpdateId = await addUser(userData[3]); - const docRef = firestore.collection("usersStatus").doc(); - await docRef.set( - generateOooUserStatusDoc(userBoundaryUpdateId, today, { - currentStatusFromOffset: -24 * 60 * 60 * 1000, - currentStatusUntilOffset: 24 * 60 * 60 * 1000, - futureStatusFromOffset: 0, - }) - ); + const { docRef } = await setupOooUserStatus(userData[3], today, { + currentStatusFromOffset: -24 * 60 * 60 * 1000, + currentStatusUntilOffset: 24 * 60 * 60 * 1000, + futureStatusFromOffset: 0, + }); const response = await chai .request(app)