From 5a5c0c610d2e6010a18135ec73a05c434761b374 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 31 Oct 2024 15:01:08 +0800 Subject: [PATCH 01/34] replace cleanup method with better shutdown method --- Server/index.js | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/Server/index.js b/Server/index.js index b83a3d5ce..52a3784be 100644 --- a/Server/index.js +++ b/Server/index.js @@ -8,7 +8,6 @@ import cors from "cors"; import logger from "./utils/logger.js"; import { verifyJWT } from "./middleware/verifyJWT.js"; import { handleErrors } from "./middleware/handleErrors.js"; -import { errorMessages } from "./utils/messages.js"; import authRouter from "./routes/authRoute.js"; import inviteRouter from "./routes/inviteRoute.js"; import monitorRouter from "./routes/monitorRoute.js"; @@ -46,7 +45,6 @@ import NotificationService from "./service/notificationService.js"; import db from "./db/mongo/MongoDB.js"; const SERVICE_NAME = "Server"; -let cleaningUp = false; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -113,11 +111,11 @@ const startApp = async () => { // Create services await db.connect(); - app.listen(PORT, () => { + const server = app.listen(PORT, () => { logger.info({ message: `server started on port:${PORT}` }); }); - const settingsService = new SettingsService(AppSettings); + const settingsService = new SettingsService(AppSettings); await settingsService.loadSettings(); const emailService = new EmailService( settingsService, @@ -142,30 +140,35 @@ const startApp = async () => { Worker ); - const cleanup = async () => { - if (cleaningUp) { - logger.warn({ message: "Already cleaning up" }); - return; - } - cleaningUp = true; + const shutdown = async () => { + logger.info({ message: "Attempting graceful shutdown" }); + setTimeout(() => { + logger.error({ + message: "Could not shut down in time, forcing shutdown", + service: SERVICE_NAME, + method: "shutdown", + }); + process.exit(1); + }, 10000); try { - logger.info({ message: "shutting down gracefully" }); + server.close(); await jobQueue.obliterate(); await db.disconnect(); - logger.info({ message: "shut down gracefully" }); + logger.info({ message: "Graceful shutdown complete" }); + process.exit(0); } catch (error) { logger.error({ message: error.message, service: SERVICE_NAME, - method: "cleanup", + method: "shutdown", stack: error.stack, }); } - process.exit(0); }; - process.on("SIGUSR2", cleanup); - process.on("SIGINT", cleanup); - process.on("SIGTERM", cleanup); + + process.on("SIGUSR2", shutdown); + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); }; startApp().catch((error) => { From 0f30d58125ddbe658a40bd94bdf3d20ef827b8b6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 31 Oct 2024 15:59:51 +0800 Subject: [PATCH 02/34] Added isShuttigDown var to handle case of multiple kill/interupt signals --- Server/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Server/index.js b/Server/index.js index 52a3784be..3e083df50 100644 --- a/Server/index.js +++ b/Server/index.js @@ -45,6 +45,7 @@ import NotificationService from "./service/notificationService.js"; import db from "./db/mongo/MongoDB.js"; const SERVICE_NAME = "Server"; +let isShuttingDown = false; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -141,6 +142,10 @@ const startApp = async () => { ); const shutdown = async () => { + if (isShuttingDown) { + return; + } + isShuttingDown = true; logger.info({ message: "Attempting graceful shutdown" }); setTimeout(() => { logger.error({ @@ -149,7 +154,7 @@ const startApp = async () => { method: "shutdown", }); process.exit(1); - }, 10000); + }, 2000); try { server.close(); await jobQueue.obliterate(); From 120296763bf46e3723eeb1136059fe5be777fcbe Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 31 Oct 2024 16:00:29 +0800 Subject: [PATCH 03/34] Attempt to stop active jobs --- Server/service/jobQueue.js | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index 6d46052f1..3793a256a 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -36,10 +36,10 @@ class JobQueue { host: redisHost, port: redisPort, }; - this.connection = connection; this.queue = new Queue(QUEUE_NAME, { connection, }); + this.connection = connection; this.workers = []; this.db = null; this.networkService = null; @@ -406,9 +406,6 @@ class JobQueue { delayed: await this.queue.getDelayedCount(), repeatableJobs: (await this.queue.getRepeatableJobs()).length, }; - this.logger.info({ - message: metrics, - }); return metrics; } catch (error) { this.logger.error({ @@ -424,17 +421,33 @@ class JobQueue { * @async * @returns {Promise} - Returns true if obliteration is successful */ + async obliterate() { try { - let metrics = await this.getMetrics(); - this.logger.info({ message: metrics }); + this.logger.info({ message: "Attempting to obliterate job queue..." }); await this.queue.pause(); const jobs = await this.getJobs(); + // Stop currently active jobs + const redisClient = await this.queue.client; + const activeJobs = await this.queue.getJobs(["active"]); + if (activeJobs.length !== 0) { + this.logger.info({ message: "Attempting to stop active jobs..." }); + } + await Promise.all( + activeJobs.map(async (job) => { + await redisClient.del(`${this.queue.toKey(job.id)}:lock`); + await job.remove(); + }) + ); + + // Remove all repeatable jobs for (const job of jobs) { await this.queue.removeRepeatableByKey(job.key); await this.queue.remove(job.id); } + + // Close workers await Promise.all( this.workers.map(async (worker) => { await worker.close(); @@ -442,9 +455,13 @@ class JobQueue { ); await this.queue.obliterate(); - metrics = await this.getMetrics(); - this.logger.info({ message: metrics }); - this.logger.info({ message: successMessages.JOB_QUEUE_OBLITERATE }); + const metrics = await this.getMetrics(); + this.logger.info({ + message: successMessages.JOB_QUEUE_OBLITERATE, + service: SERVICE_NAME, + method: "obliterate", + details: metrics, + }); return true; } catch (error) { error.service === undefined ? (error.service = SERVICE_NAME) : null; From 3fc79d47ba4e690ca7e771891e0bdf4d8203f48f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 31 Oct 2024 16:01:04 +0800 Subject: [PATCH 04/34] Stringify objects in the details field of logger config --- Server/utils/logger.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Server/utils/logger.js b/Server/utils/logger.js index 60d509a6a..e263085a7 100644 --- a/Server/utils/logger.js +++ b/Server/utils/logger.js @@ -7,6 +7,10 @@ class Logger { if (message instanceof Object) { message = JSON.stringify(message, null, 2); } + + if (details instanceof Object) { + details = JSON.stringify(details, null, 2); + } let msg = `${timestamp} ${level}:`; service && (msg += ` [${service}]`); method && (msg += `(${method})`); From c83e2cb6f2fe042448ec7f27e3c16d6d2ae5f340 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 31 Oct 2024 17:01:27 +0800 Subject: [PATCH 05/34] Add module var for shutdown timeout --- Server/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Server/index.js b/Server/index.js index 3e083df50..e12d83a18 100644 --- a/Server/index.js +++ b/Server/index.js @@ -44,6 +44,7 @@ import NotificationService from "./service/notificationService.js"; import db from "./db/mongo/MongoDB.js"; const SERVICE_NAME = "Server"; +const SHUTDOWN_TIMEOUT = 0; let isShuttingDown = false; const __filename = fileURLToPath(import.meta.url); @@ -154,7 +155,7 @@ const startApp = async () => { method: "shutdown", }); process.exit(1); - }, 2000); + }, SHUTDOWN_TIMEOUT); try { server.close(); await jobQueue.obliterate(); From ce221afa9fe0e8247455633668df8845d66d9908 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 31 Oct 2024 17:02:02 +0800 Subject: [PATCH 06/34] remove active job code, does not accomplish intended purpose --- Server/service/jobQueue.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index 3793a256a..aaab725e5 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -428,19 +428,6 @@ class JobQueue { await this.queue.pause(); const jobs = await this.getJobs(); - // Stop currently active jobs - const redisClient = await this.queue.client; - const activeJobs = await this.queue.getJobs(["active"]); - if (activeJobs.length !== 0) { - this.logger.info({ message: "Attempting to stop active jobs..." }); - } - await Promise.all( - activeJobs.map(async (job) => { - await redisClient.del(`${this.queue.toKey(job.id)}:lock`); - await job.remove(); - }) - ); - // Remove all repeatable jobs for (const job of jobs) { await this.queue.removeRepeatableByKey(job.key); From 401c3b83feaf8a2c135ca7669d03750c99d6b12c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 5 Nov 2024 12:08:19 +0800 Subject: [PATCH 07/34] Refactor getMonitorStatsById for clarity --- Server/db/mongo/modules/monitorModule.js | 223 ++++++++++++----------- 1 file changed, 117 insertions(+), 106 deletions(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 391e02146..e3bdfbb57 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -56,7 +56,7 @@ const calculateUptimeDuration = (checks) => { const latestCheck = new Date(checks[0].createdAt); let latestDownCheck = 0; - for (let i = checks.length; i <= 0; i--) { + for (let i = checks.length; i >= 0; i--) { if (checks[i].status === false) { latestDownCheck = new Date(checks[i].createdAt); break; @@ -143,6 +143,98 @@ const getIncidents = (checks) => { }, 0); }; +/** + * Get date range parameters + * @param {string} dateRange - 'day' | 'week' | 'month' + * @returns {Object} Start and end dates + */ +const getDateRange = (dateRange) => { + const startDates = { + day: new Date(new Date().setDate(new Date().getDate() - 1)), + week: new Date(new Date().setDate(new Date().getDate() - 7)), + month: new Date(new Date().setMonth(new Date().getMonth() - 1)), + }; + return { + start: startDates[dateRange], + end: new Date(), + }; +}; + +/** + * Get checks for a monitor + * @param {string} monitorId - Monitor ID + * @param {Object} model - Check model to use + * @param {Object} dateRange - Date range parameters + * @param {number} sortOrder - Sort order (1 for ascending, -1 for descending) + * @returns {Promise} All checks and date-ranged checks + */ +const getMonitorChecks = async (monitorId, model, dateRange, sortOrder) => { + const [checksAll, checksForDateRange] = await Promise.all([ + model.find({ monitorId }).sort({ createdAt: sortOrder }), + model + .find({ + monitorId, + createdAt: { $gte: dateRange.start, $lte: dateRange.end }, + }) + .sort({ createdAt: sortOrder }), + ]); + return { checksAll, checksForDateRange }; +}; + +/** + * Process checks for display + * @param {Array} checks - Checks to process + * @param {number} numToDisplay - Number of checks to display + * @param {boolean} normalize - Whether to normalize the data + * @returns {Array} Processed checks + */ +const processChecksForDisplay = (checks, numToDisplay, normalize) => { + let processedChecks = checks; + if (numToDisplay && checks.length > numToDisplay) { + const n = Math.ceil(checks.length / numToDisplay); + processedChecks = checks.filter((_, index) => index % n === 0); + } + return normalize ? NormalizeData(processedChecks, 1, 100) : processedChecks; +}; + +/** + * Get time-grouped checks based on date range + * @param {Array} checks Array of check objects + * @param {string} dateRange 'day' | 'week' | 'month' + * @returns {Object} Grouped checks by time period + */ +const groupChecksByTime = (checks, dateRange) => { + return checks.reduce((acc, check) => { + const time = + dateRange === "day" + ? new Date(check.createdAt).setMinutes(0, 0, 0) + : new Date(check.createdAt).toISOString().split("T")[0]; + + if (!acc[time]) { + acc[time] = { time, checks: [] }; + } + acc[time].checks.push(check); + return acc; + }, {}); +}; + +/** + * Calculate aggregate stats for a group of checks + * @param {Object} group Group of checks + * @returns {Object} Stats for the group + */ +const calculateGroupStats = (group) => { + const totalChecks = group.checks.length; + return { + time: group.time, + uptimePercentage: getUptimePercentage(group.checks), + totalChecks, + totalIncidents: group.checks.filter((check) => !check.status).length, + avgResponseTime: + group.checks.reduce((sum, check) => sum + check.responseTime, 0) / totalChecks, + }; +}; + /** * Get stats by monitor ID * @async @@ -152,129 +244,48 @@ const getIncidents = (checks) => { * @throws {Error} */ const getMonitorStatsById = async (req) => { - const startDates = { - day: new Date(new Date().setDate(new Date().getDate() - 1)), - week: new Date(new Date().setDate(new Date().getDate() - 7)), - month: new Date(new Date().setMonth(new Date().getMonth() - 1)), - }; - const endDate = new Date(); try { - // Get monitor const { monitorId } = req.params; - let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query; + + // Get monitor, if we can't find it, abort with error const monitor = await Monitor.findById(monitorId); if (monitor === null || monitor === undefined) { throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)); } - // This effectively removes limit, returning all checks - if (limit === undefined) limit = 0; - // Default sort order is newest -> oldest - sortOrder = sortOrder === "asc" ? 1 : -1; - let model = CHECK_MODEL_LOOKUP[monitor.type]; + // Get query params + let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query; + const sort = sortOrder === "asc" ? 1 : -1; + // Get Checks for monitor in date range requested + const model = CHECK_MODEL_LOOKUP[monitor.type]; + const dates = getDateRange(dateRange); + const { checksAll, checksForDateRange } = await getMonitorChecks( + monitorId, + model, + dates, + sort + ); + + // Build monitor stats const monitorStats = { ...monitor.toObject(), + uptimeDuration: calculateUptimeDuration(checksAll), + lastChecked: getLastChecked(checksAll), + latestResponseTime: getLatestResponseTime(checksAll), + periodIncidents: getIncidents(checksForDateRange), + periodTotalChecks: checksForDateRange.length, + checks: processChecksForDisplay(checksForDateRange, numToDisplay, normalize), }; - // Build checks query - const checksQuery = { monitorId: monitor._id }; - - // Get all checks - const checksAll = await model.find(checksQuery).sort({ - createdAt: sortOrder, - }); - - const checksQueryForDateRange = { - ...checksQuery, - createdAt: { - $gte: startDates[dateRange], - $lte: endDate, - }, - }; - - const checksForDateRange = await model - .find(checksQueryForDateRange) - .sort({ createdAt: sortOrder }); - if (monitor.type === "http" || monitor.type === "ping") { // HTTP/PING Specific stats monitorStats.periodAvgResponseTime = getAverageResponseTime(checksForDateRange); monitorStats.periodUptime = getUptimePercentage(checksForDateRange); - - // Aggregate data - let groupedChecks; - // Group checks by hour if range is day - if (dateRange === "day") { - groupedChecks = checksForDateRange.reduce((acc, check) => { - const time = new Date(check.createdAt); - time.setMinutes(0, 0, 0); - if (!acc[time]) { - acc[time] = { time, checks: [] }; - } - acc[time].checks.push(check); - return acc; - }, {}); - } else { - groupedChecks = checksForDateRange.reduce((acc, check) => { - const time = new Date(check.createdAt).toISOString().split("T")[0]; // Extract the date part - if (!acc[time]) { - acc[time] = { time, checks: [] }; - } - acc[time].checks.push(check); - return acc; - }, {}); - } - - // Map grouped checks to stats - const aggregateData = Object.values(groupedChecks).map((group) => { - const totalChecks = group.checks.length; - const uptimePercentage = getUptimePercentage(group.checks); - const totalIncidents = group.checks.filter( - (check) => check.status === false - ).length; - const avgResponseTime = - group.checks.reduce((sum, check) => sum + check.responseTime, 0) / totalChecks; - - return { - time: group.time, - uptimePercentage, - totalChecks, - totalIncidents, - avgResponseTime, - }; - }); - monitorStats.aggregateData = aggregateData; + const groupedChecks = groupChecksByTime(checksForDateRange, dateRange); + monitorStats.aggregateData = Object.values(groupedChecks).map(calculateGroupStats); } - monitorStats.periodIncidents = getIncidents(checksForDateRange); - monitorStats.periodTotalChecks = checksForDateRange.length; - - // If more than numToDisplay checks, pick every nth check - - let nthChecks = checksForDateRange; - - if ( - numToDisplay !== undefined && - checksForDateRange && - checksForDateRange.length > numToDisplay - ) { - const n = Math.ceil(checksForDateRange.length / numToDisplay); - nthChecks = checksForDateRange.filter((_, index) => index % n === 0); - } - - // Normalize checks if requested - if (normalize !== undefined) { - const normailzedChecks = NormalizeData(nthChecks, 1, 100); - monitorStats.checks = normailzedChecks; - } else { - monitorStats.checks = nthChecks; - } - - monitorStats.uptimeDuration = calculateUptimeDuration(checksAll); - monitorStats.lastChecked = getLastChecked(checksAll); - monitorStats.latestResponseTime = getLatestResponseTime(checksAll); - return monitorStats; } catch (error) { error.service = SERVICE_NAME; From b5e8bdbdcdc581616c3c0f14a8eb821f406d8dbe Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 5 Nov 2024 12:22:48 +0800 Subject: [PATCH 08/34] fix conditional inloop for uptime duration --- Server/db/mongo/modules/monitorModule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index e3bdfbb57..c40e7ea7b 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -56,7 +56,7 @@ const calculateUptimeDuration = (checks) => { const latestCheck = new Date(checks[0].createdAt); let latestDownCheck = 0; - for (let i = checks.length; i >= 0; i--) { + for (let i = checks.length - 1; i >= 0; i--) { if (checks[i].status === false) { latestDownCheck = new Date(checks[i].createdAt); break; From 4601f956866a6c715b2b172b102bc28a78666874 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 5 Nov 2024 12:27:13 +0800 Subject: [PATCH 09/34] refactor getMonitorById for clarity --- Server/db/mongo/modules/monitorModule.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index c40e7ea7b..76761ef26 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -314,12 +314,12 @@ const getMonitorById = async (monitorId) => { const notifications = await Notification.find({ monitorId: monitorId, }); - const updatedMonitor = await Monitor.findByIdAndUpdate( - monitorId, - { notifications }, - { new: true } - ).populate("notifications"); - return updatedMonitor; + + // Update monitor with notifications and save + monitor.notifications = notifications; + await monitor.save(); + + return monitor; } catch (error) { error.service = SERVICE_NAME; error.method = "getMonitorById"; From 2873f68868543e4ee21fb4324147a480a8146cd7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 5 Nov 2024 12:39:35 +0800 Subject: [PATCH 10/34] reactor getMonitorsByTeamId for clarity --- Server/db/mongo/modules/monitorModule.js | 46 ++++++++---------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 76761ef26..d6f18b3b7 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -383,9 +383,11 @@ const getMonitorsByTeamId = async (req, res) => { field, order, } = req.query || {}; + const monitorQuery = { teamId: req.params.teamId }; + if (type !== undefined) { - monitorQuery.type = type; + monitorQuery.type = Array.isArray(type) ? { $in: type } : type; } // Add filter if provided // $options: "i" makes the search case-insensitive @@ -395,29 +397,13 @@ const getMonitorsByTeamId = async (req, res) => { { url: { $regex: filter, $options: "i" } }, ]; } - const monitorsCount = await Monitor.countDocuments(monitorQuery); + const monitorCount = await Monitor.countDocuments(monitorQuery); // Pagination - let skip = 0; - if (page && rowsPerPage) { - skip = page * rowsPerPage; - } + const skip = page && rowsPerPage ? page * rowsPerPage : 0; - if (type !== undefined) { - const types = Array.isArray(type) ? type : [type]; - monitorQuery.type = { $in: types }; - } - - // Default sort order is newest -> oldest - if (checkOrder === "asc") { - checkOrder = 1; - } else checkOrder = -1; - - // Sort order for monitors - let sort = {}; - if (field !== undefined && order !== undefined) { - sort[field] = order === "asc" ? 1 : -1; - } + // Build Sort option + const sort = field ? { [field]: order === "asc" ? 1 : -1 } : {}; const monitors = await Monitor.find(monitorQuery) .skip(skip) @@ -426,16 +412,12 @@ const getMonitorsByTeamId = async (req, res) => { // Early return if limit is set to -1, indicating we don't want any checks if (limit === "-1") { - return { monitors, monitorCount: monitorsCount }; + return { monitors, monitorCount }; } - // This effectively removes limit, returning all checks - if (limit === undefined) limit = 0; - // Map each monitor to include its associated checks const monitorsWithChecks = await Promise.all( monitors.map(async (monitor) => { - const checksQuery = { monitorId: monitor._id }; if (status !== undefined) { checksQuery.status = status; } @@ -444,11 +426,13 @@ const getMonitorsByTeamId = async (req, res) => { // Checks are order newest -> oldest let checks = await model - .find(checksQuery) - .sort({ - createdAt: checkOrder, + .find({ + monitorId: monitor._id, + ...(status && { status }), }) - .limit(limit); + .sort({ createdAt: checkOrder === "asc" ? 1 : -1 }) + + .limit(limit || 0); //Normalize checks if requested if (normalize !== undefined) { @@ -457,7 +441,7 @@ const getMonitorsByTeamId = async (req, res) => { return { ...monitor.toObject(), checks }; }) ); - return { monitors: monitorsWithChecks, monitorCount: monitorsCount }; + return { monitors: monitorsWithChecks, monitorCount }; } catch (error) { error.service = SERVICE_NAME; error.method = "getMonitorsByTeamId"; From 4ca3e3211fca1d340fae9e17f2ce49611cc12c7f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 5 Nov 2024 13:31:10 +0800 Subject: [PATCH 11/34] Export helper methods, add safe return to --- Server/db/mongo/modules/monitorModule.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index d6f18b3b7..57497485d 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -94,7 +94,8 @@ const getLatestResponseTime = (checks) => { if (!checks || checks.length === 0) { return 0; } - return checks[0].responseTime; + + return checks[0]?.responseTime ?? 0; }; /** @@ -586,3 +587,13 @@ export { editMonitor, addDemoMonitors, }; + +// Helper functions +export { + calculateUptimeDuration, + getLastChecked, + getLatestResponseTime, + getAverageResponseTime, + getUptimePercentage, + getIncidents, +}; From 8605b068ce8b366eb8d418495f1f7dd1507fd739 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 5 Nov 2024 13:36:31 +0800 Subject: [PATCH 12/34] Refactor for robustness --- Server/db/mongo/modules/monitorModule.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 57497485d..9f009b451 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -107,10 +107,15 @@ const getAverageResponseTime = (checks) => { if (!checks || checks.length === 0) { return 0; } - const aggResponseTime = checks.reduce((sum, check) => { + + const validChecks = checks.filter((check) => typeof check.responseTime === "number"); + if (validChecks.length === 0) { + return 0; + } + const aggResponseTime = validChecks.reduce((sum, check) => { return sum + check.responseTime; }, 0); - return aggResponseTime / checks.length; + return aggResponseTime / validChecks.length; }; /** From 0fd84d5ad816e0a1ff98443a32433abb725be39f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 5 Nov 2024 13:50:46 +0800 Subject: [PATCH 13/34] Fix spelling error, export functions, refactor to inject for ease of testing --- Server/db/mongo/modules/monitorModule.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 9f009b451..4d7bd7208 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -119,7 +119,7 @@ const getAverageResponseTime = (checks) => { }; /** - * Helper function to get precentage 24h uptime + * Helper function to get percentage 24h uptime * @param {Array} checks Array of check objects. * @returns {number} Timestamp of the most recent check. */ @@ -194,13 +194,13 @@ const getMonitorChecks = async (monitorId, model, dateRange, sortOrder) => { * @param {boolean} normalize - Whether to normalize the data * @returns {Array} Processed checks */ -const processChecksForDisplay = (checks, numToDisplay, normalize) => { +const processChecksForDisplay = (normalizeData, checks, numToDisplay, normalize) => { let processedChecks = checks; if (numToDisplay && checks.length > numToDisplay) { const n = Math.ceil(checks.length / numToDisplay); processedChecks = checks.filter((_, index) => index % n === 0); } - return normalize ? NormalizeData(processedChecks, 1, 100) : processedChecks; + return normalize ? normalizeData(processedChecks, 1, 100) : processedChecks; }; /** @@ -281,7 +281,12 @@ const getMonitorStatsById = async (req) => { latestResponseTime: getLatestResponseTime(checksAll), periodIncidents: getIncidents(checksForDateRange), periodTotalChecks: checksForDateRange.length, - checks: processChecksForDisplay(checksForDateRange, numToDisplay, normalize), + checks: processChecksForDisplay( + NormalizeData, + checksForDateRange, + numToDisplay, + normalize + ), }; if (monitor.type === "http" || monitor.type === "ping") { @@ -601,4 +606,9 @@ export { getAverageResponseTime, getUptimePercentage, getIncidents, + getDateRange, + getMonitorChecks, + processChecksForDisplay, + groupChecksByTime, + calculateGroupStats, }; From f3ec84d36fd89fc2450fce7d8f2f7b3ec833f300 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 5 Nov 2024 14:09:32 +0800 Subject: [PATCH 14/34] Refactor for robustness --- Server/db/mongo/modules/monitorModule.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 4d7bd7208..63236993a 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -211,10 +211,16 @@ const processChecksForDisplay = (normalizeData, checks, numToDisplay, normalize) */ const groupChecksByTime = (checks, dateRange) => { return checks.reduce((acc, check) => { + // Validate the date + const checkDate = new Date(check.createdAt); + if (isNaN(checkDate.getTime()) || checkDate.getTime() === 0) { + return acc; + } + const time = dateRange === "day" - ? new Date(check.createdAt).setMinutes(0, 0, 0) - : new Date(check.createdAt).toISOString().split("T")[0]; + ? checkDate.setMinutes(0, 0, 0) + : checkDate.toISOString().split("T")[0]; if (!acc[time]) { acc[time] = { time, checks: [] }; @@ -231,13 +237,21 @@ const groupChecksByTime = (checks, dateRange) => { */ const calculateGroupStats = (group) => { const totalChecks = group.checks.length; + + const checksWithResponseTime = group.checks.filter( + (check) => typeof check.responseTime === "number" && !isNaN(check.responseTime) + ); + return { time: group.time, uptimePercentage: getUptimePercentage(group.checks), totalChecks, totalIncidents: group.checks.filter((check) => !check.status).length, avgResponseTime: - group.checks.reduce((sum, check) => sum + check.responseTime, 0) / totalChecks, + checksWithResponseTime.length > 0 + ? checksWithResponseTime.reduce((sum, check) => sum + check.responseTime, 0) / + checksWithResponseTime.length + : 0, }; }; From 53fb79bbfb3d49c5bc624f46857c3afac2f6eff3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 5 Nov 2024 16:03:37 +0800 Subject: [PATCH 15/34] Add tests for monitor module --- Server/db/mongo/modules/monitorModule.js | 3 +- Server/tests/db/monitorModule.test.js | 1581 ++++++++++++++++++++++ 2 files changed, 1582 insertions(+), 2 deletions(-) create mode 100644 Server/tests/db/monitorModule.test.js diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 63236993a..d12394305 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -52,7 +52,6 @@ const calculateUptimeDuration = (checks) => { if (!checks || checks.length === 0) { return 0; } - const latestCheck = new Date(checks[0].createdAt); let latestDownCheck = 0; @@ -439,7 +438,6 @@ const getMonitorsByTeamId = async (req, res) => { if (limit === "-1") { return { monitors, monitorCount }; } - // Map each monitor to include its associated checks const monitorsWithChecks = await Promise.all( monitors.map(async (monitor) => { @@ -488,6 +486,7 @@ const createMonitor = async (req, res) => { // Remove notifications fom monitor as they aren't needed here monitor.notifications = undefined; await monitor.save(); + console.log(monitor); return monitor; } catch (error) { error.service = SERVICE_NAME; diff --git a/Server/tests/db/monitorModule.test.js b/Server/tests/db/monitorModule.test.js new file mode 100644 index 000000000..a9092074a --- /dev/null +++ b/Server/tests/db/monitorModule.test.js @@ -0,0 +1,1581 @@ +import sinon from "sinon"; +import Monitor from "../../db/models/Monitor.js"; +import Check from "../../db/models/Check.js"; +import Notification from "../../db/models/Notification.js"; + +import { errorMessages } from "../../utils/messages.js"; +import { + getAllMonitors, + getMonitorStatsById, + getMonitorById, + getMonitorsAndSummaryByTeamId, + getMonitorsByTeamId, + createMonitor, + deleteMonitor, + deleteAllMonitors, + deleteMonitorsByUserId, + editMonitor, + addDemoMonitors, + calculateUptimeDuration, + getLastChecked, + getLatestResponseTime, + getAverageResponseTime, + getUptimePercentage, + getIncidents, + getDateRange, + getMonitorChecks, + processChecksForDisplay, + groupChecksByTime, + calculateGroupStats, +} from "../../db/mongo/modules/monitorModule.js"; + +describe("monitorModule", () => { + let monitorFindStub, + monitorFindByIdStub, + monitorFindByIdAndUpdateStub, + monitorFindByIdAndDeleteStub, + monitorDeleteManyStub, + monitorCountStub, + monitorInsertManyStub, + checkFindStub; + beforeEach(() => { + monitorFindStub = sinon.stub(Monitor, "find"); + monitorFindByIdStub = sinon.stub(Monitor, "findById"); + monitorFindByIdAndUpdateStub = sinon.stub(Monitor, "findByIdAndUpdate"); + monitorFindByIdAndDeleteStub = sinon.stub(Monitor, "findByIdAndDelete"); + monitorDeleteManyStub = sinon.stub(Monitor, "deleteMany"); + monitorCountStub = sinon.stub(Monitor, "countDocuments"); + + monitorInsertManyStub = sinon.stub(Monitor, "insertMany"); + checkFindStub = sinon.stub(Check, "find").returns({ + sort: sinon.stub(), + }); + }); + afterEach(() => { + sinon.restore(); + }); + + describe("getAllMonitors", () => { + it("should return all monitors", async () => { + const mockMonitors = [ + { _id: "1", name: "Monitor 1", url: "test1.com" }, + { _id: "2", name: "Monitor 2", url: "test2.com" }, + ]; + monitorFindStub.returns(mockMonitors); + const result = await getAllMonitors(); + + expect(result).to.deep.equal(mockMonitors); + expect(monitorFindStub.calledOnce).to.be.true; + expect(monitorFindStub.firstCall.args).to.deep.equal([]); + }); + it("should handle empty results", async () => { + monitorFindStub.returns([]); + const result = await getAllMonitors(); + expect(result).to.be.an("array").that.is.empty; + }); + it("should throw error when database fails", async () => { + // Arrange + const error = new Error("Database error"); + error.service = "MonitorModule"; + error.method = "getAllMonitors"; + monitorFindStub.rejects(error); + // Act & Assert + try { + await getAllMonitors(); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err).to.equal(error); + expect(err.service).to.equal("monitorModule"); + expect(err.method).to.equal("getAllMonitors"); + } + }); + }); + + describe("calculateUptimeDuration", () => { + let clock; + const NOW = new Date("2024-01-01T12:00:00Z").getTime(); + + beforeEach(() => { + // Fix the current time + clock = sinon.useFakeTimers(NOW); + }); + + afterEach(() => { + clock.restore(); + }); + + it("should return 0 when checks array is empty", () => { + expect(calculateUptimeDuration([])).to.equal(0); + }); + + it("should return 0 when checks array is null", () => { + expect(calculateUptimeDuration(null)).to.equal(0); + }); + + it("should calculate uptime from last down check to most recent check", () => { + const checks = [ + { status: true, createdAt: "2024-01-01T11:00:00Z" }, // Most recent + { status: true, createdAt: "2024-01-01T10:00:00Z" }, + { status: false, createdAt: "2024-01-01T09:00:00Z" }, // Last down + { status: true, createdAt: "2024-01-01T08:00:00Z" }, + ]; + + // Expected: 2 hours (from 09:00 to 11:00) = 7200000ms + expect(calculateUptimeDuration(checks)).to.equal(7200000); + }); + + it("should calculate uptime from first check when no down checks exist", () => { + const checks = [ + { status: true, createdAt: "2024-01-01T11:00:00Z" }, + { status: true, createdAt: "2024-01-01T10:00:00Z" }, + { status: true, createdAt: "2024-01-01T09:00:00Z" }, + ]; + + // Expected: Current time (12:00) - First check (09:00) = 3 hours = 10800000ms + expect(calculateUptimeDuration(checks)).to.equal(10800000); + }); + }); + + describe("getLastChecked", () => { + let clock; + const NOW = new Date("2024-01-01T12:00:00Z").getTime(); + + beforeEach(() => { + // Fix the current time + clock = sinon.useFakeTimers(NOW); + }); + + afterEach(() => { + clock.restore(); + }); + + it("should return 0 when checks array is empty", () => { + expect(getLastChecked([])).to.equal(0); + }); + + it("should return 0 when checks array is null", () => { + expect(getLastChecked(null)).to.equal(0); + }); + + it("should return time difference between now and most recent check", () => { + const checks = [ + { createdAt: "2024-01-01T11:30:00Z" }, // 30 minutes ago + { createdAt: "2024-01-01T11:00:00Z" }, + { createdAt: "2024-01-01T10:30:00Z" }, + ]; + + // Expected: 30 minutes = 1800000ms + expect(getLastChecked(checks)).to.equal(1800000); + }); + + it("should handle checks from different days", () => { + const checks = [ + { createdAt: "2023-12-31T12:00:00Z" }, // 24 hours ago + { createdAt: "2023-12-30T12:00:00Z" }, + ]; + + // Expected: 24 hours = 86400000ms + expect(getLastChecked(checks)).to.equal(86400000); + }); + }); + describe("getLatestResponseTime", () => { + it("should return 0 when checks array is empty", () => { + expect(getLatestResponseTime([])).to.equal(0); + }); + + it("should return 0 when checks array is null", () => { + expect(getLatestResponseTime(null)).to.equal(0); + }); + + it("should return response time from most recent check", () => { + const checks = [ + { responseTime: 150, createdAt: "2024-01-01T11:30:00Z" }, // Most recent + { responseTime: 200, createdAt: "2024-01-01T11:00:00Z" }, + { responseTime: 250, createdAt: "2024-01-01T10:30:00Z" }, + ]; + + expect(getLatestResponseTime(checks)).to.equal(150); + }); + + it("should handle missing responseTime in checks", () => { + const checks = [ + { createdAt: "2024-01-01T11:30:00Z" }, + { responseTime: 200, createdAt: "2024-01-01T11:00:00Z" }, + ]; + + expect(getLatestResponseTime(checks)).to.equal(0); + }); + }); + describe("getAverageResponseTime", () => { + it("should return 0 when checks array is empty", () => { + expect(getAverageResponseTime([])).to.equal(0); + }); + + it("should return 0 when checks array is null", () => { + expect(getAverageResponseTime(null)).to.equal(0); + }); + + it("should calculate average response time from all checks", () => { + const checks = [ + { responseTime: 100, createdAt: "2024-01-01T11:30:00Z" }, + { responseTime: 200, createdAt: "2024-01-01T11:00:00Z" }, + { responseTime: 300, createdAt: "2024-01-01T10:30:00Z" }, + ]; + + // Average: (100 + 200 + 300) / 3 = 200 + expect(getAverageResponseTime(checks)).to.equal(200); + }); + + it("should handle missing responseTime in some checks", () => { + const checks = [ + { responseTime: 100, createdAt: "2024-01-01T11:30:00Z" }, + { createdAt: "2024-01-01T11:00:00Z" }, + { responseTime: 300, createdAt: "2024-01-01T10:30:00Z" }, + ]; + + // Average: (100 + 300) / 2 = 200 + expect(getAverageResponseTime(checks)).to.equal(200); + }); + + it("should return 0 when no checks have responseTime", () => { + const checks = [ + { createdAt: "2024-01-01T11:30:00Z" }, + { createdAt: "2024-01-01T11:00:00Z" }, + ]; + + expect(getAverageResponseTime(checks)).to.equal(0); + }); + }); + describe("getUptimePercentage", () => { + it("should return 0 when checks array is empty", () => { + expect(getUptimePercentage([])).to.equal(0); + }); + + it("should return 0 when checks array is null", () => { + expect(getUptimePercentage(null)).to.equal(0); + }); + + it("should return 100 when all checks are up", () => { + const checks = [{ status: true }, { status: true }, { status: true }]; + expect(getUptimePercentage(checks)).to.equal(100); + }); + + it("should return 0 when all checks are down", () => { + const checks = [{ status: false }, { status: false }, { status: false }]; + expect(getUptimePercentage(checks)).to.equal(0); + }); + + it("should calculate correct percentage for mixed status checks", () => { + const checks = [ + { status: true }, + { status: false }, + { status: true }, + { status: true }, + ]; + // 3 up out of 4 total = 75% + expect(getUptimePercentage(checks)).to.equal(75); + }); + + it("should handle undefined status values", () => { + const checks = [{ status: true }, { status: undefined }, { status: true }]; + // 2 up out of 3 total ≈ 66.67% + expect(getUptimePercentage(checks)).to.equal((2 / 3) * 100); + }); + }); + describe("getIncidents", () => { + it("should return 0 when checks array is empty", () => { + expect(getIncidents([])).to.equal(0); + }); + + it("should return 0 when checks array is null", () => { + expect(getIncidents(null)).to.equal(0); + }); + + it("should return 0 when all checks are up", () => { + const checks = [{ status: true }, { status: true }, { status: true }]; + expect(getIncidents(checks)).to.equal(0); + }); + + it("should count all incidents when all checks are down", () => { + const checks = [{ status: false }, { status: false }, { status: false }]; + expect(getIncidents(checks)).to.equal(3); + }); + + it("should count correct number of incidents for mixed status checks", () => { + const checks = [ + { status: true }, + { status: false }, + { status: true }, + { status: false }, + { status: true }, + ]; + expect(getIncidents(checks)).to.equal(2); + }); + + it("should handle undefined status values", () => { + const checks = [ + { status: true }, + { status: undefined }, + { status: false }, + { status: false }, + ]; + // Only counts explicit false values + expect(getIncidents(checks)).to.equal(2); + }); + }); + describe("getMonitorChecks", () => { + let mockModel; + + beforeEach(() => { + // Create a mock model with chainable methods + const mockChecks = [ + { monitorId: "123", createdAt: new Date("2024-01-01") }, + { monitorId: "123", createdAt: new Date("2024-01-02") }, + ]; + + mockModel = { + find: sinon.stub().returns({ + sort: sinon.stub().returns(mockChecks), + }), + }; + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return all checks and date-ranged checks", async () => { + // Arrange + const monitorId = "123"; + const dateRange = { + start: new Date("2024-01-01"), + end: new Date("2024-01-02"), + }; + const sortOrder = -1; + + // Act + const result = await getMonitorChecks(monitorId, mockModel, dateRange, sortOrder); + + // Assert + expect(result).to.have.keys(["checksAll", "checksForDateRange"]); + + // Verify find was called with correct parameters + expect(mockModel.find.firstCall.args[0]).to.deep.equal({ monitorId }); + expect(mockModel.find.secondCall.args[0]).to.deep.equal({ + monitorId, + createdAt: { $gte: dateRange.start, $lte: dateRange.end }, + }); + + // Verify sort was called with correct parameters + const sortCalls = mockModel.find().sort.getCalls(); + sortCalls.forEach((call) => { + expect(call.args[0]).to.deep.equal({ createdAt: sortOrder }); + }); + }); + + it("should handle empty results", async () => { + // Arrange + const emptyModel = { + find: sinon.stub().returns({ + sort: sinon.stub().returns([]), + }), + }; + + // Act + const result = await getMonitorChecks( + "123", + emptyModel, + { + start: new Date(), + end: new Date(), + }, + -1 + ); + + // Assert + expect(result.checksAll).to.be.an("array").that.is.empty; + expect(result.checksForDateRange).to.be.an("array").that.is.empty; + }); + + it("should maintain sort order", async () => { + // Arrange + const sortedChecks = [ + { monitorId: "123", createdAt: new Date("2024-01-02") }, + { monitorId: "123", createdAt: new Date("2024-01-01") }, + ]; + + const sortedModel = { + find: sinon.stub().returns({ + sort: sinon.stub().returns(sortedChecks), + }), + }; + + // Act + const result = await getMonitorChecks( + "123", + sortedModel, + { + start: new Date("2024-01-01"), + end: new Date("2024-01-02"), + }, + -1 + ); + + // Assert + expect(result.checksAll[0].createdAt).to.be.greaterThan( + result.checksAll[1].createdAt + ); + expect(result.checksForDateRange[0].createdAt).to.be.greaterThan( + result.checksForDateRange[1].createdAt + ); + }); + }); + + describe("processChecksForDisplay", () => { + let normalizeStub; + + beforeEach(() => { + normalizeStub = sinon.stub(); + }); + + it("should return original checks when numToDisplay is not provided", () => { + const checks = [1, 2, 3, 4, 5]; + const result = processChecksForDisplay(normalizeStub, checks); + expect(result).to.deep.equal(checks); + }); + + it("should return original checks when numToDisplay is greater than checks length", () => { + const checks = [1, 2, 3]; + const result = processChecksForDisplay(normalizeStub, checks, 5); + expect(result).to.deep.equal(checks); + }); + + it("should filter checks based on numToDisplay", () => { + const checks = [1, 2, 3, 4, 5, 6]; + const result = processChecksForDisplay(normalizeStub, checks, 3); + // Should return [1, 3, 5] as n = ceil(6/3) = 2 + expect(result).to.deep.equal([1, 3, 5]); + }); + + it("should handle empty checks array", () => { + const checks = []; + const result = processChecksForDisplay(normalizeStub, checks, 3); + expect(result).to.be.an("array").that.is.empty; + }); + + it("should call normalizeData when normalize is true", () => { + const checks = [1, 2, 3]; + normalizeStub.returns([10, 20, 30]); + + const result = processChecksForDisplay(normalizeStub, checks, null, true); + + expect(normalizeStub.args[0]).to.deep.equal([checks, 1, 100]); + expect(result).to.deep.equal([10, 20, 30]); + }); + + it("should handle both filtering and normalization", () => { + const checks = [1, 2, 3, 4, 5, 6]; + normalizeStub.returns([10, 30, 50]); + + const result = processChecksForDisplay(normalizeStub, checks, 3, true); + + expect(normalizeStub.args[0][0]).to.deep.equal([1, 3, 5]); + expect(result).to.deep.equal([10, 30, 50]); + }); + }); + describe("groupChecksByTime", () => { + const mockChecks = [ + { createdAt: "2024-01-15T10:30:45Z" }, + { createdAt: "2024-01-15T10:45:15Z" }, + { createdAt: "2024-01-15T11:15:00Z" }, + { createdAt: "2024-01-16T10:30:00Z" }, + ]; + + it("should group checks by hour when dateRange is 'day'", () => { + const result = groupChecksByTime(mockChecks, "day"); + + // Get timestamps for 10:00 and 11:00 on Jan 15 + const time1 = new Date("2024-01-15T10:00:00Z").getTime(); + const time2 = new Date("2024-01-15T11:00:00Z").getTime(); + const time3 = new Date("2024-01-16T10:00:00Z").getTime(); + + expect(Object.keys(result)).to.have.lengthOf(3); + + expect(result[time1].checks).to.have.lengthOf(2); + expect(result[time2].checks).to.have.lengthOf(1); + expect(result[time3].checks).to.have.lengthOf(1); + }); + + it("should group checks by day when dateRange is not 'day'", () => { + const result = groupChecksByTime(mockChecks, "week"); + + expect(Object.keys(result)).to.have.lengthOf(2); + expect(result["2024-01-15"].checks).to.have.lengthOf(3); + expect(result["2024-01-16"].checks).to.have.lengthOf(1); + }); + + it("should handle empty checks array", () => { + const result = groupChecksByTime([], "day"); + expect(result).to.deep.equal({}); + }); + + it("should handle single check", () => { + const singleCheck = [{ createdAt: "2024-01-15T10:30:45Z" }]; + const result = groupChecksByTime(singleCheck, "day"); + + const expectedTime = new Date("2024-01-15T10:00:00Z").getTime(); + expect(Object.keys(result)).to.have.lengthOf(1); + expect(result[expectedTime].checks).to.have.lengthOf(1); + }); + it("should skip invalid dates and process valid ones", () => { + const checksWithInvalidDate = [ + { createdAt: "invalid-date" }, + { createdAt: "2024-01-15T10:30:45Z" }, + { createdAt: null }, + { createdAt: undefined }, + { createdAt: "" }, + ]; + + const result = groupChecksByTime(checksWithInvalidDate, "day"); + + const expectedTime = new Date("2024-01-15T10:00:00Z").getTime(); + expect(Object.keys(result)).to.have.lengthOf(1); + expect(result[expectedTime].checks).to.have.lengthOf(1); + expect(result[expectedTime].checks[0].createdAt).to.equal("2024-01-15T10:30:45Z"); + }); + it("should handle checks in same time group", () => { + const checksInSameHour = [ + { createdAt: "2024-01-15T10:15:00Z" }, + { createdAt: "2024-01-15T10:45:00Z" }, + ]; + + const result = groupChecksByTime(checksInSameHour, "day"); + + const expectedTime = new Date("2024-01-15T10:00:00Z").getTime(); + expect(Object.keys(result)).to.have.lengthOf(1); + expect(result[expectedTime].checks).to.have.lengthOf(2); + }); + }); + describe("calculateGroupStats", () => { + // Mock getUptimePercentage function + let uptimePercentageStub; + + beforeEach(() => { + uptimePercentageStub = sinon.stub(); + uptimePercentageStub.returns(95); // Default return value + }); + + it("should calculate stats correctly for a group of checks", () => { + const mockGroup = { + time: "2024-01-15", + checks: [ + { status: true, responseTime: 100 }, + { status: false, responseTime: 200 }, + { status: true, responseTime: 300 }, + ], + }; + + const result = calculateGroupStats(mockGroup, uptimePercentageStub); + + expect(result).to.deep.equal({ + time: "2024-01-15", + uptimePercentage: (2 / 3) * 100, + totalChecks: 3, + totalIncidents: 1, + avgResponseTime: 200, // (100 + 200 + 300) / 3 + }); + }); + + it("should handle empty checks array", () => { + const mockGroup = { + time: "2024-01-15", + checks: [], + }; + + const result = calculateGroupStats(mockGroup, uptimePercentageStub); + + expect(result).to.deep.equal({ + time: "2024-01-15", + uptimePercentage: 0, + totalChecks: 0, + totalIncidents: 0, + avgResponseTime: 0, + }); + }); + + it("should handle missing responseTime values", () => { + const mockGroup = { + time: "2024-01-15", + checks: [ + { status: true }, + { status: false, responseTime: 200 }, + { status: true, responseTime: undefined }, + ], + }; + + const result = calculateGroupStats(mockGroup, uptimePercentageStub); + + expect(result).to.deep.equal({ + time: "2024-01-15", + uptimePercentage: (2 / 3) * 100, + totalChecks: 3, + totalIncidents: 1, + avgResponseTime: 200, // 200 / 1 + }); + }); + + it("should handle all checks with status false", () => { + const mockGroup = { + time: "2024-01-15", + checks: [ + { status: false, responseTime: 100 }, + { status: false, responseTime: 200 }, + { status: false, responseTime: 300 }, + ], + }; + + const result = calculateGroupStats(mockGroup, uptimePercentageStub); + + expect(result).to.deep.equal({ + time: "2024-01-15", + uptimePercentage: 0, + totalChecks: 3, + totalIncidents: 3, + avgResponseTime: 200, + }); + }); + + it("should handle all checks with status true", () => { + const mockGroup = { + time: "2024-01-15", + checks: [ + { status: true, responseTime: 100 }, + { status: true, responseTime: 200 }, + { status: true, responseTime: 300 }, + ], + }; + + const result = calculateGroupStats(mockGroup, uptimePercentageStub); + + expect(result).to.deep.equal({ + time: "2024-01-15", + uptimePercentage: 100, + totalChecks: 3, + totalIncidents: 0, + avgResponseTime: 200, + }); + }); + }); + + describe("getMonitorStatsById", () => { + const now = new Date(); + const oneHourAgo = new Date(now - 3600000); + const twoHoursAgo = new Date(now - 7200000); + + const mockMonitor = { + _id: "monitor123", + type: "http", + name: "Test Monitor", + url: "https://test.com", + toObject: () => ({ + _id: "monitor123", + type: "http", + name: "Test Monitor", + url: "https://test.com", + }), + }; + + const checkDocs = [ + { + monitorId: "monitor123", + status: true, + responseTime: 100, + createdAt: new Date("2024-01-01T12:00:00Z"), + toObject: function () { + return { + monitorId: this.monitorId, + status: this.status, + responseTime: this.responseTime, + createdAt: this.createdAt, + }; + }, + }, + { + monitorId: "monitor123", + status: true, + responseTime: 150, + createdAt: new Date("2024-01-01T11:00:00Z"), + toObject: function () { + return { + monitorId: this.monitorId, + status: this.status, + responseTime: this.responseTime, + createdAt: this.createdAt, + }; + }, + }, + { + monitorId: "monitor123", + status: false, + responseTime: 200, + createdAt: new Date("2024-01-01T10:00:00Z"), + toObject: function () { + return { + monitorId: this.monitorId, + status: this.status, + responseTime: this.responseTime, + createdAt: this.createdAt, + }; + }, + }, + ]; + const req = { + params: { monitorId: "monitor123" }, + query: { + dateRange: "day", + sortOrder: "desc", + numToDisplay: 10, + normalize: true, + }, + }; + + beforeEach(() => { + checkFindStub.returns({ + sort: () => checkDocs, + }); + monitorFindByIdStub.returns(mockMonitor); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return monitor stats with calculated values", async () => { + const result = await getMonitorStatsById(req); + expect(result).to.include.keys([ + "_id", + "type", + "name", + "url", + "uptimeDuration", + "lastChecked", + "latestResponseTime", + "periodIncidents", + "periodTotalChecks", + "periodAvgResponseTime", + "periodUptime", + "aggregateData", + ]); + expect(result.latestResponseTime).to.equal(100); + expect(result.periodTotalChecks).to.equal(3); + expect(result.periodIncidents).to.equal(1); + expect(result.periodUptime).to.be.a("number"); + expect(result.aggregateData).to.be.an("array"); + }); + it("should throw error when monitor is not found", async () => { + monitorFindByIdStub.returns(Promise.resolve(null)); + + const req = { + params: { monitorId: "nonexistent" }, + query: {}, + }; + + try { + await getMonitorStatsById(req); + expect.fail("Should have thrown an error"); + } catch (error) { + expect(error).to.be.an("Error"); + expect(error.service).to.equal("monitorModule"); + expect(error.method).to.equal("getMonitorStatsById"); + } + }); + }); + + describe("getMonitorById", () => { + let notificationFindStub; + let monitorSaveStub; + + beforeEach(() => { + // Create stubs + notificationFindStub = sinon.stub(Notification, "find"); + monitorSaveStub = sinon.stub().resolves(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return monitor with notifications when found", async () => { + // Arrange + const monitorId = "123"; + const mockMonitor = { + _id: monitorId, + name: "Test Monitor", + save: monitorSaveStub, + }; + const mockNotifications = [ + { _id: "notif1", message: "Test notification 1" }, + { _id: "notif2", message: "Test notification 2" }, + ]; + + monitorFindByIdStub.resolves(mockMonitor); + notificationFindStub.resolves(mockNotifications); + + const result = await getMonitorById(monitorId); + expect(result._id).to.equal(monitorId); + expect(result.name).to.equal("Test Monitor"); + expect(monitorFindByIdStub.calledWith(monitorId)).to.be.true; + expect(notificationFindStub.calledWith({ monitorId })).to.be.true; + expect(monitorSaveStub.calledOnce).to.be.true; + }); + + it("should throw 404 error when monitor not found", async () => { + // Arrange + const monitorId = "nonexistent"; + monitorFindByIdStub.resolves(null); + + // Act & Assert + try { + await getMonitorById(monitorId); + expect.fail("Should have thrown an error"); + } catch (error) { + expect(error.message).to.equal(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)); + expect(error.status).to.equal(404); + expect(error.service).to.equal("monitorModule"); + expect(error.method).to.equal("getMonitorById"); + } + }); + + it("should handle database errors properly", async () => { + // Arrange + const monitorId = "123"; + const dbError = new Error("Database connection failed"); + monitorFindByIdStub.rejects(dbError); + + // Act & Assert + try { + await getMonitorById(monitorId); + expect.fail("Should have thrown an error"); + } catch (error) { + expect(error.service).to.equal("monitorModule"); + expect(error.method).to.equal("getMonitorById"); + expect(error.message).to.equal("Database connection failed"); + } + }); + + it("should handle notification fetch errors", async () => { + // Arrange + const monitorId = "123"; + const mockMonitor = { + _id: monitorId, + name: "Test Monitor", + save: monitorSaveStub, + }; + const notificationError = new Error("Notification fetch failed"); + + monitorFindByIdStub.resolves(mockMonitor); + notificationFindStub.rejects(notificationError); + + // Act & Assert + try { + await getMonitorById(monitorId); + expect.fail("Should have thrown an error"); + } catch (error) { + expect(error.service).to.equal("monitorModule"); + expect(error.method).to.equal("getMonitorById"); + expect(error.message).to.equal("Notification fetch failed"); + } + }); + + it("should handle monitor save errors", async () => { + // Arrange + const monitorId = "123"; + const mockMonitor = { + _id: monitorId, + name: "Test Monitor", + save: sinon.stub().rejects(new Error("Save failed")), + }; + const mockNotifications = []; + + monitorFindByIdStub.resolves(mockMonitor); + notificationFindStub.resolves(mockNotifications); + + // Act & Assert + try { + await getMonitorById(monitorId); + expect.fail("Should have thrown an error"); + } catch (error) { + expect(error.service).to.equal("monitorModule"); + expect(error.method).to.equal("getMonitorById"); + expect(error.message).to.equal("Save failed"); + } + }); + }); + describe("getMonitorsAndSummaryByTeamId", () => { + it("should return monitors and correct summary counts", async () => { + // Arrange + const teamId = "team123"; + const type = "http"; + const mockMonitors = [ + { teamId, type, status: true, isActive: true }, // up + { teamId, type, status: false, isActive: true }, // down + { teamId, type, status: null, isActive: false }, // paused + { teamId, type, status: true, isActive: true }, // up + ]; + monitorFindStub.resolves(mockMonitors); + + // Act + const result = await getMonitorsAndSummaryByTeamId(teamId, type); + + // Assert + expect(result.monitors).to.have.lengthOf(4); + expect(result.monitorCounts).to.deep.equal({ + up: 2, + down: 1, + paused: 1, + total: 4, + }); + expect(monitorFindStub.calledOnceWith({ teamId, type })).to.be.true; + }); + + it("should return empty results for non-existent team", async () => { + // Arrange + monitorFindStub.resolves([]); + + // Act + const result = await getMonitorsAndSummaryByTeamId("nonexistent", "http"); + + // Assert + expect(result.monitors).to.have.lengthOf(0); + expect(result.monitorCounts).to.deep.equal({ + up: 0, + down: 0, + paused: 0, + total: 0, + }); + }); + + it("should handle database errors", async () => { + // Arrange + const error = new Error("Database error"); + error.service = "MonitorModule"; + error.method = "getMonitorsAndSummaryByTeamId"; + monitorFindStub.rejects(error); + + // Act & Assert + try { + await getMonitorsAndSummaryByTeamId("team123", "http"); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err).to.equal(error); + expect(err.service).to.equal("monitorModule"); + expect(err.method).to.equal("getMonitorsAndSummaryByTeamId"); + } + }); + }); + describe("getMonitorsByTeamId", () => { + beforeEach(() => { + // Chain stubs for Monitor.find().skip().limit().sort() + + // Stub for CHECK_MODEL_LOOKUP model find + checkFindStub.returns({ + sort: sinon.stub().returnsThis(), + limit: sinon.stub().returnsThis(), + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return monitors with basic query parameters", async () => { + const mockMonitors = [ + { _id: "1", type: "http", toObject: () => ({ _id: "1", type: "http" }) }, + { _id: "2", type: "ping", toObject: () => ({ _id: "2", type: "ping" }) }, + ]; + monitorFindStub.returns({ + skip: sinon.stub().returns({ + limit: sinon.stub().returns({ + sort: sinon.stub().returns(mockMonitors), + }), + }), + }); + + const req = { + params: { teamId: "team123" }, + query: { + page: 0, + rowsPerPage: 10, + }, + }; + + monitorCountStub.resolves(2); + + const result = await getMonitorsByTeamId(req); + + expect(result).to.have.property("monitors"); + expect(result).to.have.property("monitorCount", 2); + }); + + it("should handle type filter with array input", async () => { + const req = { + params: { teamId: "team123" }, + query: { + type: ["http", "ping"], + }, + }; + + monitorFindStub.returns({ + skip: sinon.stub().returns({ + limit: sinon.stub().returns({ + sort: sinon.stub().returns([]), + }), + }), + }); + monitorCountStub.resolves(0); + + await getMonitorsByTeamId(req); + + expect(Monitor.find.firstCall.args[0]).to.deep.equal({ + teamId: "team123", + type: { $in: ["http", "ping"] }, + }); + }); + + it("should handle text search filter", async () => { + const req = { + params: { teamId: "team123" }, + query: { + filter: "search", + }, + }; + + monitorFindStub.returns({ + skip: sinon.stub().returns({ + limit: sinon.stub().returns({ + sort: sinon.stub().returns([]), + }), + }), + }); + monitorCountStub.resolves(0); + + await getMonitorsByTeamId(req); + + expect(Monitor.find.firstCall.args[0]).to.deep.equal({ + teamId: "team123", + $or: [ + { name: { $regex: "search", $options: "i" } }, + { url: { $regex: "search", $options: "i" } }, + ], + }); + }); + + it("should handle pagination parameters", async () => { + const req = { + params: { teamId: "team123" }, + query: { + page: 2, + rowsPerPage: 5, + }, + }; + + monitorFindStub.returns({ + skip: sinon.stub().returns({ + limit: sinon.stub().returns({ + sort: sinon.stub().returns([]), + }), + }), + }); + monitorCountStub.resolves(0); + + const result = await getMonitorsByTeamId(req); + expect(result).to.deep.equal({ + monitors: [], + monitorCount: 0, + }); + }); + + it("should handle sorting parameters", async () => { + const req = { + params: { teamId: "team123" }, + query: { + field: "name", + order: "asc", + }, + }; + + monitorFindStub.returns({ + skip: sinon.stub().returns({ + limit: sinon.stub().returns({ + sort: sinon.stub().returns([]), + }), + }), + }); + monitorCountStub.resolves(0); + + await getMonitorsByTeamId(req); + + const result = await getMonitorsByTeamId(req); + expect(result).to.deep.equal({ + monitors: [], + monitorCount: 0, + }); + }); + + it("should return early when limit is -1", async () => { + // Arrange + const req = { + params: { teamId: "team123" }, + query: { + limit: "-1", + }, + }; + + const mockMonitors = [ + { _id: "1", type: "http" }, + { _id: "2", type: "ping" }, + ]; + + monitorFindStub.returns({ + skip: sinon.stub().returns({ + limit: sinon.stub().returns({ + sort: sinon.stub().returns(mockMonitors), + }), + }), + }); + + monitorCountStub.resolves(2); + + // Act + const result = await getMonitorsByTeamId(req); + + // Assert + expect(result).to.deep.equal({ + monitors: mockMonitors, + monitorCount: 2, + }); + }); + it("should normalize checks when normalize parameter is provided", async () => { + const req = { + params: { teamId: "team123" }, + query: { normalize: "true" }, + }; + + const mockMonitors = [ + { _id: "1", type: "http" }, + { _id: "2", type: "ping" }, + ]; + + monitorFindStub.returns({ + skip: sinon.stub().returns({ + limit: sinon.stub().returns({ + sort: sinon.stub().returns(mockMonitors), + }), + }), + }); + + const result = await getMonitorsByTeamId(req); + + expect(NormalizeDataStub.calledOnce).to.be.true; + expect(NormalizeDataStub.firstCall.args[0]).to.deep.equal([{ responseTime: 100 }]); + expect(NormalizeDataStub.firstCall.args[1]).to.equal(10); + expect(NormalizeDataStub.firstCall.args[2]).to.equal(100); + }); + it("should handle database errors", async () => { + // Arrange + const req = { + params: { teamId: "team123" }, + query: {}, + }; + + const error = new Error("Database error"); + monitorFindStub.returns({ + skip: sinon.stub().returns({ + limit: sinon.stub().returns({ + sort: sinon.stub().throws(error), + }), + }), + }); + + // Act & Assert + try { + await getMonitorsByTeamId(req); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err.service).to.equal("monitorModule"); + expect(err.method).to.equal("getMonitorsByTeamId"); + expect(err.message).to.equal("Database error"); + } + }); + }); + describe("createMonitor", () => { + it("should create a monitor without notifications", async () => { + let monitorSaveStub = sinon.stub(Monitor.prototype, "save").resolves(); + + const req = { + body: { + name: "Test Monitor", + url: "http://test.com", + type: "http", + notifications: ["someNotification"], + }, + }; + + const expectedMonitor = { + name: "Test Monitor", + url: "http://test.com", + type: "http", + notifications: undefined, + save: monitorSaveStub, + }; + + const result = await createMonitor(req); + expect(result.name).to.equal(expectedMonitor.name); + expect(result.url).to.equal(expectedMonitor.url); + }); + it("should handle database errors", async () => { + const req = { + body: { + name: "Test Monitor", + }, + }; + + try { + await createMonitor(req); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err.service).to.equal("monitorModule"); + expect(err.method).to.equal("createMonitor"); + } + }); + }); + + describe("deleteMonitor", () => { + it("should delete a monitor successfully", async () => { + const monitorId = "123456789"; + const mockMonitor = { + _id: monitorId, + name: "Test Monitor", + url: "http://test.com", + }; + + const req = { + params: { monitorId }, + }; + + monitorFindByIdAndDeleteStub.resolves(mockMonitor); + + const result = await deleteMonitor(req); + + expect(result).to.deep.equal(mockMonitor); + sinon.assert.calledWith(monitorFindByIdAndDeleteStub, monitorId); + }); + + it("should throw error when monitor not found", async () => { + const monitorId = "nonexistent123"; + const req = { + params: { monitorId }, + }; + + monitorFindByIdAndDeleteStub.resolves(null); + + try { + await deleteMonitor(req); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err.message).to.equal(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)); + expect(err.service).to.equal("monitorModule"); + expect(err.method).to.equal("deleteMonitor"); + } + }); + + it("should handle database errors", async () => { + const monitorId = "123456789"; + const req = { + params: { monitorId }, + }; + + const dbError = new Error("Database connection error"); + monitorFindByIdAndDeleteStub.rejects(dbError); + + try { + await deleteMonitor(req); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err.message).to.equal("Database connection error"); + expect(err.service).to.equal("monitorModule"); + expect(err.method).to.equal("deleteMonitor"); + } + }); + }); + + describe("deleteAllMonitors", () => { + it("should delete all monitors for a team successfully", async () => { + const teamId = "team123"; + const mockMonitors = [ + { _id: "1", name: "Monitor 1", teamId }, + { _id: "2", name: "Monitor 2", teamId }, + ]; + + monitorFindStub.resolves(mockMonitors); + monitorDeleteManyStub.resolves({ deletedCount: 2 }); + + const result = await deleteAllMonitors(teamId); + + expect(result).to.deep.equal({ + monitors: mockMonitors, + deletedCount: 2, + }); + sinon.assert.calledWith(monitorFindStub, { teamId }); + sinon.assert.calledWith(monitorDeleteManyStub, { teamId }); + }); + + it("should return empty array when no monitors found", async () => { + const teamId = "emptyTeam"; + + monitorFindStub.resolves([]); + monitorDeleteManyStub.resolves({ deletedCount: 0 }); + + const result = await deleteAllMonitors(teamId); + + expect(result).to.deep.equal({ + monitors: [], + deletedCount: 0, + }); + sinon.assert.calledWith(monitorFindStub, { teamId }); + sinon.assert.calledWith(monitorDeleteManyStub, { teamId }); + }); + + it("should handle database errors", async () => { + const teamId = "team123"; + const dbError = new Error("Database connection error"); + monitorFindStub.rejects(dbError); + + try { + await deleteAllMonitors(teamId); + } catch (err) { + expect(err.message).to.equal("Database connection error"); + expect(err.service).to.equal("monitorModule"); + expect(err.method).to.equal("deleteAllMonitors"); + } + }); + + it("should handle deleteMany errors", async () => { + const teamId = "team123"; + monitorFindStub.resolves([{ _id: "1", name: "Monitor 1" }]); + monitorDeleteManyStub.rejects(new Error("Delete operation failed")); + + try { + await deleteAllMonitors(teamId); + } catch (err) { + expect(err.message).to.equal("Delete operation failed"); + expect(err.service).to.equal("monitorModule"); + expect(err.method).to.equal("deleteAllMonitors"); + } + }); + }); + + describe("deleteMonitorsByUserId", () => { + beforeEach(() => {}); + + afterEach(() => { + sinon.restore(); + }); + + it("should delete all monitors for a user successfully", async () => { + // Arrange + const userId = "user123"; + const mockResult = { + deletedCount: 3, + acknowledged: true, + }; + + monitorDeleteManyStub.resolves(mockResult); + + // Act + const result = await deleteMonitorsByUserId(userId); + + // Assert + expect(result).to.deep.equal(mockResult); + sinon.assert.calledWith(monitorDeleteManyStub, { userId: userId }); + }); + + it("should return zero deletedCount when no monitors found", async () => { + // Arrange + const userId = "nonexistentUser"; + const mockResult = { + deletedCount: 0, + acknowledged: true, + }; + + monitorDeleteManyStub.resolves(mockResult); + + // Act + const result = await deleteMonitorsByUserId(userId); + + // Assert + expect(result.deletedCount).to.equal(0); + sinon.assert.calledWith(monitorDeleteManyStub, { userId: userId }); + }); + + it("should handle database errors", async () => { + // Arrange + const userId = "user123"; + const dbError = new Error("Database connection error"); + monitorDeleteManyStub.rejects(dbError); + + // Act & Assert + try { + await deleteMonitorsByUserId(userId); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err.message).to.equal("Database connection error"); + expect(err.service).to.equal("monitorModule"); + expect(err.method).to.equal("deleteMonitorsByUserId"); + } + }); + }); + + describe("editMonitor", () => { + it("should edit a monitor successfully", async () => { + // Arrange + const candidateId = "monitor123"; + const candidateMonitor = { + name: "Updated Monitor", + url: "http://updated.com", + type: "http", + notifications: ["someNotification"], + }; + + const expectedUpdateData = { + name: "Updated Monitor", + url: "http://updated.com", + type: "http", + notifications: undefined, + }; + + const mockUpdatedMonitor = { + _id: candidateId, + ...expectedUpdateData, + }; + + monitorFindByIdAndUpdateStub.resolves(mockUpdatedMonitor); + + // Act + const result = await editMonitor(candidateId, candidateMonitor); + + // Assert + expect(result).to.deep.equal(mockUpdatedMonitor); + sinon.assert.calledWith( + monitorFindByIdAndUpdateStub, + candidateId, + expectedUpdateData, + { + new: true, + } + ); + }); + + it("should return null when monitor not found", async () => { + // Arrange + const candidateId = "nonexistent123"; + const candidateMonitor = { + name: "Updated Monitor", + }; + + monitorFindByIdAndUpdateStub.resolves(null); + + // Act + const result = await editMonitor(candidateId, candidateMonitor); + + // Assert + expect(result).to.be.null; + sinon.assert.calledWith( + monitorFindByIdAndUpdateStub, + candidateId, + { name: "Updated Monitor", notifications: undefined }, + { new: true } + ); + }); + + it("should remove notifications from update data", async () => { + // Arrange + const candidateId = "monitor123"; + const candidateMonitor = { + name: "Updated Monitor", + notifications: ["notification1", "notification2"], + }; + + const expectedUpdateData = { + name: "Updated Monitor", + notifications: undefined, + }; + + monitorFindByIdAndUpdateStub.resolves({ + _id: candidateId, + ...expectedUpdateData, + }); + + // Act + await editMonitor(candidateId, candidateMonitor); + + // Assert + sinon.assert.calledWith( + monitorFindByIdAndUpdateStub, + candidateId, + expectedUpdateData, + { + new: true, + } + ); + }); + + it("should handle database errors", async () => { + // Arrange + const candidateId = "monitor123"; + const candidateMonitor = { + name: "Updated Monitor", + }; + + const dbError = new Error("Database connection error"); + monitorFindByIdAndUpdateStub.rejects(dbError); + + // Act & Assert + try { + await editMonitor(candidateId, candidateMonitor); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err.message).to.equal("Database connection error"); + expect(err.service).to.equal("monitorModule"); + expect(err.method).to.equal("editMonitor"); + } + }); + }); + + describe("addDemoMonitors", () => { + it("should add demo monitors successfully", async () => { + // Arrange + const userId = "user123"; + const teamId = "team123"; + monitorInsertManyStub.resolves([{ _id: "123" }]); + const result = await addDemoMonitors(userId, teamId); + expect(result).to.deep.equal([{ _id: "123" }]); + }); + + it("should handle database errors", async () => { + const userId = "user123"; + const teamId = "team123"; + + const dbError = new Error("Database connection error"); + monitorInsertManyStub.rejects(dbError); + + try { + await addDemoMonitors(userId, teamId); + } catch (err) { + expect(err.message).to.equal("Database connection error"); + expect(err.service).to.equal("monitorModule"); + expect(err.method).to.equal("addDemoMonitors"); + } + }); + }); +}); From abdd51802a75b84c28fd79e4b5dbabfded5a7409 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 6 Nov 2024 10:38:04 +0800 Subject: [PATCH 16/34] remove redundant checks and console.log --- Server/db/mongo/modules/monitorModule.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index d12394305..2a692596e 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -406,7 +406,7 @@ const getMonitorsByTeamId = async (req, res) => { filter, field, order, - } = req.query || {}; + } = req.query; const monitorQuery = { teamId: req.params.teamId }; @@ -441,10 +441,6 @@ const getMonitorsByTeamId = async (req, res) => { // Map each monitor to include its associated checks const monitorsWithChecks = await Promise.all( monitors.map(async (monitor) => { - if (status !== undefined) { - checksQuery.status = status; - } - let model = CHECK_MODEL_LOOKUP[monitor.type]; // Checks are order newest -> oldest @@ -486,7 +482,6 @@ const createMonitor = async (req, res) => { // Remove notifications fom monitor as they aren't needed here monitor.notifications = undefined; await monitor.save(); - console.log(monitor); return monitor; } catch (error) { error.service = SERVICE_NAME; From ba37fb3871d6ccb1949ddc07090614b1985710d5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 6 Nov 2024 10:38:16 +0800 Subject: [PATCH 17/34] implement remaining tests --- Server/tests/db/monitorModule.test.js | 119 +++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 12 deletions(-) diff --git a/Server/tests/db/monitorModule.test.js b/Server/tests/db/monitorModule.test.js index a9092074a..854c37132 100644 --- a/Server/tests/db/monitorModule.test.js +++ b/Server/tests/db/monitorModule.test.js @@ -685,6 +685,19 @@ describe("monitorModule", () => { }), }; + const mockMonitorPing = { + _id: "monitor123", + type: "ping", + name: "Test Monitor", + url: "https://test.com", + toObject: () => ({ + _id: "monitor123", + type: "http", + name: "Test Monitor", + url: "https://test.com", + }), + }; + const checkDocs = [ { monitorId: "monitor123", @@ -750,7 +763,55 @@ describe("monitorModule", () => { sinon.restore(); }); + it("should return monitor stats with calculated values, sort order desc", async () => { + req.query.sortOrder = "desc"; + const result = await getMonitorStatsById(req); + expect(result).to.include.keys([ + "_id", + "type", + "name", + "url", + "uptimeDuration", + "lastChecked", + "latestResponseTime", + "periodIncidents", + "periodTotalChecks", + "periodAvgResponseTime", + "periodUptime", + "aggregateData", + ]); + expect(result.latestResponseTime).to.equal(100); + expect(result.periodTotalChecks).to.equal(3); + expect(result.periodIncidents).to.equal(1); + expect(result.periodUptime).to.be.a("number"); + expect(result.aggregateData).to.be.an("array"); + }); + it("should return monitor stats with calculated values, ping type", async () => { + monitorFindByIdStub.returns(mockMonitorPing); + req.query.sortOrder = "desc"; + const result = await getMonitorStatsById(req); + expect(result).to.include.keys([ + "_id", + "type", + "name", + "url", + "uptimeDuration", + "lastChecked", + "latestResponseTime", + "periodIncidents", + "periodTotalChecks", + "periodAvgResponseTime", + "periodUptime", + "aggregateData", + ]); + expect(result.latestResponseTime).to.equal(100); + expect(result.periodTotalChecks).to.equal(3); + expect(result.periodIncidents).to.equal(1); + expect(result.periodUptime).to.be.a("number"); + expect(result.aggregateData).to.be.an("array"); + }); it("should return monitor stats with calculated values", async () => { + req.query.sortOrder = "asc"; const result = await getMonitorStatsById(req); expect(result).to.include.keys([ "_id", @@ -777,7 +838,6 @@ describe("monitorModule", () => { const req = { params: { monitorId: "nonexistent" }, - query: {}, }; try { @@ -979,8 +1039,9 @@ describe("monitorModule", () => { // Stub for CHECK_MODEL_LOOKUP model find checkFindStub.returns({ - sort: sinon.stub().returnsThis(), - limit: sinon.stub().returnsThis(), + sort: sinon.stub().returns({ + limit: sinon.stub().returns([]), + }), }); }); @@ -1004,8 +1065,45 @@ describe("monitorModule", () => { const req = { params: { teamId: "team123" }, query: { + type: "http", page: 0, rowsPerPage: 10, + field: "name", + status: false, + checkOrder: "desc", + }, + }; + + monitorCountStub.resolves(2); + + const result = await getMonitorsByTeamId(req); + + expect(result).to.have.property("monitors"); + expect(result).to.have.property("monitorCount", 2); + }); + + it("should return monitors with basic query parameters", async () => { + const mockMonitors = [ + { _id: "1", type: "http", toObject: () => ({ _id: "1", type: "http" }) }, + { _id: "2", type: "ping", toObject: () => ({ _id: "2", type: "ping" }) }, + ]; + monitorFindStub.returns({ + skip: sinon.stub().returns({ + limit: sinon.stub().returns({ + sort: sinon.stub().returns(mockMonitors), + }), + }), + }); + + const req = { + params: { teamId: "team123" }, + query: { + type: "http", + page: 0, + rowsPerPage: 10, + field: "name", + status: true, + checkOrder: "asc", }, }; @@ -1155,15 +1253,17 @@ describe("monitorModule", () => { monitorCount: 2, }); }); + it("should normalize checks when normalize parameter is provided", async () => { const req = { params: { teamId: "team123" }, query: { normalize: "true" }, }; + monitorCountStub.resolves(2); const mockMonitors = [ - { _id: "1", type: "http" }, - { _id: "2", type: "ping" }, + { _id: "1", type: "http", toObject: () => ({ _id: "1", type: "http" }) }, + { _id: "2", type: "ping", toObject: () => ({ _id: "2", type: "ping" }) }, ]; monitorFindStub.returns({ @@ -1175,14 +1275,10 @@ describe("monitorModule", () => { }); const result = await getMonitorsByTeamId(req); - - expect(NormalizeDataStub.calledOnce).to.be.true; - expect(NormalizeDataStub.firstCall.args[0]).to.deep.equal([{ responseTime: 100 }]); - expect(NormalizeDataStub.firstCall.args[1]).to.equal(10); - expect(NormalizeDataStub.firstCall.args[2]).to.equal(100); + expect(result.monitorCount).to.equal(2); + expect(result.monitors).to.have.lengthOf(2); }); it("should handle database errors", async () => { - // Arrange const req = { params: { teamId: "team123" }, query: {}, @@ -1197,7 +1293,6 @@ describe("monitorModule", () => { }), }); - // Act & Assert try { await getMonitorsByTeamId(req); expect.fail("Should have thrown an error"); From f613c5f19a14fbd01c2ae2e93059e38a50496d4c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 6 Nov 2024 11:16:45 +0800 Subject: [PATCH 18/34] 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 19/34] 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 20/34] 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 21/34] 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 22/34] 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 23/34] 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 24/34] 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 91bacf976ed2e126e27e4c7740e1ba1dfa8e23b4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 7 Nov 2024 11:05:48 +0800 Subject: [PATCH 25/34] isNaN -> Number.isNaN --- Server/db/mongo/modules/monitorModule.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 2a692596e..d8762a94e 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -212,7 +212,7 @@ const groupChecksByTime = (checks, dateRange) => { return checks.reduce((acc, check) => { // Validate the date const checkDate = new Date(check.createdAt); - if (isNaN(checkDate.getTime()) || checkDate.getTime() === 0) { + if (Number.isNaN(checkDate.getTime()) || checkDate.getTime() === 0) { return acc; } @@ -238,7 +238,7 @@ const calculateGroupStats = (group) => { const totalChecks = group.checks.length; const checksWithResponseTime = group.checks.filter( - (check) => typeof check.responseTime === "number" && !isNaN(check.responseTime) + (check) => typeof check.responseTime === "number" && !Number.isNaN(check.responseTime) ); return { From 8e7619dd7832b7a88faa4c2281764e5ef7925f81 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 7 Nov 2024 12:04:25 +0800 Subject: [PATCH 26/34] 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 27/34] 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 28/34] 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); + } + }); + }); +}); From 5129aca6c2881c8ad5609517c9abc6268d014fa5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 8 Nov 2024 09:46:30 +0800 Subject: [PATCH 29/34] Fix typo in monitor type lookup --- Server/db/mongo/modules/monitorModule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 391e02146..d4f540f2c 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -20,7 +20,7 @@ const SERVICE_NAME = "monitorModule"; const CHECK_MODEL_LOOKUP = { http: Check, ping: Check, - pageSpeed: PageSpeedCheck, + pagespeed: PageSpeedCheck, hardware: HardwareCheck, }; From 3de8706a478e86910ed143ce65b4b5c358b7be2f Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 8 Nov 2024 12:45:44 +0000 Subject: [PATCH 30/34] fix(deps): update dependency bullmq to v5.25.3 --- Server/package-lock.json | 8 ++++---- Server/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index 00c38c029..fd34bf380 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -12,7 +12,7 @@ "@sendgrid/mail": "^8.1.3", "axios": "^1.7.2", "bcrypt": "^5.1.1", - "bullmq": "5.25.0", + "bullmq": "5.25.3", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", @@ -1164,9 +1164,9 @@ "license": "MIT" }, "node_modules/bullmq": { - "version": "5.25.0", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.25.0.tgz", - "integrity": "sha512-QBbtabDUgdztalbYbrCc5NFwrRUKOZyiAkVFPhdrBGFsdraPq5SrXx6WP7U1stKj3hYYp1IuW+n3wuksYGITvw==", + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.25.3.tgz", + "integrity": "sha512-nUFTszxV/V3qJMZQxSMNOBF1HiGKh895WyJmE5keUonkutpTsxdYIr0dzVUTPbhXvBvW9LWlY7BetWY3afy/MQ==", "license": "MIT", "dependencies": { "cron-parser": "^4.6.0", diff --git a/Server/package.json b/Server/package.json index 27e6acd4e..d01167708 100644 --- a/Server/package.json +++ b/Server/package.json @@ -15,7 +15,7 @@ "@sendgrid/mail": "^8.1.3", "axios": "^1.7.2", "bcrypt": "^5.1.1", - "bullmq": "5.25.0", + "bullmq": "5.25.3", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", From 1fd1820563046dc611215f7394e6e7490bdc6b6f Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 8 Nov 2024 12:45:58 +0000 Subject: [PATCH 31/34] fix(deps): update material-ui monorepo to v7.22.2 --- Client/package-lock.json | 24 ++++++++++++------------ Client/package.json | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Client/package-lock.json b/Client/package-lock.json index 145d2783b..83ffa6cfa 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -15,8 +15,8 @@ "@mui/lab": "^5.0.0-alpha.170", "@mui/material": "^5.15.16", "@mui/x-charts": "^7.5.1", - "@mui/x-data-grid": "7.22.1", - "@mui/x-date-pickers": "7.22.1", + "@mui/x-data-grid": "7.22.2", + "@mui/x-date-pickers": "7.22.2", "@reduxjs/toolkit": "2.3.0", "axios": "^1.7.4", "chart.js": "^4.4.3", @@ -1400,9 +1400,9 @@ } }, "node_modules/@mui/x-charts": { - "version": "7.22.1", - "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.22.1.tgz", - "integrity": "sha512-zgr8CN4yLen5puqaX7Haj5+AoVG7E13HHsIiDoEAuQvuFDF0gKTxTTdLSKXqhd1qJUIIzJaztZtrr3YCVrENqw==", + "version": "7.22.2", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.22.2.tgz", + "integrity": "sha512-0Y2du4Ed7gOT53l8vVJ4vKT+Jz4Dh/iHnLy8TtL3+XhbPH9Ndu9Q30WwyyzOn84yt37hSUru/njQ1BWaSvVPHw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", @@ -1458,9 +1458,9 @@ } }, "node_modules/@mui/x-data-grid": { - "version": "7.22.1", - "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.22.1.tgz", - "integrity": "sha512-YHF96MEv7ACG/VuiycZjEAPH7cZLNuV2+bi/MyR1t/e6E6LTolYFykvjSFq+Imz1mYbW4+9mEvrHZsIKL5KKIQ==", + "version": "7.22.2", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.22.2.tgz", + "integrity": "sha512-yfy2s5A6tbajQZiEdsba49T4FYb9F0WPrzbbG30dl1+sIiX4ZRX7ma44UIDGPZrsZv8xkkE+p8qeJxZ7OaMteA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", @@ -1495,9 +1495,9 @@ } }, "node_modules/@mui/x-date-pickers": { - "version": "7.22.1", - "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.1.tgz", - "integrity": "sha512-VBgicE+7PvJrdHSL6HyieHT6a/0dENH8RaMIM2VwUFrGoZzvik50WNwY5U+Hip1BwZLIEvlqtNRQIIj6kgBR6Q==", + "version": "7.22.2", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.2.tgz", + "integrity": "sha512-1KHSlIlnSoY3oHm820By8X344pIdGYqPvCCvfVHrEeeIQ/pHdxDD8tjZFWkFl4Jgm9oVFK90fMcqNZAzc+WaCw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", @@ -1525,7 +1525,7 @@ "dayjs": "^1.10.7", "luxon": "^3.0.2", "moment": "^2.29.4", - "moment-hijri": "^2.1.2", + "moment-hijri": "^2.1.2 || ^3.0.0", "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", "react": "^17.0.0 || ^18.0.0", "react-dom": "^17.0.0 || ^18.0.0" diff --git a/Client/package.json b/Client/package.json index 1a7213c20..eec7eea4b 100644 --- a/Client/package.json +++ b/Client/package.json @@ -18,8 +18,8 @@ "@mui/lab": "^5.0.0-alpha.170", "@mui/material": "^5.15.16", "@mui/x-charts": "^7.5.1", - "@mui/x-data-grid": "7.22.1", - "@mui/x-date-pickers": "7.22.1", + "@mui/x-data-grid": "7.22.2", + "@mui/x-date-pickers": "7.22.2", "@reduxjs/toolkit": "2.3.0", "axios": "^1.7.4", "chart.js": "^4.4.3", From ca8d89437cc26fa8a931729d2ef1c2a20a1204ef Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 8 Nov 2024 19:25:24 +0000 Subject: [PATCH 32/34] fix(deps): update dependency mongoose to v8.8.1 --- Server/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index 00c38c029..63742ad87 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -4369,9 +4369,9 @@ } }, "node_modules/mongoose": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.0.tgz", - "integrity": "sha512-KluvgwnQB1GPOYZZXUHJRjS1TW6xxwTlf/YgjWExuuNanIe3W7VcR7dDXQVCIRk8L7NYge8EnoTcu2grWtN+XQ==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.1.tgz", + "integrity": "sha512-l7DgeY1szT98+EKU8GYnga5WnyatAu+kOQ2VlVX1Mxif6A0Umt0YkSiksCiyGxzx8SPhGe9a53ND1GD4yVDrPA==", "license": "MIT", "dependencies": { "bson": "^6.7.0", From b77871adb02eef00de26dc352a2a6a4488950622 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Sun, 10 Nov 2024 01:33:51 +0000 Subject: [PATCH 33/34] fix(deps): update dependency bullmq to v5.25.4 --- Server/package-lock.json | 8 ++++---- Server/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index 7612eee1d..19390dc3f 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -12,7 +12,7 @@ "@sendgrid/mail": "^8.1.3", "axios": "^1.7.2", "bcrypt": "^5.1.1", - "bullmq": "5.25.3", + "bullmq": "5.25.4", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", @@ -1164,9 +1164,9 @@ "license": "MIT" }, "node_modules/bullmq": { - "version": "5.25.3", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.25.3.tgz", - "integrity": "sha512-nUFTszxV/V3qJMZQxSMNOBF1HiGKh895WyJmE5keUonkutpTsxdYIr0dzVUTPbhXvBvW9LWlY7BetWY3afy/MQ==", + "version": "5.25.4", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.25.4.tgz", + "integrity": "sha512-f9M5qfFOg9hdoMWmux9x9rZm9ZUPTMFfdDMO2zRsi7IOzgvZ0UxB6oTk77PlC9YSDYoufAgBw82xU1nwvnsKSA==", "license": "MIT", "dependencies": { "cron-parser": "^4.6.0", diff --git a/Server/package.json b/Server/package.json index d01167708..8e822c024 100644 --- a/Server/package.json +++ b/Server/package.json @@ -15,7 +15,7 @@ "@sendgrid/mail": "^8.1.3", "axios": "^1.7.2", "bcrypt": "^5.1.1", - "bullmq": "5.25.3", + "bullmq": "5.25.4", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", From aea462c8c43b0d01f2d35e3831f692ec3dbc9190 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Sun, 10 Nov 2024 03:09:04 +0000 Subject: [PATCH 34/34] fix(deps): update dependency winston to v3.17.0 --- Server/package-lock.json | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index 19390dc3f..53278a0e6 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -3467,9 +3467,10 @@ } }, "node_modules/logform": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", - "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", @@ -6673,34 +6674,35 @@ } }, "node_modules/winston": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.16.0.tgz", - "integrity": "sha512-xz7+cyGN5M+4CmmD4Npq1/4T+UZaz7HaeTlAruFUTjk79CNMq+P6H30vlE4z0qfqJ01VHYQwd7OZo03nYm/+lg==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", - "logform": "^2.6.0", + "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", - "winston-transport": "^4.7.0" + "winston-transport": "^4.9.0" }, "engines": { "node": ">= 12.0.0" } }, "node_modules/winston-transport": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz", - "integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", "dependencies": { - "logform": "^2.3.2", - "readable-stream": "^3.6.0", + "logform": "^2.7.0", + "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" }, "engines": {