diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index d4f540f2c..5cb04d59e 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -52,11 +52,10 @@ const calculateUptimeDuration = (checks) => { if (!checks || checks.length === 0) { return 0; } - 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; @@ -94,7 +93,8 @@ const getLatestResponseTime = (checks) => { if (!checks || checks.length === 0) { return 0; } - return checks[0].responseTime; + + return checks[0]?.responseTime ?? 0; }; /** @@ -106,14 +106,19 @@ 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; }; /** - * 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. */ @@ -143,6 +148,112 @@ 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 = (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; +}; + +/** + * 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) => { + // Validate the date + const checkDate = new Date(check.createdAt); + if (Number.isNaN(checkDate.getTime()) || checkDate.getTime() === 0) { + return acc; + } + + const time = + dateRange === "day" + ? checkDate.setMinutes(0, 0, 0) + : checkDate.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; + + const checksWithResponseTime = group.checks.filter( + (check) => typeof check.responseTime === "number" && !Number.isNaN(check.responseTime) + ); + + return { + time: group.time, + uptimePercentage: getUptimePercentage(group.checks), + totalChecks, + totalIncidents: group.checks.filter((check) => !check.status).length, + avgResponseTime: + checksWithResponseTime.length > 0 + ? checksWithResponseTime.reduce((sum, check) => sum + check.responseTime, 0) / + checksWithResponseTime.length + : 0, + }; +}; + /** * Get stats by monitor ID * @async @@ -152,129 +263,53 @@ 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( + NormalizeData, + 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; @@ -303,12 +338,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"; @@ -371,10 +406,12 @@ const getMonitorsByTeamId = async (req, res) => { filter, field, order, - } = req.query || {}; + } = 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 @@ -384,29 +421,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) @@ -415,29 +436,22 @@ 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; - } - let model = CHECK_MODEL_LOOKUP[monitor.type]; // 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) { @@ -446,7 +460,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"; @@ -591,3 +605,18 @@ export { editMonitor, addDemoMonitors, }; + +// Helper functions +export { + calculateUptimeDuration, + getLastChecked, + getLatestResponseTime, + getAverageResponseTime, + getUptimePercentage, + getIncidents, + getDateRange, + getMonitorChecks, + processChecksForDisplay, + groupChecksByTime, + calculateGroupStats, +}; diff --git a/Server/tests/db/monitorModule.test.js b/Server/tests/db/monitorModule.test.js new file mode 100644 index 000000000..854c37132 --- /dev/null +++ b/Server/tests/db/monitorModule.test.js @@ -0,0 +1,1676 @@ +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 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", + 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, 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", + "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" }, + }; + + 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().returns({ + limit: sinon.stub().returns([]), + }), + }); + }); + + 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: { + 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", + }, + }; + + 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" }, + }; + monitorCountStub.resolves(2); + + 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 result = await getMonitorsByTeamId(req); + expect(result.monitorCount).to.equal(2); + expect(result.monitors).to.have.lengthOf(2); + }); + it("should handle database errors", async () => { + 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), + }), + }), + }); + + 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"); + } + }); + }); +});