From f613c5f19a14fbd01c2ae2e93059e38a50496d4c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 6 Nov 2024 11:16:45 +0800 Subject: [PATCH 01/10] Add controller method for getAllMonitorsWithUptimeStats --- Server/controllers/monitorController.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 06dcb9998..0615578c4 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -45,6 +45,28 @@ const getAllMonitors = async (req, res, next) => { } }; +/** + * Returns all monitors with uptime stats for 1,7,30, and 90 days + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @param {function} next + * @returns {Promise} + * @throws {Error} + */ +const getAllMonitorsWithUptimeStats = async (req, res, next) => { + try { + const monitors = await req.db.getAllMonitorsWithUptimeStats(); + return res.status(200).json({ + success: true, + msg: successMessages.MONITOR_GET_ALL, + data: monitors, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getAllMonitorsWithUptimeStats")); + } +}; + /** * Returns monitor stats for monitor with matching ID * @async @@ -495,6 +517,7 @@ const addDemoMonitors = async (req, res, next) => { export { getAllMonitors, + getAllMonitorsWithUptimeStats, getMonitorStatsById, getMonitorCertificate, getMonitorById, From 33e88642b3fac433a5d5231f2b3c767552cfb520 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 6 Nov 2024 11:17:08 +0800 Subject: [PATCH 02/10] Add db method for getAllMonitorsWithUptimeStats --- Server/db/mongo/modules/monitorModule.js | 48 ++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 2a692596e..826f14d49 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -43,6 +43,53 @@ const getAllMonitors = async (req, res) => { } }; +/** + * Get all monitors with uptime stats for 1,7,30, and 90 days + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise>} + * @throws {Error} + */ +const getAllMonitorsWithUptimeStats = async () => { + const timeRanges = { + 1: new Date(Date.now() - 24 * 60 * 60 * 1000), + 7: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), + 30: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), + 90: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000), + }; + + try { + const monitors = await Monitor.find(); + const monitorsWithStats = await Promise.all( + monitors.map(async (monitor) => { + const model = CHECK_MODEL_LOOKUP[monitor.type]; + + const uptimeStats = await Promise.all( + Object.entries(timeRanges).map(async ([days, startDate]) => { + const checks = await model.find({ + monitorId: monitor._id, + createdAt: { $gte: startDate }, + }); + return [days, getUptimePercentage(checks)]; + }) + ); + + return { + ...monitor.toObject(), + ...Object.fromEntries(uptimeStats), + }; + }) + ); + + return monitorsWithStats; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getAllMonitorsWithUptimeStats"; + throw error; + } +}; + /** * Function to calculate uptime duration based on the most recent check. * @param {Array} checks Array of check objects. @@ -594,6 +641,7 @@ const addDemoMonitors = async (userId, teamId) => { export { getAllMonitors, + getAllMonitorsWithUptimeStats, getMonitorStatsById, getMonitorById, getMonitorsAndSummaryByTeamId, From 21de95b89a912ed15c5e3db3c43a05bad46f0045 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 6 Nov 2024 11:17:26 +0800 Subject: [PATCH 03/10] Add route for getAllMonitorsWithUptimeStats --- Server/routes/monitorRoute.js | 44 +++++++++++++++++------------------ 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index f306a5146..056d81ed6 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -1,18 +1,19 @@ import { Router } from "express"; import { - getAllMonitors, - getMonitorStatsById, - getMonitorCertificate, - getMonitorById, - getMonitorsAndSummaryByTeamId, - getMonitorsByTeamId, - createMonitor, - checkEndpointResolution, - deleteMonitor, - deleteAllMonitors, - editMonitor, - pauseMonitor, - addDemoMonitors, + getAllMonitors, + getAllMonitorsWithUptimeStats, + getMonitorStatsById, + getMonitorCertificate, + getMonitorById, + getMonitorsAndSummaryByTeamId, + getMonitorsByTeamId, + createMonitor, + checkEndpointResolution, + deleteMonitor, + deleteAllMonitors, + editMonitor, + pauseMonitor, + addDemoMonitors, } from "../controllers/monitorController.js"; import { isAllowed } from "../middleware/isAllowed.js"; import { fetchMonitorCertificate } from "../controllers/controllerUtils.js"; @@ -20,26 +21,23 @@ import { fetchMonitorCertificate } from "../controllers/controllerUtils.js"; const router = Router(); router.get("/", getAllMonitors); +router.get("/uptime", getAllMonitorsWithUptimeStats); router.get("/stats/:monitorId", getMonitorStatsById); router.get("/certificate/:monitorId", (req, res, next) => { - getMonitorCertificate(req, res, next, fetchMonitorCertificate); + getMonitorCertificate(req, res, next, fetchMonitorCertificate); }); router.get("/:monitorId", getMonitorById); router.get("/team/summary/:teamId", getMonitorsAndSummaryByTeamId); router.get("/team/:teamId", getMonitorsByTeamId); router.get( - "/resolution/url", - isAllowed(["admin", "superadmin"]), - checkEndpointResolution -) - -router.delete( - "/:monitorId", - isAllowed(["admin", "superadmin"]), - deleteMonitor + "/resolution/url", + isAllowed(["admin", "superadmin"]), + checkEndpointResolution ); +router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), deleteMonitor); + router.post("/", isAllowed(["admin", "superadmin"]), createMonitor); router.put("/:monitorId", isAllowed(["admin", "superadmin"]), editMonitor); From 22506efaefbfc9db019dd9864cad3846ff3b2e3f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 6 Nov 2024 11:17:42 +0800 Subject: [PATCH 04/10] Update openapi docs for new route --- Server/openapi.json | 337 +++++++++++++------------------------------- 1 file changed, 96 insertions(+), 241 deletions(-) diff --git a/Server/openapi.json b/Server/openapi.json index 9c3e6d062..5d2d512e6 100644 --- a/Server/openapi.json +++ b/Server/openapi.json @@ -22,16 +22,12 @@ "variables": { "PORT": { "description": "API Port", - "enum": [ - "5000" - ], + "enum": ["5000"], "default": "5000" }, "API_PATH": { "description": "API Base Path", - "enum": [ - "api/v1" - ], + "enum": ["api/v1"], "default": "api/v1" } } @@ -42,9 +38,7 @@ "variables": { "API_PATH": { "description": "API Base Path", - "enum": [ - "api/v1" - ], + "enum": ["api/v1"], "default": "api/v1" } } @@ -55,16 +49,12 @@ "variables": { "PORT": { "description": "API Port", - "enum": [ - "5000" - ], + "enum": ["5000"], "default": "5000" }, "API_PATH": { "description": "API Base Path", - "enum": [ - "api/v1" - ], + "enum": ["api/v1"], "default": "api/v1" } } @@ -99,9 +89,7 @@ "paths": { "/auth/register": { "post": { - "tags": [ - "auth" - ], + "tags": ["auth"], "description": "Register a new user", "requestBody": { "content": { @@ -137,23 +125,8 @@ }, "role": { "type": "array", - "enum": [ - [ - "user" - ], - [ - "admin" - ], - [ - "superadmin" - ], - [ - "Demo" - ] - ], - "default": [ - "superadmin" - ] + "enum": [["user"], ["admin"], ["superadmin"], ["Demo"]], + "default": ["superadmin"] }, "teamId": { "type": "string", @@ -200,19 +173,14 @@ }, "/auth/login": { "post": { - "tags": [ - "auth" - ], + "tags": ["auth"], "description": "Login with credentials", "requestBody": { "content": { "application/json": { "schema": { "type": "object", - "required": [ - "email", - "password" - ], + "required": ["email", "password"], "properties": { "email": { "type": "string", @@ -263,9 +231,7 @@ }, "/auth/refresh": { "post": { - "tags": [ - "auth" - ], + "tags": ["auth"], "description": "Generates a new auth token if the refresh token is valid.", "requestBody": { "content": { @@ -334,9 +300,7 @@ }, "/auth/user/{userId}": { "put": { - "tags": [ - "auth" - ], + "tags": ["auth"], "description": "Change user information", "parameters": [ { @@ -397,9 +361,7 @@ ] }, "delete": { - "tags": [ - "auth" - ], + "tags": ["auth"], "description": "Delete user", "parameters": [ { @@ -452,9 +414,7 @@ }, "/auth/users/superadmin": { "get": { - "tags": [ - "auth" - ], + "tags": ["auth"], "description": "Checks to see if an admin account exists", "responses": { "200": { @@ -497,9 +457,7 @@ }, "/auth/users": { "get": { - "tags": [ - "auth" - ], + "tags": ["auth"], "description": "Get all users", "responses": { "200": { @@ -542,18 +500,14 @@ }, "/auth/recovery/request": { "post": { - "tags": [ - "auth" - ], + "tags": ["auth"], "description": "Request a recovery token", "requestBody": { "content": { "application/json": { "schema": { "type": "object", - "required": [ - "email" - ], + "required": ["email"], "properties": { "email": { "type": "string", @@ -600,18 +554,14 @@ }, "/auth/recovery/validate": { "post": { - "tags": [ - "auth" - ], + "tags": ["auth"], "description": "Validate recovery token", "requestBody": { "content": { "application/json": { "schema": { "type": "object", - "required": [ - "recoveryToken" - ], + "required": ["recoveryToken"], "properties": { "recoveryToken": { "type": "string" @@ -657,19 +607,14 @@ }, "/auth/recovery/reset": { "post": { - "tags": [ - "auth" - ], + "tags": ["auth"], "description": "Password reset", "requestBody": { "content": { "application/json": { "schema": { "type": "object", - "required": [ - "recoveryToken", - "password" - ], + "required": ["recoveryToken", "password"], "properties": { "recoveryToken": { "type": "string" @@ -718,19 +663,14 @@ }, "/invite": { "post": { - "tags": [ - "invite" - ], + "tags": ["invite"], "description": "Request an invitation", "requestBody": { "content": { "application/json": { "schema": { "type": "object", - "required": [ - "email", - "role" - ], + "required": ["email", "role"], "properties": { "email": { "type": "string" @@ -784,18 +724,14 @@ }, "/invite/verify": { "post": { - "tags": [ - "invite" - ], + "tags": ["invite"], "description": "Request an invitation", "requestBody": { "content": { "application/json": { "schema": { "type": "object", - "required": [ - "token" - ], + "required": ["token"], "properties": { "token": { "type": "string" @@ -846,9 +782,7 @@ }, "/monitors": { "get": { - "tags": [ - "monitors" - ], + "tags": ["monitors"], "description": "Get all monitors", "responses": { "200": { @@ -889,9 +823,7 @@ ] }, "post": { - "tags": [ - "monitors" - ], + "tags": ["monitors"], "description": "Create a new monitor", "requestBody": { "content": { @@ -941,9 +873,7 @@ ] }, "delete": { - "tags": [ - "monitors" - ], + "tags": ["monitors"], "description": "Delete all monitors", "responses": { "200": { @@ -984,11 +914,42 @@ ] } }, + "/monitors/uptime": { + "get": { + "tags": ["monitors"], + "description": "Get all monitors with uptime stats for 1, 7, 30, and 90 days", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "/monitors/resolution/url": { "get": { - "tags": [ - "monitors" - ], + "tags": ["monitors"], "description": "Check DNS resolution for a given URL", "parameters": [ { @@ -1053,9 +1014,7 @@ }, "/monitors/{monitorId}": { "get": { - "tags": [ - "monitors" - ], + "tags": ["monitors"], "description": "Get monitor by id", "parameters": [ { @@ -1106,9 +1065,7 @@ ] }, "put": { - "tags": [ - "monitors" - ], + "tags": ["monitors"], "description": "Update monitor by id", "parameters": [ { @@ -1168,9 +1125,7 @@ ] }, "delete": { - "tags": [ - "monitors" - ], + "tags": ["monitors"], "description": "Delete monitor by id", "parameters": [ { @@ -1223,9 +1178,7 @@ }, "/monitors/stats/{monitorId}": { "get": { - "tags": [ - "monitors" - ], + "tags": ["monitors"], "description": "Get monitor stats", "parameters": [ { @@ -1278,9 +1231,7 @@ }, "/monitors/certificate/{monitorId}": { "get": { - "tags": [ - "monitors" - ], + "tags": ["monitors"], "description": "Get monitor certificate", "parameters": [ { @@ -1333,9 +1284,7 @@ }, "/monitors/team/summary/{teamId}": { "get": { - "tags": [ - "monitors" - ], + "tags": ["monitors"], "description": "Get monitors and summary by teamId", "parameters": [ { @@ -1352,11 +1301,7 @@ "required": false, "schema": { "type": "array", - "enum": [ - "http", - "ping", - "pagespeed" - ] + "enum": ["http", "ping", "pagespeed"] } } ], @@ -1401,9 +1346,7 @@ }, "/monitors/team/{teamId}": { "get": { - "tags": [ - "monitors" - ], + "tags": ["monitors"], "description": "Get monitors by teamId", "parameters": [ { @@ -1430,10 +1373,7 @@ "required": false, "schema": { "type": "string", - "enum": [ - "asc", - "desc" - ] + "enum": ["asc", "desc"] } }, { @@ -1452,11 +1392,7 @@ "required": false, "schema": { "type": "string", - "enum": [ - "http", - "ping", - "pagespeed" - ] + "enum": ["http", "ping", "pagespeed"] } }, { @@ -1500,11 +1436,7 @@ "required": false, "schema": { "type": "string", - "enum": [ - "http", - "ping", - "pagespeed" - ] + "enum": ["http", "ping", "pagespeed"] } } ], @@ -1549,9 +1481,7 @@ }, "/monitors/pause/{monitorId}": { "post": { - "tags": [ - "monitors" - ], + "tags": ["monitors"], "description": "Pause monitor", "parameters": [ { @@ -1604,9 +1534,7 @@ }, "/monitors/demo": { "post": { - "tags": [ - "monitors" - ], + "tags": ["monitors"], "description": "Create a demo monitor", "requestBody": { "content": { @@ -1658,9 +1586,7 @@ }, "/checks/{monitorId}": { "get": { - "tags": [ - "checks" - ], + "tags": ["checks"], "description": "Get all checks for a monitor", "parameters": [ { @@ -1711,9 +1637,7 @@ ] }, "post": { - "tags": [ - "checks" - ], + "tags": ["checks"], "description": "Create a new check", "parameters": [ { @@ -1773,9 +1697,7 @@ ] }, "delete": { - "tags": [ - "checks" - ], + "tags": ["checks"], "description": "Delete all checks for a monitor", "parameters": [ { @@ -1828,9 +1750,7 @@ }, "/checks/team/{teamId}": { "get": { - "tags": [ - "checks" - ], + "tags": ["checks"], "description": "Get all checks for a team", "parameters": [ { @@ -1881,9 +1801,7 @@ ] }, "delete": { - "tags": [ - "checks" - ], + "tags": ["checks"], "description": "Delete all checks for a team", "parameters": [ { @@ -1936,9 +1854,7 @@ }, "/checks/team/ttl": { "put": { - "tags": [ - "checks" - ], + "tags": ["checks"], "description": "Update check TTL", "requestBody": { "content": { @@ -1990,9 +1906,7 @@ }, "/maintenance-window/monitor/{monitorId}": { "get": { - "tags": [ - "maintenance-window" - ], + "tags": ["maintenance-window"], "description": "Get maintenance window for monitor", "parameters": [ { @@ -2043,9 +1957,7 @@ ] }, "post": { - "tags": [ - "maintenance-window" - ], + "tags": ["maintenance-window"], "description": "Create maintenance window for monitor", "parameters": [ { @@ -2107,9 +2019,7 @@ }, "/maintenance-window/user/{userId}": { "get": { - "tags": [ - "maintenance-window" - ], + "tags": ["maintenance-window"], "description": "Get maintenance window for user", "parameters": [ { @@ -2162,9 +2072,7 @@ }, "/queue/jobs": { "get": { - "tags": [ - "queue" - ], + "tags": ["queue"], "description": "Get all jobs in queue", "responses": { "200": { @@ -2205,9 +2113,7 @@ ] }, "post": { - "tags": [ - "queue" - ], + "tags": ["queue"], "description": "Create a new job. Useful for testing scaling workers", "responses": { "200": { @@ -2250,9 +2156,7 @@ }, "/queue/metrics": { "get": { - "tags": [ - "queue" - ], + "tags": ["queue"], "description": "Get queue metrics", "responses": { "200": { @@ -2295,9 +2199,7 @@ }, "/queue/obliterate": { "post": { - "tags": [ - "queue" - ], + "tags": ["queue"], "description": "Obliterate job queue", "responses": { "200": { @@ -2377,14 +2279,7 @@ }, "UserUpdateRequest": { "type": "object", - "required": [ - "firstName", - "lastName", - "email", - "password", - "role", - "teamId" - ], + "required": ["firstName", "lastName", "email", "password", "role", "teamId"], "properties": { "firstName": { "type": "string" @@ -2406,23 +2301,8 @@ }, "role": { "type": "array", - "enum": [ - [ - "user" - ], - [ - "admin" - ], - [ - "superadmin" - ], - [ - "Demo" - ] - ], - "default": [ - "superadmin" - ] + "enum": [["user"], ["admin"], ["superadmin"], ["Demo"]], + "default": ["superadmin"] }, "deleteProfileImage": { "type": "boolean" @@ -2431,14 +2311,7 @@ }, "CreateMonitorBody": { "type": "object", - "required": [ - "userId", - "teamId", - "name", - "description", - "type", - "url" - ], + "required": ["userId", "teamId", "name", "description", "type", "url"], "properties": { "_id": { "type": "string" @@ -2457,11 +2330,7 @@ }, "type": { "type": "string", - "enum": [ - "http", - "ping", - "pagespeed" - ] + "enum": ["http", "ping", "pagespeed"] }, "url": { "type": "string" @@ -2502,13 +2371,7 @@ }, "CreateCheckBody": { "type": "object", - "required": [ - "monitorId", - "status", - "responseTime", - "statusCode", - "message" - ], + "required": ["monitorId", "status", "responseTime", "statusCode", "message"], "properties": { "monitorId": { "type": "string" @@ -2529,9 +2392,7 @@ }, "UpdateCheckTTLBody": { "type": "object", - "required": [ - "ttl" - ], + "required": ["ttl"], "properties": { "ttl": { "type": "integer" @@ -2540,13 +2401,7 @@ }, "CreateMaintenanceWindowBody": { "type": "object", - "required": [ - "userId", - "active", - "oneTime", - "start", - "end" - ], + "required": ["userId", "active", "oneTime", "start", "end"], "properties": { "userId": { "type": "string" From 792de1105f74ece9f4123cd9d504d58b4693ffaa Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 6 Nov 2024 11:18:03 +0800 Subject: [PATCH 05/10] Add tests for new controller and DB methods, add missing statusService test --- .../controllers/monitorController.test.js | 42 ++++++++ Server/tests/db/monitorModule.test.js | 96 ++++++++++++++++++- Server/tests/services/statusService.test.js | 21 ++++ 3 files changed, 158 insertions(+), 1 deletion(-) diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index 0d4deb742..1074c35b1 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -1,5 +1,6 @@ import { getAllMonitors, + getAllMonitorsWithUptimeStats, getMonitorStatsById, getMonitorCertificate, getMonitorById, @@ -61,6 +62,47 @@ describe("Monitor Controller - getAllMonitors", () => { ).to.be.true; }); }); +describe("Monitor Controller - getAllMonitorsWithUptimeStats", () => { + let req, res, next; + beforeEach(() => { + req = { + params: {}, + query: {}, + body: {}, + db: { + getAllMonitorsWithUptimeStats: sinon.stub(), + }, + }; + res = { + status: sinon.stub().returnsThis(), + json: sinon.stub(), + }; + next = sinon.stub(); + }); + afterEach(() => { + sinon.restore(); + }); + it("should reject with an error if DB operations fail", async () => { + req.db.getAllMonitorsWithUptimeStats.throws(new Error("DB error")); + await getAllMonitorsWithUptimeStats(req, res, next); + expect(next.firstCall.args[0]).to.be.an("error"); + expect(next.firstCall.args[0].message).to.equal("DB error"); + }); + + it("should return success message and data if all operations succeed", async () => { + const data = [{ monitor: "data" }]; + req.db.getAllMonitorsWithUptimeStats.returns(data); + await getAllMonitorsWithUptimeStats(req, res, next); + expect(res.status.firstCall.args[0]).to.equal(200); + expect( + res.json.calledOnceWith({ + success: true, + msg: successMessages.MONITOR_GET_ALL, + data: data, + }) + ).to.be.true; + }); +}); describe("Monitor Controller - getMonitorStatsById", () => { let req, res, next; diff --git a/Server/tests/db/monitorModule.test.js b/Server/tests/db/monitorModule.test.js index 854c37132..558dcb4d2 100644 --- a/Server/tests/db/monitorModule.test.js +++ b/Server/tests/db/monitorModule.test.js @@ -6,6 +6,7 @@ import Notification from "../../db/models/Notification.js"; import { errorMessages } from "../../utils/messages.js"; import { getAllMonitors, + getAllMonitorsWithUptimeStats, getMonitorStatsById, getMonitorById, getMonitorsAndSummaryByTeamId, @@ -22,7 +23,6 @@ import { getAverageResponseTime, getUptimePercentage, getIncidents, - getDateRange, getMonitorChecks, processChecksForDisplay, groupChecksByTime, @@ -91,6 +91,100 @@ describe("monitorModule", () => { }); }); + describe("getAllMonitorsWithUptimeStats", () => { + it("should return monitors with uptime stats for different time periods", async () => { + // Mock data + const mockMonitors = [ + { + _id: "monitor1", + type: "http", + toObject: () => ({ + _id: "monitor1", + type: "http", + name: "Test Monitor", + }), + }, + ]; + + const mockChecks = [ + { status: true }, + { status: true }, + { status: false }, + { status: true }, + ]; + + monitorFindStub.resolves(mockMonitors); + checkFindStub.resolves(mockChecks); + + const result = await getAllMonitorsWithUptimeStats(); + + expect(result).to.be.an("array"); + expect(result).to.have.lengthOf(1); + + const monitor = result[0]; + expect(monitor).to.have.property("_id", "monitor1"); + expect(monitor).to.have.property("name", "Test Monitor"); + + // Check uptime percentages exist for all time periods + expect(monitor).to.have.property("1"); + expect(monitor).to.have.property("7"); + expect(monitor).to.have.property("30"); + expect(monitor).to.have.property("90"); + + // Verify uptime percentage calculation (3 successful out of 4 = 75%) + expect(monitor["1"]).to.equal(75); + expect(monitor["7"]).to.equal(75); + expect(monitor["30"]).to.equal(75); + expect(monitor["90"]).to.equal(75); + }); + + it("should handle errors appropriately", async () => { + // Setup stub to throw error + monitorFindStub.rejects(new Error("Database error")); + + try { + await getAllMonitorsWithUptimeStats(); + } catch (error) { + expect(error).to.be.an("error"); + expect(error.message).to.equal("Database error"); + expect(error.service).to.equal("monitorModule"); + expect(error.method).to.equal("getAllMonitorsWithUptimeStats"); + } + }); + it("should handle empty monitor list", async () => { + monitorFindStub.resolves([]); + + const result = await getAllMonitorsWithUptimeStats(); + + expect(result).to.be.an("array"); + expect(result).to.have.lengthOf(0); + }); + + it("should handle monitor with no checks", async () => { + const mockMonitors = [ + { + _id: "monitor1", + type: "http", + toObject: () => ({ + _id: "monitor1", + type: "http", + name: "Test Monitor", + }), + }, + ]; + + monitorFindStub.resolves(mockMonitors); + checkFindStub.resolves([]); + + const result = await getAllMonitorsWithUptimeStats(); + + expect(result[0]).to.have.property("1", 0); + expect(result[0]).to.have.property("7", 0); + expect(result[0]).to.have.property("30", 0); + expect(result[0]).to.have.property("90", 0); + }); + }); + describe("calculateUptimeDuration", () => { let clock; const NOW = new Date("2024-01-01T12:00:00Z").getTime(); diff --git a/Server/tests/services/statusService.test.js b/Server/tests/services/statusService.test.js index 19b0edf19..432f8c20d 100644 --- a/Server/tests/services/statusService.test.js +++ b/Server/tests/services/statusService.test.js @@ -98,6 +98,7 @@ describe("StatusService", () => { expect(check.responseTime).to.equal(100); expect(check.message).to.equal("Test message"); }); + it("should build a check object for pagespeed type", () => { const check = statusService.buildCheck({ monitorId: "test", @@ -193,6 +194,26 @@ describe("StatusService", () => { expect(check.disk).to.equal("disk"); expect(check.host).to.equal("host"); }); + it("should build a check for hardware type with missing data", () => { + const check = statusService.buildCheck({ + monitorId: "test", + type: "hardware", + status: true, + responseTime: 100, + code: 200, + message: "Test message", + payload: {}, + }); + expect(check.monitorId).to.equal("test"); + expect(check.status).to.be.true; + expect(check.statusCode).to.equal(200); + expect(check.responseTime).to.equal(100); + expect(check.message).to.equal("Test message"); + expect(check.cpu).to.deep.equal({}); + expect(check.memory).to.deep.equal({}); + expect(check.disk).to.deep.equal({}); + expect(check.host).to.deep.equal({}); + }); }); describe("insertCheck", () => { it("should log an error if one is thrown", async () => { From b7923b882ced80b85c5e652c4537905836f4b011 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 6 Nov 2024 11:18:34 +0800 Subject: [PATCH 06/10] Add new DB method to MongoDB --- Server/db/mongo/MongoDB.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js index 22460eff4..581a9e588 100644 --- a/Server/db/mongo/MongoDB.js +++ b/Server/db/mongo/MongoDB.js @@ -99,6 +99,7 @@ import { import { getAllMonitors, + getAllMonitorsWithUptimeStats, getMonitorStatsById, getMonitorById, getMonitorsAndSummaryByTeamId, @@ -187,6 +188,7 @@ export default { resetPassword, checkSuperadmin, getAllMonitors, + getAllMonitorsWithUptimeStats, getMonitorStatsById, getMonitorById, getMonitorsAndSummaryByTeamId, From 42d83a68b55503020c0fa463ee605e8f2265b002 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 6 Nov 2024 11:18:43 +0800 Subject: [PATCH 07/10] update nyc configuration for DB tests --- Server/.nycrc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Server/.nycrc b/Server/.nycrc index 83b717bb5..159e02a0d 100644 --- a/Server/.nycrc +++ b/Server/.nycrc @@ -1,6 +1,11 @@ { "all": true, - "include": ["controllers/*.js", "utils/*.js", "service/*.js"], + "include": [ + "controllers/*.js", + "utils/*.js", + "service/*.js", + "db/mongo/modules/monitorModule.js" + ], "exclude": ["**/*.test.js"], "reporter": ["html", "text", "lcov"], "sourceMap": false, From 8e7619dd7832b7a88faa4c2281764e5ef7925f81 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 7 Nov 2024 12:04:25 +0800 Subject: [PATCH 08/10] Add missing try/catch block to --- Server/db/mongo/modules/checkModule.js | 100 +++++++++++++------------ 1 file changed, 53 insertions(+), 47 deletions(-) diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js index 2457d7131..319cef100 100644 --- a/Server/db/mongo/modules/checkModule.js +++ b/Server/db/mongo/modules/checkModule.js @@ -48,7 +48,7 @@ const createCheck = async (checkData) => { } await monitor.save(); - + console.log("check", check); return check; } catch (error) { error.service = SERVICE_NAME; @@ -155,58 +155,64 @@ const getChecks = async (req) => { }; const getTeamChecks = async (req) => { - const { teamId } = req.params; - let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query; + try { + const { teamId } = req.params; + let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query; - // Get monitorIDs - const userMonitors = await Monitor.find({ teamId: teamId }).select("_id"); + // Get monitorIDs + const userMonitors = await Monitor.find({ teamId: teamId }).select("_id"); - //Build check query - // Default limit to 0 if not provided - limit = limit === undefined ? 0 : limit; - // Default sort order is newest -> oldest - sortOrder = sortOrder === "asc" ? 1 : -1; + //Build check query + // Default limit to 0 if not provided + limit = limit === undefined ? 0 : limit; + // Default sort order is newest -> oldest + sortOrder = sortOrder === "asc" ? 1 : -1; - const checksQuery = { monitorId: { $in: userMonitors } }; + const checksQuery = { monitorId: { $in: userMonitors } }; - if (filter !== undefined) { - checksQuery.status = false; - switch (filter) { - case "all": - break; - case "down": - break; - case "resolve": - checksQuery.statusCode = 5000; - break; - default: - logger.warn({ - message: "invalid filter", - service: SERVICE_NAME, - method: "getTeamChecks", - }); - break; + if (filter !== undefined) { + checksQuery.status = false; + switch (filter) { + case "all": + break; + case "down": + break; + case "resolve": + checksQuery.statusCode = 5000; + break; + default: + logger.warn({ + message: "invalid filter", + service: SERVICE_NAME, + method: "getTeamChecks", + }); + break; + } } + + if (dateRange !== undefined) { + checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] }; + } + + // Skip and limit for pagination + let skip = 0; + if (page && rowsPerPage) { + skip = page * rowsPerPage; + } + + const checksCount = await Check.countDocuments(checksQuery); + + const checks = await Check.find(checksQuery) + .skip(skip) + .limit(rowsPerPage) + .sort({ createdAt: sortOrder }) + .select(["monitorId", "status", "responseTime", "statusCode", "message"]); + return { checksCount, checks }; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getTeamChecks"; + throw error; } - - if (dateRange !== undefined) { - checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] }; - } - - // Skip and limit for pagination - let skip = 0; - if (page && rowsPerPage) { - skip = page * rowsPerPage; - } - - const checksCount = await Check.countDocuments(checksQuery); - - const checks = await Check.find(checksQuery) - .skip(skip) - .limit(rowsPerPage) - .sort({ createdAt: sortOrder }) - .select(["monitorId", "status", "responseTime", "statusCode", "message"]); - return { checksCount, checks }; }; /** From f9036f36140885bf19b8d11616b37b5499f52156 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 7 Nov 2024 12:36:56 +0800 Subject: [PATCH 09/10] fix undefined check, remove console.log --- Server/db/mongo/modules/checkModule.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js index 319cef100..773d6ef1e 100644 --- a/Server/db/mongo/modules/checkModule.js +++ b/Server/db/mongo/modules/checkModule.js @@ -48,7 +48,6 @@ const createCheck = async (checkData) => { } await monitor.save(); - console.log("check", check); return check; } catch (error) { error.service = SERVICE_NAME; @@ -105,7 +104,7 @@ const getChecks = async (req) => { const { monitorId } = req.params; let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query; // Default limit to 0 if not provided - limit = limit === "undefined" ? 0 : limit; + limit = limit === undefined ? 0 : limit; // Default sort order is newest -> oldest sortOrder = sortOrder === "asc" ? 1 : -1; From 6a5ffdeb795c084845ba445e756369af20c1cd1b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 7 Nov 2024 12:37:16 +0800 Subject: [PATCH 10/10] add tests for checkModule --- Server/tests/db/checkModule.test.js | 565 ++++++++++++++++++++++++++++ 1 file changed, 565 insertions(+) create mode 100644 Server/tests/db/checkModule.test.js diff --git a/Server/tests/db/checkModule.test.js b/Server/tests/db/checkModule.test.js new file mode 100644 index 000000000..96854b3f3 --- /dev/null +++ b/Server/tests/db/checkModule.test.js @@ -0,0 +1,565 @@ +import sinon from "sinon"; +import { + createCheck, + getChecksCount, + getChecks, + getTeamChecks, + deleteChecks, + deleteChecksByTeamId, + updateChecksTTL, +} from "../../db/mongo/modules/checkModule.js"; +import Check from "../../db/models/Check.js"; +import Monitor from "../../db/models/Monitor.js"; +import User from "../../db/models/User.js"; +import logger from "../../utils/logger.js"; + +describe("checkModule", () => { + describe("createCheck", () => { + let checkCountDocumentsStub, checkSaveStub, monitorFindByIdStub, monitorSaveStub; + const mockMonitor = { + _id: "123", + uptimePercentage: 0.5, + status: true, + save: () => this, + }; + const mockCheck = { active: true }; + beforeEach(() => { + checkSaveStub = sinon.stub(Check.prototype, "save"); + checkCountDocumentsStub = sinon.stub(Check, "countDocuments"); + monitorFindByIdStub = sinon.stub(Monitor, "findById"); + monitorSaveStub = sinon.stub(Monitor.prototype, "save"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return undefined early if no monitor is found", async () => { + monitorFindByIdStub.returns(null); + const check = await createCheck({ monitorId: "123" }); + expect(check).to.be.undefined; + }); + + it("should return a check", async () => { + monitorFindByIdStub.returns(mockMonitor); + checkSaveStub.returns(mockCheck); + monitorSaveStub.returns(mockMonitor); + const check = await createCheck({ monitorId: "123", status: true }); + expect(check).to.deep.equal(mockCheck); + }); + it("should return a check if status is down", async () => { + mockMonitor.status = false; + monitorFindByIdStub.returns(mockMonitor); + checkSaveStub.returns(mockCheck); + monitorSaveStub.returns(mockMonitor); + const check = await createCheck({ monitorId: "123", status: false }); + expect(check).to.deep.equal(mockCheck); + }); + it("should return a check if uptimePercentage is undefined", async () => { + mockMonitor.uptimePercentage = undefined; + monitorFindByIdStub.returns(mockMonitor); + checkSaveStub.returns(mockCheck); + monitorSaveStub.returns(mockMonitor); + const check = await createCheck({ monitorId: "123", status: true }); + expect(check).to.deep.equal(mockCheck); + }); + it("should return a check if uptimePercentage is undefined and status is down", async () => { + mockMonitor.uptimePercentage = undefined; + monitorFindByIdStub.returns(mockMonitor); + checkSaveStub.returns(mockCheck); + monitorSaveStub.returns(mockMonitor); + const check = await createCheck({ monitorId: "123", status: false }); + expect(check).to.deep.equal(mockCheck); + }); + it("should monitor save error", async () => { + const err = new Error("Save Error"); + monitorSaveStub.throws(err); + try { + await createCheck({ monitorId: "123" }); + } catch (error) { + expect(error).to.deep.equal(err); + } + }); + it("should handle errors", async () => { + const err = new Error("DB Error"); + checkCountDocumentsStub.throws(err); + try { + await createCheck({ monitorId: "123" }); + } catch (error) { + expect(error).to.deep.equal(err); + } + }); + }); + describe("getChecksCount", () => { + let checkCountDocumentStub; + + beforeEach(() => { + checkCountDocumentStub = sinon.stub(Check, "countDocuments"); + }); + + afterEach(() => { + checkCountDocumentStub.restore(); + }); + + it("should return count with basic monitorId query", async () => { + const req = { + params: { monitorId: "test123" }, + query: {}, + }; + checkCountDocumentStub.resolves(5); + + const result = await getChecksCount(req); + + expect(result).to.equal(5); + expect(checkCountDocumentStub.calledOnce).to.be.true; + expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({ + monitorId: "test123", + }); + }); + + it("should include dateRange in query when provided", async () => { + const req = { + params: { monitorId: "test123" }, + query: { dateRange: "day" }, + }; + checkCountDocumentStub.resolves(3); + + const result = await getChecksCount(req); + + expect(result).to.equal(3); + expect(checkCountDocumentStub.firstCall.args[0]).to.have.property("createdAt"); + expect(checkCountDocumentStub.firstCall.args[0].createdAt).to.have.property("$gte"); + }); + + it('should handle "all" filter correctly', async () => { + const req = { + params: { monitorId: "test123" }, + query: { filter: "all" }, + }; + checkCountDocumentStub.resolves(2); + + const result = await getChecksCount(req); + + expect(result).to.equal(2); + expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({ + monitorId: "test123", + status: false, + }); + }); + + it('should handle "down" filter correctly', async () => { + const req = { + params: { monitorId: "test123" }, + query: { filter: "down" }, + }; + checkCountDocumentStub.resolves(2); + + const result = await getChecksCount(req); + + expect(result).to.equal(2); + expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({ + monitorId: "test123", + status: false, + }); + }); + + it('should handle "resolve" filter correctly', async () => { + const req = { + params: { monitorId: "test123" }, + query: { filter: "resolve" }, + }; + checkCountDocumentStub.resolves(1); + + const result = await getChecksCount(req); + + expect(result).to.equal(1); + expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({ + monitorId: "test123", + status: false, + statusCode: 5000, + }); + }); + it("should handle unknown filter correctly", async () => { + const req = { + params: { monitorId: "test123" }, + query: { filter: "unknown" }, + }; + checkCountDocumentStub.resolves(1); + + const result = await getChecksCount(req); + + expect(result).to.equal(1); + expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({ + monitorId: "test123", + status: false, + }); + }); + + it("should combine dateRange and filter in query", async () => { + const req = { + params: { monitorId: "test123" }, + query: { + dateRange: "week", + filter: "down", + }, + }; + checkCountDocumentStub.resolves(4); + + const result = await getChecksCount(req); + + expect(result).to.equal(4); + expect(checkCountDocumentStub.firstCall.args[0]).to.have.all.keys( + "monitorId", + "createdAt", + "status" + ); + }); + }); + describe("getChecks", () => { + let checkFindStub, monitorFindStub; + + beforeEach(() => { + checkFindStub = sinon.stub(Check, "find").returns({ + skip: sinon.stub().returns({ + limit: sinon.stub().returns({ + sort: sinon.stub().returns([{ id: 1 }, { id: 2 }]), + }), + }), + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return checks with basic monitorId query", async () => { + const req = { + params: { monitorId: "test123" }, + query: {}, + }; + + const result = await getChecks(req); + + expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); + }); + it("should return checks with limit query", async () => { + const req = { + params: { monitorId: "test123" }, + query: { limit: 10 }, + }; + + const result = await getChecks(req); + + expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); + }); + + it("should handle pagination correctly", async () => { + const req = { + params: { monitorId: "test123" }, + query: { + page: 2, + rowsPerPage: 10, + }, + }; + + const result = await getChecks(req); + + expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); + }); + + it("should handle dateRange filter", async () => { + const req = { + params: { monitorId: "test123" }, + query: { dateRange: "week" }, + }; + const result = await getChecks(req); + + expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); + }); + + it('should handle "all" filter', async () => { + const req = { + params: { monitorId: "test123" }, + query: { filter: "all" }, + }; + + await getChecks(req); + const result = await getChecks(req); + expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); + }); + it('should handle "down" filter', async () => { + const req = { + params: { monitorId: "test123" }, + query: { filter: "down" }, + }; + + await getChecks(req); + const result = await getChecks(req); + expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); + }); + + it('should handle "resolve" filter', async () => { + const req = { + params: { monitorId: "test123" }, + query: { filter: "resolve" }, + }; + + await getChecks(req); + const result = await getChecks(req); + expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); + }); + it('should handle "unknown" filter', async () => { + const req = { + params: { monitorId: "test123" }, + query: { filter: "unknown" }, + }; + + await getChecks(req); + const result = await getChecks(req); + expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); + }); + + it("should handle ascending sort order", async () => { + const req = { + params: { monitorId: "test123" }, + query: { sortOrder: "asc" }, + }; + + await getChecks(req); + const result = await getChecks(req); + expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); + }); + + it("should handle error case", async () => { + const req = { + params: { monitorId: "test123" }, + query: {}, + }; + + checkFindStub.throws(new Error("Database error")); + + try { + await getChecks(req); + } catch (error) { + expect(error.message).to.equal("Database error"); + expect(error.service).to.equal("checkModule"); + expect(error.method).to.equal("getChecks"); + } + }); + }); + describe("getTeamChecks", () => { + let checkFindStub, checkCountDocumentsStub, monitorFindStub; + const mockMonitors = [{ _id: "123" }]; + beforeEach(() => { + monitorFindStub = sinon.stub(Monitor, "find").returns({ + select: sinon.stub().returns(mockMonitors), + }); + checkCountDocumentsStub = sinon.stub(Check, "countDocuments").returns(2); + checkFindStub = sinon.stub(Check, "find").returns({ + skip: sinon.stub().returns({ + limit: sinon.stub().returns({ + sort: sinon.stub().returns({ + select: sinon.stub().returns([{ id: 1 }, { id: 2 }]), + }), + }), + }), + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return checks with basic monitorId query", async () => { + const req = { + params: { teamId: "test123" }, + query: {}, + }; + + const result = await getTeamChecks(req); + expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); + }); + + it("should handle pagination correctly", async () => { + const req = { + params: { monitorId: "test123" }, + query: { limit: 1, page: 2, rowsPerPage: 10 }, + }; + + const result = await getTeamChecks(req); + expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); + }); + + it("should handle dateRange filter", async () => { + const req = { + params: { monitorId: "test123" }, + query: { dateRange: "week" }, + }; + const result = await getTeamChecks(req); + expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); + }); + + it('should handle "all" filter', async () => { + const req = { + params: { monitorId: "test123" }, + query: { filter: "all" }, + }; + + await getChecks(req); + const result = await getTeamChecks(req); + expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); + }); + it('should handle "down" filter', async () => { + const req = { + params: { monitorId: "test123" }, + query: { filter: "down" }, + }; + + await getChecks(req); + const result = await getTeamChecks(req); + expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); + }); + + it('should handle "resolve" filter', async () => { + const req = { + params: { monitorId: "test123" }, + query: { filter: "resolve" }, + }; + + await getChecks(req); + const result = await getTeamChecks(req); + expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); + }); + it('should handle "unknown" filter', async () => { + const req = { + params: { monitorId: "test123" }, + query: { filter: "unknown" }, + }; + + await getChecks(req); + const result = await getTeamChecks(req); + expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); + }); + + it("should handle ascending sort order", async () => { + const req = { + params: { monitorId: "test123" }, + query: { sortOrder: "asc" }, + }; + + await getChecks(req); + const result = await getTeamChecks(req); + expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); + }); + + it("should handle error case", async () => { + const req = { + params: { monitorId: "test123" }, + query: {}, + }; + + checkFindStub.throws(new Error("Database error")); + + try { + await getTeamChecks(req); + } catch (error) { + expect(error.message).to.equal("Database error"); + expect(error.service).to.equal("checkModule"); + expect(error.method).to.equal("getTeamChecks"); + } + }); + }); + describe("deleteChecks", () => { + let checkDeleteManyStub; + beforeEach(() => { + checkDeleteManyStub = sinon.stub(Check, "deleteMany").resolves({ deletedCount: 1 }); + }); + afterEach(() => { + sinon.restore(); + }); + + it("should return a value if a check is deleted", async () => { + const result = await deleteChecks("123"); + expect(result).to.equal(1); + }); + it("should handle an error", async () => { + checkDeleteManyStub.throws(new Error("Database error")); + try { + await deleteChecks("123"); + } catch (error) { + expect(error.message).to.equal("Database error"); + expect(error.method).to.equal("deleteChecks"); + } + }); + }); + describe("deleteChecksByTeamId", () => { + let mockMonitors = [{ _id: 123, save: () => this }]; + let monitorFindStub, monitorSaveStub, checkDeleteManyStub; + beforeEach(() => { + monitorSaveStub = sinon.stub(Monitor.prototype, "save"); + monitorFindStub = sinon.stub(Monitor, "find").returns(mockMonitors); + checkDeleteManyStub = sinon.stub(Check, "deleteMany").resolves({ deletedCount: 1 }); + }); + afterEach(() => { + sinon.restore(); + }); + it("should return a deleted count", async () => { + const result = await deleteChecksByTeamId("123"); + expect(result).to.equal(1); + }); + it("should handle errors", async () => { + const err = new Error("DB Error"); + monitorFindStub.throws(err); + try { + const result = await deleteChecksByTeamId("123"); + } catch (error) { + expect(error).to.deep.equal(err); + } + }); + }); + describe("updateChecksTTL", () => { + let userUpdateManyStub; + let loggerStub; + beforeEach(() => { + loggerStub = sinon.stub(logger, "error"); + userUpdateManyStub = sinon.stub(User, "updateMany"); + Check.collection = { dropIndex: sinon.stub(), createIndex: sinon.stub() }; + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return undefined", async () => { + const result = await updateChecksTTL("123", 10); + expect(result).to.be.undefined; + }); + + it("should log an error if dropIndex throws an error", async () => { + const err = new Error("Drop Index Error"); + Check.collection.dropIndex.throws(err); + await updateChecksTTL("123", 10); + expect(loggerStub.calledOnce).to.be.true; + expect(loggerStub.firstCall.args[0].message).to.equal(err.message); + }); + + it("should throw an error if createIndex throws an error", async () => { + const err = new Error("Create Index Error"); + Check.collection.createIndex.throws(err); + try { + await updateChecksTTL("123", 10); + } catch (error) { + expect(error).to.deep.equal(err); + } + }); + it("should throw an error if User.updateMany throws an error", async () => { + const err = new Error("Update Many Error"); + userUpdateManyStub.throws(err); + try { + await updateChecksTTL("123", 10); + } catch (error) { + expect(error).to.deep.equal(err); + } + }); + }); +});