Merge pull request #1117 from bluewave-labs/feat/be/uptime-percentage-stats

Feat/be/uptime percentage stats, references #1115
This commit is contained in:
Alexander Holliday authored and GitHub committed 2024-11-07 18:30:18 -08:00
commit ddb40bb56b
11 files changed
+972 -314

No files matched your search

+6 -1
View File
@@ -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,
+23
View File
@@ -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<Express.Response>}
* @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,
+2
View File
@@ -99,6 +99,7 @@ import {
import {
getAllMonitors,
getAllMonitorsWithUptimeStats,
getMonitorStatsById,
getMonitorById,
getMonitorsAndSummaryByTeamId,
@@ -187,6 +188,7 @@ export default {
resetPassword,
checkSuperadmin,
getAllMonitors,
getAllMonitorsWithUptimeStats,
getMonitorStatsById,
getMonitorById,
getMonitorsAndSummaryByTeamId,
+53 -48
View File
@@ -48,7 +48,6 @@ const createCheck = async (checkData) => {
}
await monitor.save();
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;
@@ -155,58 +154,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 };
};
/**
+48
View File
@@ -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<Array<Monitor>>}
* @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,
+96 -241
View File
@@ -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"
+21 -23
View File
@@ -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);
@@ -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;
+565
View File
@@ -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);
}
});
});
});
+95 -1
View File
@@ -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();
@@ -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 () => {