diff --git a/test/integration/progressesTasks.test.js b/test/integration/progressesTasks.test.js index 14a00ffe6..f6ff11a90 100644 --- a/test/integration/progressesTasks.test.js +++ b/test/integration/progressesTasks.test.js @@ -5,7 +5,7 @@ const firestore = require("../../utils/firestore"); const app = require("../../server"); const authService = require("../../services/authService"); const tasks = require("../../models/tasks"); - +const progressesModel = require("../../models/progresses"); const addUser = require("../utils/addUser"); const cleanDb = require("../utils/cleanDb"); const { @@ -16,7 +16,7 @@ const { const userData = require("../fixtures/user/user")(); const taskData = require("../fixtures/tasks/tasks")(); - +const { INTERNAL_SERVER_ERROR_MESSAGE } = require("../../constants/progresses"); const cookieName = config.get("userToken.cookieName"); const { expect } = chai; @@ -229,7 +229,7 @@ describe("Test Progress Updates API for Tasks", function () { .end((err, res) => { if (err) return done(err); expect(res).to.have.status(200); - expect(res.body).to.have.keys(["message", "data", "count"]); + expect(res.body).to.have.keys(["message", "data", "count", "links"]); expect(res.body.data).to.be.an("array"); expect(res.body.message).to.be.equal("Progress document retrieved successfully."); res.body.data.forEach((progress) => { @@ -388,7 +388,7 @@ describe("Test Progress Updates API for Tasks", function () { .end((err, res) => { if (err) return done(err); expect(res).to.have.status(200); - expect(res.body).to.have.keys(["message", "data", "count"]); + expect(res.body).to.have.keys(["message", "data", "count", "links"]); expect(res.body.data).to.be.an("array"); expect(res.body.message).to.be.equal("Progress document retrieved successfully."); expect(res.body.count).to.be.equal(4); @@ -615,4 +615,155 @@ describe("Test Progress Updates API for Tasks", function () { }); }); }); + + describe("GET /progresses (getPaginatedProgressDocument)", function () { + beforeEach(async function () { + const userId = await addUser(userData[1]); + const taskObject1 = await tasks.updateTask(taskData[0]); + const taskId1 = taskObject1.taskId; + const progressData1 = stubbedModelTaskProgressData(userId, taskId1, 1683626400000, 1683590400000); // 2023-05-09 + const progressData2 = stubbedModelTaskProgressData(userId, taskId1, 1683885600000, 1683849600000); // 2023-05-12 + await firestore.collection("progresses").doc("taskProgressDocument1").set(progressData1); + await firestore.collection("progresses").doc("taskProgressDocument2").set(progressData2); + }); + + afterEach(async function () { + await cleanDb(); + }); + + it("should return paginated results when dev=true is passed", function (done) { + chai + .request(app) + .get(`/progresses?type=task&dev=true&page=0&size=1`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(200); + expect(res.body).to.have.keys(["message", "data", "count", "links"]); + expect(res.body.links).to.have.keys(["next", "prev"]); + expect(res.body.data).to.be.an("array"); + expect(res.body.message).to.be.equal("Progress document retrieved successfully."); + expect(res.body.count).to.be.equal(1); + res.body.data.forEach((progress) => { + expect(progress).to.have.keys([ + "id", + "type", + "completed", + "planned", + "blockers", + "userId", + "userData", + "taskId", + "createdAt", + "date", + ]); + }); + + return done(); + }); + }); + + it("should not return paginated results when dev=false is passed", function (done) { + chai + .request(app) + .get(`/progresses?type=task&dev=false&page=0&size=1`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(200); + expect(res.body).to.have.keys(["message", "data", "count"]); + expect(res.body.data).to.be.an("array"); + expect(res.body.count).to.not.equal(1); + expect(res.body.message).to.be.equal("Progress document retrieved successfully."); + res.body.data.forEach((progress) => { + expect(progress).to.have.keys([ + "id", + "type", + "completed", + "planned", + "blockers", + "userId", + "taskId", + "createdAt", + "date", + ]); + }); + + return done(); + }); + }); + + it("should return null for next link on the last page", function (done) { + const size = 1; + const page = 1; + + chai + .request(app) + .get(`/progresses?type=task&dev=true&page=${page}&size=${size}`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(200); + expect(res.body).to.have.keys(["message", "data", "count", "links"]); + expect(res.body.links).to.have.keys(["next", "prev"]); + expect(res.body.data).to.be.an("array"); + expect(res.body.message).to.be.equal("Progress document retrieved successfully."); + expect(res.body.links.next).to.be.equal(null); + expect(res.body.links.prev).to.equal(`/progresses?type=task&page=${page - 1}&size=${size}&dev=true`); + return done(); + }); + }); + + it("should return a bad request error for invalid size parameter", function (done) { + chai + .request(app) + .get(`/progresses?type=task&dev=true&page=0&size=104`) + .end((_err, res) => { + expect(res).to.have.status(400); + expect(res.body).to.be.an("object"); + expect(res.body.message).to.equal("size must be in the range 1-100"); + return done(); + }); + }); + + it("should return an empty array of progresses data on a page with no data", function (done) { + const size = 10; + const page = 100; + + chai + .request(app) + .get(`/progresses?type=task&dev=true&page=${page}&size=${size}`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(200); + expect(res.body).to.be.an("object"); + expect(res.body.message).to.equal("Progress document retrieved successfully."); + // eslint-disable-next-line no-unused-expressions + expect(res.body.data).to.be.an("array").that.is.empty; + expect(res.body.links).to.have.keys(["next", "prev"]); + // eslint-disable-next-line no-unused-expressions + expect(res.body.links.next).to.be.null; + expect(res.body.links.prev).to.equal(`/progresses?type=task&page=${page - 1}&size=${size}&dev=true`); + return done(); + }); + }); + + it("Should return 500 Internal Server Error if there is an exception", function (done) { + sinon.stub(progressesModel, "getPaginatedProgressDocument").throws(new Error("Database error")); + + chai + .request(app) + .get(`/progresses?type=task&dev=true&page=0&size=1`) + .end((err, res) => { + if (err) return done(err); + + if (err) { + return done(err); + } + + expect(res).to.have.status(500); + expect(res.body).to.deep.equal({ + message: INTERNAL_SERVER_ERROR_MESSAGE, + }); + return done(); + }); + }); + }); }); diff --git a/test/integration/progressesUsers.test.js b/test/integration/progressesUsers.test.js index f2458a576..9f7ee492b 100644 --- a/test/integration/progressesUsers.test.js +++ b/test/integration/progressesUsers.test.js @@ -4,7 +4,7 @@ const sinon = require("sinon"); const firestore = require("../../utils/firestore"); const app = require("../../server"); const authService = require("../../services/authService"); - +const progressesModel = require("../../models/progresses"); const addUser = require("../utils/addUser"); const cleanDb = require("../utils/cleanDb"); const { @@ -14,7 +14,7 @@ const { } = require("../fixtures/progress/progresses"); const userData = require("../fixtures/user/user")(); - +const { INTERNAL_SERVER_ERROR_MESSAGE } = require("../../constants/progresses"); const cookieName = config.get("userToken.cookieName"); const { expect } = chai; @@ -233,7 +233,7 @@ describe("Test Progress Updates API for Users", function () { .end((err, res) => { if (err) return done(err); expect(res).to.have.status(200); - expect(res.body).to.have.keys(["message", "data", "count"]); + expect(res.body).to.have.keys(["message", "data", "count", "links"]); expect(res.body.data).to.be.an("array"); expect(res.body.message).to.be.equal("Progress document retrieved successfully."); res.body.data.forEach((progress) => { @@ -260,7 +260,7 @@ describe("Test Progress Updates API for Users", function () { .end((err, res) => { if (err) return done(err); expect(res).to.have.status(200); - expect(res.body).to.have.keys(["message", "data", "count"]); + expect(res.body).to.have.keys(["message", "data", "count", "links"]); expect(res.body.data).to.be.an("array"); expect(res.body.message).to.be.equal("Progress document retrieved successfully."); res.body.data.forEach((progress) => { @@ -499,4 +499,124 @@ describe("Test Progress Updates API for Users", function () { }); }); }); + + describe("GET /progresses (getPaginatedProgressDocument)", function () { + beforeEach(async function () { + const userId1 = await addUser(userData[0]); + const userId2 = await addUser(userData[1]); + const progressData1 = stubbedModelProgressData(userId1, 1683957764140, 1683936000000); + const progressData2 = stubbedModelProgressData(userId2, 1683957764140, 1683936000000); + await firestore.collection("progresses").doc("progressDoc1").set(progressData1); + await firestore.collection("progresses").doc("progressDoc2").set(progressData2); + }); + + afterEach(async function () { + await cleanDb(); + }); + + it("should return paginated results when dev=true is passed", function (done) { + chai + .request(app) + .get(`/progresses?type=user&dev=true&page=0&size=1`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(200); + expect(res.body).to.have.keys(["message", "data", "count", "links"]); + expect(res.body.links).to.have.keys(["next", "prev"]); + expect(res.body.data).to.be.an("array"); + expect(res.body.message).to.be.equal("Progress document retrieved successfully."); + expect(res.body.count).to.be.equal(1); + res.body.data.forEach((progress) => { + expect(progress).to.have.keys([ + "id", + "type", + "completed", + "planned", + "blockers", + "userData", + "userId", + "createdAt", + "date", + ]); + }); + + return done(); + }); + }); + + it("should return null for next link on the last page", function (done) { + const size = 1; + const page = 1; + + chai + .request(app) + .get(`/progresses?type=user&dev=true&page=${page}&size=${size}`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(200); + expect(res.body).to.have.keys(["message", "data", "count", "links"]); + expect(res.body.links).to.have.keys(["next", "prev"]); + expect(res.body.data).to.be.an("array"); + expect(res.body.message).to.be.equal("Progress document retrieved successfully."); + expect(res.body.links.next).to.be.equal(null); + expect(res.body.links.prev).to.equal(`/progresses?type=user&page=${page - 1}&size=${size}&dev=true`); + return done(); + }); + }); + + it("should return a bad request error for invalid size parameter", function (done) { + chai + .request(app) + .get(`/progresses?type=user&dev=true&page=0&size=104`) + .end((_err, res) => { + expect(res).to.have.status(400); + expect(res.body).to.be.an("object"); + expect(res.body.message).to.equal("size must be in the range 1-100"); + return done(); + }); + }); + + it("should return an empty array of progresses data on a page with no data", function (done) { + const size = 10; + const page = 100; + + chai + .request(app) + .get(`/progresses?type=user&dev=true&page=${page}&size=${size}`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(200); + expect(res.body).to.be.an("object"); + expect(res.body.message).to.equal("Progress document retrieved successfully."); + // eslint-disable-next-line no-unused-expressions + expect(res.body.data).to.be.an("array").that.is.empty; + expect(res.body.links).to.have.keys(["next", "prev"]); + // eslint-disable-next-line no-unused-expressions + expect(res.body.links.next).to.be.null; + expect(res.body.links.prev).to.equal(`/progresses?type=user&page=${page - 1}&size=${size}&dev=true`); + return done(); + }); + }); + + it("Should return 500 Internal Server Error if there is an exception", function (done) { + sinon.stub(progressesModel, "getPaginatedProgressDocument").throws(new Error("Database error")); + + chai + .request(app) + .get(`/progresses?type=user&dev=true&page=0&size=1`) + .end((err, res) => { + if (err) return done(err); + + if (err) { + return done(err); + } + + expect(res).to.have.status(500); + expect(res.body).to.deep.equal({ + message: INTERNAL_SERVER_ERROR_MESSAGE, + }); + return done(); + }); + }); + }); }); diff --git a/test/unit/models/progresses.test.js b/test/unit/models/progresses.test.js index 28a57207b..2ee7f2ab8 100644 --- a/test/unit/models/progresses.test.js +++ b/test/unit/models/progresses.test.js @@ -1,47 +1,144 @@ const chai = require("chai"); const sinon = require("sinon"); const { expect } = chai; -const { addUserDetailsToProgressDocs } = require("../../../models/progresses"); const cleanDb = require("../../utils/cleanDb"); -const users = require("../../../models/users"); +const { addUserDetailsToProgressDocs, getPaginatedProgressDocument } = require("../../../models/progresses"); +const fireStore = require("../../../utils/firestore"); +const progressesCollection = fireStore.collection("progresses"); +const { stubbedModelTaskProgressData, stubbedModelProgressData } = require("../../fixtures/progress/progresses"); +const addUser = require("../../utils/addUser"); const userDataArray = require("../../fixtures/user/user")(); const { removeSensitiveInfo } = require("../../../services/dataAccessLayer"); -describe("getProgressDocument", function () { - afterEach(function () { - cleanDb(); +const { + PROGRESSES_RESPONSE_MESSAGES: { PROGRESS_DOCUMENT_NOT_FOUND }, +} = require("../../../constants/progresses"); +const users = require("../../../models/users"); +describe("progressModel", function () { + afterEach(async function () { + await cleanDb(); sinon.restore(); }); - it("should add userData to progress documents correctly", async function () { - const userData = userDataArray[0]; - const userData2 = userDataArray[1]; - const { userId } = await users.addOrUpdate(userData); - const { userId: userId2 } = await users.addOrUpdate(userData2); - const updatedUserData = { ...userData, id: userId }; - const updatedUserData2 = { ...userData2, id: userId2 }; - removeSensitiveInfo(updatedUserData); - removeSensitiveInfo(updatedUserData2); - const mockProgressDocs = [ - { userId: userId, taskId: 101 }, - { userId: userId2, taskId: 102 }, - ]; - - const result = await addUserDetailsToProgressDocs(mockProgressDocs); - - expect(result).to.deep.equal([ - { userId, taskId: 101, userData: updatedUserData }, - { userId: userId2, taskId: 102, userData: updatedUserData2 }, - ]); + describe("getPaginatedProgressDocument", function () { + let userId; + let userId2; + let userId3; + const taskId = "taskId1"; + const taskId2 = "taskId2"; + + beforeEach(async function () { + userId = await addUser(userDataArray[0]); + userId2 = await addUser(userDataArray[1]); + userId3 = await addUser(userDataArray[2]); + const progressData = stubbedModelTaskProgressData(userId, taskId, 1683072000000, 1682985600000); + const progressData2 = stubbedModelTaskProgressData(userId2, taskId2, 1683072000000, 1682985600000); + const progressData3 = stubbedModelProgressData(userId, 1683072000000, 1682985600000); + const progressData4 = stubbedModelProgressData(userId2, 1683072000000, 1682985600000); + await progressesCollection.add(progressData); + await progressesCollection.add(progressData2); + await progressesCollection.add(progressData3); + await progressesCollection.add(progressData4); + }); + + afterEach(async function () { + await cleanDb(); + }); + + it("should return progress documents for type=task", async function () { + const size = 1; + const { progressDocs, totalProgressCount } = await getPaginatedProgressDocument({ type: "task", size }); + + expect(progressDocs).to.have.lengthOf(size); + expect(totalProgressCount).to.equal(2); + expect(progressDocs[0].type).to.equal("task"); + }); + + it("should return paginated progress documents and total count", async function () { + const size = 1; + const { progressDocs, totalProgressCount } = await getPaginatedProgressDocument({ type: "user", size }); + + expect(progressDocs).to.have.lengthOf(size); + expect(totalProgressCount).to.equal(2); + }); + + it("should throw error when no progress documents match the query", async function () { + try { + await getPaginatedProgressDocument({ + size: 1, + userId: userId3, + }); + throw new Error("Test failed: expected a NotFound error to be thrown."); + } catch (err) { + expect(err.message).to.equal(PROGRESS_DOCUMENT_NOT_FOUND); + } + }); + + it("should paginate results correctly when a specific page is requested", async function () { + const page = 1; + const size = 1; + const { progressDocs, totalProgressCount } = await getPaginatedProgressDocument({ type: "user", page, size }); + + expect(progressDocs).to.have.lengthOf(size); + expect(totalProgressCount).to.equal(2); + }); + + it("should use default page value when page is not provided", async function () { + const size = 2; + const { progressDocs, totalProgressCount } = await getPaginatedProgressDocument({ type: "user", size }); + + expect(progressDocs).to.have.lengthOf(size); + expect(totalProgressCount).to.equal(2); + }); + + it("should filter progress documents by userId", async function () { + const size = 1; + const { progressDocs, totalProgressCount } = await getPaginatedProgressDocument({ + size, + userId, + }); + + expect(totalProgressCount).to.equal(1); + expect(progressDocs).to.have.lengthOf(size); + }); }); - it("should handle errors and set userData as null", async function () { - const userData = userDataArray[0]; - await users.addOrUpdate(userData); + describe("addUserDetailsToProgressDocs", function () { + afterEach(function () { + cleanDb(); + sinon.restore(); + }); + + it("should add userData to progress documents correctly", async function () { + const userData = userDataArray[0]; + const userData2 = userDataArray[1]; + const { userId } = await users.addOrUpdate(userData); + const { userId: userId2 } = await users.addOrUpdate(userData2); + const updatedUserData = { ...userData, id: userId }; + const updatedUserData2 = { ...userData2, id: userId2 }; + removeSensitiveInfo(updatedUserData); + removeSensitiveInfo(updatedUserData2); + const mockProgressDocs = [ + { userId: userId, taskId: 101 }, + { userId: userId2, taskId: 102 }, + ]; + + const result = await addUserDetailsToProgressDocs(mockProgressDocs); + + expect(result).to.deep.equal([ + { userId, taskId: 101, userData: updatedUserData }, + { userId: userId2, taskId: 102, userData: updatedUserData2 }, + ]); + }); + + it("should handle errors and set userData as null", async function () { + const userData = userDataArray[0]; + await addUser(userData); - const mockProgressDocs = [{ userId: "userIdNotExists", taskId: 101 }]; + const mockProgressDocs = [{ userId: "userIdNotExists", taskId: 101 }]; - const result = await addUserDetailsToProgressDocs(mockProgressDocs); + const result = await addUserDetailsToProgressDocs(mockProgressDocs); - expect(result).to.deep.equal([{ userId: "userIdNotExists", taskId: 101, userData: null }]); + expect(result).to.deep.equal([{ userId: "userIdNotExists", taskId: 101, userData: null }]); + }); }); }); diff --git a/test/unit/utils/progresses.test.js b/test/unit/utils/progresses.test.js new file mode 100644 index 000000000..a5c7277cc --- /dev/null +++ b/test/unit/utils/progresses.test.js @@ -0,0 +1,156 @@ +const chai = require("chai"); +const { expect } = chai; +const sinon = require("sinon"); +const cleanDb = require("../../utils/cleanDb"); +const { buildQueryToFetchPaginatedDocs, getPaginatedProgressDocs } = require("../../../utils/progresses"); +const fireStore = require("../../../utils/firestore"); +const progressesCollection = fireStore.collection("progresses"); +const { stubbedModelTaskProgressData, stubbedModelProgressData } = require("../../fixtures/progress/progresses"); +const { + PROGRESSES_RESPONSE_MESSAGES: { PROGRESS_DOCUMENT_NOT_FOUND }, +} = require("../../../constants/progresses"); + +describe("Utils | Progresses", function () { + afterEach(async function () { + await cleanDb(); + sinon.restore(); + }); + + describe("buildQueryToFetchPaginatedDocs", function () { + beforeEach(async function () { + const progressData = stubbedModelTaskProgressData("userId", "task1", 1683072000000, 1682985600000); + const progressData2 = stubbedModelTaskProgressData("userId2", "task2", 1683072000000, 1682985600000); + const progressData3 = stubbedModelProgressData("userId", 1683072000000, 1682985600000); + const progressData4 = stubbedModelProgressData("userId2", 1683072000000, 1682985600000); + await progressesCollection.add(progressData); + await progressesCollection.add(progressData2); + await progressesCollection.add(progressData3); + await progressesCollection.add(progressData4); + }); + + afterEach(async function () { + await cleanDb(); + }); + + it("should build a query with type filter", async function () { + const queryParams = { + type: "task", + size: 100, + page: 0, + }; + + const { totalProgressCount } = await buildQueryToFetchPaginatedDocs(queryParams); + expect(totalProgressCount).to.equal(2); + }); + + it("should build a query with userId filter", async function () { + const queryParams = { + userId: "userId", + size: 100, + page: 0, + }; + + const { baseQuery, totalProgressCount } = await buildQueryToFetchPaginatedDocs(queryParams); + const results = await baseQuery.get(); + const docs = results.docs.map((doc) => doc.data()); + expect(docs[0].type).to.equal("user"); + expect(totalProgressCount).to.equal(1); + }); + + it("should build a query with taskId filter", async function () { + const queryParams = { + taskId: "task1", + size: 100, + page: 0, + }; + + const { totalProgressCount } = await buildQueryToFetchPaginatedDocs(queryParams); + expect(totalProgressCount).to.equal(1); + }); + + it("should apply default sorting when orderBy is not provided", async function () { + const queryParams = { + type: "task", + size: 100, + page: 0, + }; + + const { baseQuery } = await buildQueryToFetchPaginatedDocs(queryParams); + const results = await baseQuery.get(); + const docs = results.docs.map((doc) => doc.data()); + + expect(docs[0].type).to.equal("task"); + }); + + it("should handle pagination correctly", async function () { + const queryParams = { + type: "task", + size: 1, + page: 1, + }; + + const { baseQuery } = await buildQueryToFetchPaginatedDocs(queryParams); + const results = await baseQuery.get(); + expect(results.size).to.equal(1); + }); + + it("should return empty results for a large page number", async function () { + const queryParams = { + type: "task", + size: 100, + page: 10, + }; + + const { baseQuery } = await buildQueryToFetchPaginatedDocs(queryParams); + const results = await baseQuery.get(); + expect(results.size).to.equal(0); + }); + }); + + describe("getPaginatedProgressDocs", function () { + beforeEach(async function () { + const progressData = stubbedModelTaskProgressData("userId", "task1", 1683072000000, 1682985600000); + const progressData2 = stubbedModelTaskProgressData("userId2", "task2", 1683072000000, 1682985600000); + const progressData3 = stubbedModelProgressData("userId", 1683072000000, 1682985600000); + const progressData4 = stubbedModelProgressData("userId2", 1683072000000, 1682985600000); + await progressesCollection.add(progressData); + await progressesCollection.add(progressData2); + await progressesCollection.add(progressData3); + await progressesCollection.add(progressData4); + }); + + afterEach(async function () { + await cleanDb(); + sinon.restore(); + }); + + it("should throw a NotFound error if no documents are found and no page is specified", async function () { + const query = progressesCollection.where("userId", "==", "nonExistentUser"); + + try { + await getPaginatedProgressDocs(query); + throw new Error("Test failed: expected a NotFound error to be thrown."); + } catch (err) { + expect(err.message).to.equal(PROGRESS_DOCUMENT_NOT_FOUND); + } + }); + + it("should return an empty array if no documents are found and a page is specified", async function () { + const query = progressesCollection.where("userId", "==", "nonExistentUser"); + const results = await getPaginatedProgressDocs(query, 1); + + // eslint-disable-next-line no-unused-expressions + expect(results).to.be.an("array").that.is.empty; + }); + + it("should handle queries returning multiple documents", async function () { + const query = progressesCollection.where("type", "==", "task"); + const results = await getPaginatedProgressDocs(query); + + expect(results).to.be.an("array").that.has.lengthOf(2); + results.forEach((doc) => { + expect(doc).to.have.property("id").that.is.a("string"); + }); + }); + }); +});