From 4a1b290416c020fbc9b4b1b927c877e0645f21bb Mon Sep 17 00:00:00 2001 From: om-3004 Date: Sat, 12 Oct 2024 22:58:39 +0530 Subject: [PATCH 01/37] check if the url resolves before adding the monitor --- .../Pages/Monitors/CreateMonitor/index.jsx | 88 +++++++++++-------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index ac68ea7eb..ac6e1258c 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -124,46 +124,58 @@ const CreateMonitor = () => { const handleCreateMonitor = async (event) => { event.preventDefault(); - //obj to submit - let form = { - url: - //preprending protocol for url - monitor.type === "http" - ? `http${https ? "s" : ""}://` + monitor.url - : monitor.url, - name: monitor.name === "" ? monitor.url : monitor.name, - type: monitor.type, - interval: monitor.interval * MS_PER_MINUTE, - }; - const { error } = monitorValidation.validate(form, { - abortEarly: false, - }); + const formattedUrl = monitor.type === "http" + ? `http${https ? "s" : ""}://` + monitor.url + : monitor.url; - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - createToast({ body: "Error validation data." }); - } else { - form = { - ...form, - description: form.name, - teamId: user.teamId, - userId: user._id, - notifications: monitor.notifications, - }; - const action = await dispatch( - createUptimeMonitor({ authToken, monitor: form }) - ); - if (action.meta.requestStatus === "fulfilled") { - createToast({ body: "Monitor created successfully!" }); - navigate("/monitors"); - } else { - createToast({ body: "Failed to create monitor." }); + try { + // Check if the URL resolves by sending a request + const response = await fetch(formattedUrl, { method: 'GET', mode: 'no-cors' }); + if (!response) { + throw new Error(); } + + //obj to submit + let form = { + url:formattedUrl, + name: monitor.name === "" ? monitor.url : monitor.name, + type: monitor.type, + interval: monitor.interval * MS_PER_MINUTE, + }; + + const { error } = monitorValidation.validate(form, { + abortEarly: false, + }); + + if (error) { + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + setErrors(newErrors); + createToast({ body: "Error validation data." }); + } else { + form = { + ...form, + description: form.name, + teamId: user.teamId, + userId: user._id, + notifications: monitor.notifications, + }; + const action = await dispatch( + createUptimeMonitor({ authToken, monitor: form }) + ); + if (action.meta.requestStatus === "fulfilled") { + createToast({ body: "Monitor created successfully!" }); + navigate("/monitors"); + } else { + createToast({ body: "Failed to create monitor." }); + } + } + } catch (error) { + createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." }); + setErrors({ url: "The entered URL is not reachable." }); } }; @@ -375,7 +387,7 @@ const CreateMonitor = () => { onClick={handleCreateMonitor} disabled={Object.keys(errors).length !== 0 && true} > - Create monitor + {monitor.type === "http" ? "Create Monitor" : "Check Endpoint"} From ebf6a0ceb718527a8bcb412d20a189d9e2ac745a Mon Sep 17 00:00:00 2001 From: om-3004 Date: Tue, 15 Oct 2024 17:20:54 +0530 Subject: [PATCH 02/37] Check the monitor URL resolution before adding it --- .../UptimeMonitors/uptimeMonitorsSlice.js | 42 ++++++++ .../Pages/Monitors/CreateMonitor/index.jsx | 98 +++++++++---------- Client/src/Utils/NetworkService.js | 26 +++++ Server/controllers/monitorController.js | 39 ++++++++ Server/routes/monitorRoute.js | 6 ++ 5 files changed, 161 insertions(+), 50 deletions(-) diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js index f6c3cb10a..05ec9b316 100644 --- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js @@ -31,6 +31,30 @@ export const createUptimeMonitor = createAsyncThunk( } ); +export const checkEndpointResolution = createAsyncThunk( + "monitors/checkEnpoint", + async(data, thunkApi) => { + try{ + const { authToken, monitorURL } = data; + + const res = await networkService.checkEndpointResolution({ + authToken: authToken, + monitorURL: monitorURL, + }) + return res.data; + } catch (error) { + if (error.response && error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); + } + } +) + export const getUptimeMonitorById = createAsyncThunk( "monitors/getMonitorById", async (data, thunkApi) => { @@ -271,6 +295,24 @@ const uptimeMonitorsSlice = createSlice({ : "Failed to create uptime monitor"; }) // ***************************************************** + // Resolve Endpoint + // ***************************************************** + .addCase(checkEndpointResolution.pending, (state) => { + state.isLoading = true; + }) + .addCase(checkEndpointResolution.fulfilled, (state, action) => { + state.isLoading = false; + state.success = action.payload.success; + state.msg = action.payload.msg; + }) + .addCase(checkEndpointResolution.rejected, (state, action) => { + state.isLoading = false; + state.success = false; + state.msg = action.payload + ? action.payload.msg + : "Failed to check endpoint resolution"; + }) + // ***************************************************** // Get Monitor By Id // ***************************************************** .addCase(getUptimeMonitorById.pending, (state) => { diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index ac6e1258c..50d4e3195 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -3,6 +3,7 @@ import { Box, Button, ButtonGroup, Stack, Typography } from "@mui/material"; import { useSelector, useDispatch } from "react-redux"; import { monitorValidation } from "../../../Validation/validation"; import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; +import { checkEndpointResolution } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice" import { useNavigate, useParams } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { createToast } from "../../../Utils/toastUtils"; @@ -124,58 +125,55 @@ const CreateMonitor = () => { const handleCreateMonitor = async (event) => { event.preventDefault(); + //obj to submit + let form = { + url: + //preprending protocol for url + monitor.type === "http" + ? `http${https ? "s" : ""}://` + monitor.url + : monitor.url, + name: monitor.name === "" ? monitor.url : monitor.name, + type: monitor.type, + interval: monitor.interval * MS_PER_MINUTE, + }; - const formattedUrl = monitor.type === "http" - ? `http${https ? "s" : ""}://` + monitor.url - : monitor.url; + const { error } = monitorValidation.validate(form, { + abortEarly: false, + }); - try { - // Check if the URL resolves by sending a request - const response = await fetch(formattedUrl, { method: 'GET', mode: 'no-cors' }); - if (!response) { - throw new Error(); - } - - //obj to submit - let form = { - url:formattedUrl, - name: monitor.name === "" ? monitor.url : monitor.name, - type: monitor.type, - interval: monitor.interval * MS_PER_MINUTE, - }; - - const { error } = monitorValidation.validate(form, { - abortEarly: false, + if (error) { + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; }); - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - createToast({ body: "Error validation data." }); - } else { - form = { - ...form, - description: form.name, - teamId: user.teamId, - userId: user._id, - notifications: monitor.notifications, - }; - const action = await dispatch( - createUptimeMonitor({ authToken, monitor: form }) - ); - if (action.meta.requestStatus === "fulfilled") { - createToast({ body: "Monitor created successfully!" }); - navigate("/monitors"); - } else { - createToast({ body: "Failed to create monitor." }); - } + setErrors(newErrors); + createToast({ body: "Error validation data." }); + } else { + const checkEndpointAction = await dispatch( + checkEndpointResolution({ authToken, monitorURL: form.url }) + ) + if (checkEndpointAction.meta.requestStatus === "rejected") { + createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." }); + setErrors({ url: "The entered URL is not reachable." }); + return; + } + + form = { + ...form, + description: form.name, + teamId: user.teamId, + userId: user._id, + notifications: monitor.notifications, + }; + const action = await dispatch( + createUptimeMonitor({ authToken, monitor: form }) + ); + if (action.meta.requestStatus === "fulfilled") { + createToast({ body: "Monitor created successfully!" }); + navigate("/monitors"); + } else { + createToast({ body: "Failed to create monitor." }); } - } catch (error) { - createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." }); - setErrors({ url: "The entered URL is not reachable." }); } }; @@ -387,7 +385,7 @@ const CreateMonitor = () => { onClick={handleCreateMonitor} disabled={Object.keys(errors).length !== 0 && true} > - {monitor.type === "http" ? "Create Monitor" : "Check Endpoint"} + Create Monitor @@ -395,4 +393,4 @@ const CreateMonitor = () => { ); }; -export default CreateMonitor; +export default CreateMonitor; \ No newline at end of file diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index 573651f58..a9dd80731 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -89,6 +89,32 @@ class NetworkService { }); } + /** + * + * ************************************ + * Check the endpoint resolution + * ************************************ + * + * @async + * @param {Object} config - The configuration object. + * @param {string} config.authToken - The authorization token to be used in the request header. + * @param {Object} config.monitorURL - The monitor url to be sent in the request body. + * @returns {Promise} The response from the axios POST request. + */ + async checkEndpointResolution(config) { + const { authToken, monitorURL } = config; + const params = new URLSearchParams(); + + if (monitorURL) params.append("monitorURL", monitorURL); + + return this.axiosInstance.get(`/monitors/check-endpoint/url?${params.toString()}`, { + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + } + }) + } + /** * * ************************************ diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index b7e27451b..44ba5a0e3 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -20,6 +20,7 @@ const jwt = require("jsonwebtoken"); const { getTokenFromHeaders } = require("../utils/utils"); const logger = require("../utils/logger"); const { handleError, handleValidationError } = require("./controllerUtils"); +const dns = require('dns'); /** * Returns all monitors @@ -262,6 +263,43 @@ const createMonitor = async (req, res, next) => { } }; +/** + * Checks if the endpoint can be resolved and adds it to the job queue. + * @async + * @param {Object} req - The Express request object. + * @property {Object} req.query - The query parameters of the request. + * @param {Object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Object} The response object with a success status, a message, and the resolution result. + * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + */ + +const checkEndpointResolution = async (req, res, next) => { + try { + let { monitorURL } = req.query; + + // Remove the protocol (http:// or https://) if present + monitorURL = monitorURL.replace(/^https?:\/\//, ''); + // Remove the trailing slash if it exists + monitorURL = monitorURL.endsWith('/') ? monitorURL.slice(0, -1) : monitorURL; + + dns.setServers(['8.8.8.8', '1.1.1.1']);// Google, Cloudfare + dns.resolve(monitorURL, (error) => { + console.log("Inside .resolve"); + if (error) { + next(handleError(error, SERVICE_NAME, "checkEndpointResolution")); + return ; + } + return res.status(200).json({ + success: true, + msg: `URL resolved successfully`, + }); + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "checkEndpointResolution")); + } +} + /** * Deletes a monitor by its ID and also deletes associated checks, alerts, and notifications. * @async @@ -475,6 +513,7 @@ module.exports = { getMonitorsAndSummaryByTeamId, getMonitorsByTeamId, createMonitor, + checkEndpointResolution, deleteMonitor, deleteAllMonitors, editMonitor, diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index aaa9c10e9..c3f695556 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -18,6 +18,12 @@ router.post( monitorController.createMonitor ); +router.get( + "/check-endpoint/url", + isAllowed(["admin", "superadmin"]), + monitorController.checkEndpointResolution +) + router.delete( "/:monitorId", isAllowed(["admin", "superadmin"]), From d1e65f53ba723c5b7ac58f89c2a39b9b3dc8dd14 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Wed, 16 Oct 2024 18:28:04 +0530 Subject: [PATCH 03/37] Add test cases for checking monitor endpoint resolution --- Server/controllers/monitorController.js | 9 ++-- .../controllers/monitorController.test.js | 54 +++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index e6a924863..0586d650e 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -274,7 +274,6 @@ const createMonitor = async (req, res, next) => { * @returns {Object} The response object with a success status, a message, and the resolution result. * @throws {Error} If there is an error during the process, especially if there is a validation error (422). */ - const checkEndpointResolution = async (req, res, next) => { try { let { monitorURL } = req.query; @@ -284,12 +283,12 @@ const checkEndpointResolution = async (req, res, next) => { // Remove the trailing slash if it exists monitorURL = monitorURL.endsWith('/') ? monitorURL.slice(0, -1) : monitorURL; - dns.setServers(['8.8.8.8', '1.1.1.1']);// Google, Cloudfare dns.resolve(monitorURL, (error) => { - console.log("Inside .resolve"); if (error) { - next(handleError(error, SERVICE_NAME, "checkEndpointResolution")); - return ; + return res.status(400).json({ + success: false, + msg: `DNS resolution failed`, + }); } return res.status(200).json({ success: true, diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index e2855e81b..730deb327 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -6,6 +6,7 @@ const { getMonitorsAndSummaryByTeamId, getMonitorsByTeamId, createMonitor, + checkEndpointResolution, deleteMonitor, deleteAllMonitors, editMonitor, @@ -21,6 +22,7 @@ const monitorController = proxyquire("../../controllers/monitorController", { "ssl-checker": sslCheckerStub, }); const logger = require("../../utils/logger"); +const dns = require("dns"); const SERVICE_NAME = "monitorController"; describe("Monitor Controller - getAllMonitors", () => { @@ -473,6 +475,58 @@ describe("Monitor Controller - createMonitor", () => { }); }); +describe('checkEndpointResolution', () => { + let dnsResolveStub; + beforeEach(() => { + req = { query: { monitorURL: 'example.com' } }; + res = { status: sinon.stub().returnsThis(), json: sinon.stub() }; + next = sinon.stub(); + dnsResolveStub = sinon.stub(dns, 'resolve'); + }); + afterEach(() => { + dnsResolveStub.restore(); + }); + it('should resolve the URL successfully', async () => { + dnsResolveStub.callsFake((monitorURL, callback) => callback(null)); + await checkEndpointResolution(req, res, next); + expect(res.status.calledWith(200)).to.be.true; + expect(res.json.calledWith({ + success: true, + msg: 'URL resolved successfully', + })).to.be.true; + expect(next.called).to.be.false; + }); + it("should return a 400 error message if DNS resolution fails", async () => { + const dnsError = new Error("DNS resolution failed"); + dnsResolveStub.callsFake((monitorURL, callback) => callback(dnsError)); + await checkEndpointResolution(req, res, next); + expect(res.status.calledOnceWith(400)).to.be.true; + expect(res.json.calledOnceWith({ + success: false, + msg: "DNS resolution failed", + })).to.be.true; + expect(next.notCalled).to.be.true; + }); + it("should remove the trailing slash and resolve the URL successfully", async () => { + req.query.monitorURL = 'http://example.com/'; // URL with a trailing slash + dnsResolveStub.callsFake((monitorURL, callback) => callback(null)); + await checkEndpointResolution(req, res, next); + expect(res.status.calledWith(200)).to.be.true; + expect(res.json.calledWith({ + success: true, + msg: 'URL resolved successfully', + })).to.be.true; + expect(next.called).to.be.false; + }); + it("should call next with an error if an exception is thrown", async () => { + req.query = {}; // Missing monitorURL will cause an error + await checkEndpointResolution(req, res, next); + expect(next.calledOnce).to.be.true; + const error = next.getCall(0).args[0]; + expect(error).to.be.an.instanceOf(Error); + }); +}); + describe("Monitor Controller - deleteMonitor", () => { beforeEach(() => { req = { From 6715a8b500a079a0a5c13e29593b197180977d8c Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 10:02:01 +0530 Subject: [PATCH 04/37] Refactor code for check endpoint and update test cases accordingly --- Server/controllers/monitorController.js | 9 ++---- .../controllers/monitorController.test.js | 30 +++++++++---------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 0586d650e..80cc1eccf 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -277,13 +277,8 @@ const createMonitor = async (req, res, next) => { const checkEndpointResolution = async (req, res, next) => { try { let { monitorURL } = req.query; - - // Remove the protocol (http:// or https://) if present - monitorURL = monitorURL.replace(/^https?:\/\//, ''); - // Remove the trailing slash if it exists - monitorURL = monitorURL.endsWith('/') ? monitorURL.slice(0, -1) : monitorURL; - - dns.resolve(monitorURL, (error) => { + monitorURL = new URL(monitorURL); + dns.resolve(monitorURL.hostname, (error) => { if (error) { return res.status(400).json({ success: false, diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index 730deb327..b446edf73 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -478,7 +478,7 @@ describe("Monitor Controller - createMonitor", () => { describe('checkEndpointResolution', () => { let dnsResolveStub; beforeEach(() => { - req = { query: { monitorURL: 'example.com' } }; + req = { query: { monitorURL: 'https://example.com' } }; res = { status: sinon.stub().returnsThis(), json: sinon.stub() }; next = sinon.stub(); dnsResolveStub = sinon.stub(dns, 'resolve'); @@ -487,7 +487,7 @@ describe('checkEndpointResolution', () => { dnsResolveStub.restore(); }); it('should resolve the URL successfully', async () => { - dnsResolveStub.callsFake((monitorURL, callback) => callback(null)); + dnsResolveStub.callsFake((hostname, callback) => callback(null)); await checkEndpointResolution(req, res, next); expect(res.status.calledWith(200)).to.be.true; expect(res.json.calledWith({ @@ -498,7 +498,7 @@ describe('checkEndpointResolution', () => { }); it("should return a 400 error message if DNS resolution fails", async () => { const dnsError = new Error("DNS resolution failed"); - dnsResolveStub.callsFake((monitorURL, callback) => callback(dnsError)); + dnsResolveStub.callsFake((hostname, callback) => callback(dnsError)); await checkEndpointResolution(req, res, next); expect(res.status.calledOnceWith(400)).to.be.true; expect(res.json.calledOnceWith({ @@ -507,23 +507,21 @@ describe('checkEndpointResolution', () => { })).to.be.true; expect(next.notCalled).to.be.true; }); - it("should remove the trailing slash and resolve the URL successfully", async () => { - req.query.monitorURL = 'http://example.com/'; // URL with a trailing slash - dnsResolveStub.callsFake((monitorURL, callback) => callback(null)); - await checkEndpointResolution(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect(res.json.calledWith({ - success: true, - msg: 'URL resolved successfully', - })).to.be.true; - expect(next.called).to.be.false; - }); - it("should call next with an error if an exception is thrown", async () => { - req.query = {}; // Missing monitorURL will cause an error + it('should call next with an error for invalid monitorURL', async () => { + req.query.monitorURL = 'invalid-url'; await checkEndpointResolution(req, res, next); expect(next.calledOnce).to.be.true; const error = next.getCall(0).args[0]; expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.include('Invalid URL'); + }); + it('should handle an edge case where monitorURL is not provided', async () => { + req.query = {}; + await checkEndpointResolution(req, res, next); + expect(next.calledOnce).to.be.true; + const error = next.getCall(0).args[0]; + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.include('Invalid URL'); }); }); From 3de2106c70a796032c456accb67ef1f2880aba80 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 10:09:50 +0530 Subject: [PATCH 05/37] Add api documentation for check resolution endpoint create --- Server/openapi.json | 55 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/Server/openapi.json b/Server/openapi.json index 6d079e685..aeb7285a8 100644 --- a/Server/openapi.json +++ b/Server/openapi.json @@ -845,6 +845,61 @@ ] } }, + "/monitors//check-endpoint/url": { + "get": { + "tags": ["monitors"], + "description": "Check DNS resolution for a given URL", + "parameters": [ + { + "name": "monitorURL", + "in": "query", + "required": true, + "schema": { + "type": "string", + "example": "https://example.com" + }, + "description": "The URL to check DNS resolution for" + } + ], + "responses": { + "200": { + "description": "URL resolved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse" + } + } + } + }, + "400": { + "description": "DNS resolution failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "/monitors/{monitorId}": { "get": { "tags": ["monitors"], From 9bc42cbf4fa51a0e6ac95f3f493330587f87028f Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 10:14:34 +0530 Subject: [PATCH 06/37] Add resolution check before adding PageSpeed monitors --- Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx index e855328da..802572782 100644 --- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx @@ -5,6 +5,7 @@ import { monitorValidation } from "../../../Validation/validation"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { createPageSpeed } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; +import { checkEndpointResolution } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice" import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; import { ConfigBox } from "../../Monitors/styled"; @@ -111,6 +112,15 @@ const CreatePageSpeed = () => { setErrors(newErrors); createToast({ body: "Error validation data." }); } else { + const checkEndpointAction = await dispatch( + checkEndpointResolution({ authToken, monitorURL: form.url }) + ) + if (checkEndpointAction.meta.requestStatus === "rejected") { + createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." }); + setErrors({ url: "The entered URL is not reachable." }); + return; + } + form = { ...form, description: form.name, From d61a41fd95392f30f28f692ab9f440271d6df08d Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 10:18:12 +0530 Subject: [PATCH 07/37] Update the api endpoint from /check-endpoint/url to /resolution/url --- Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js | 2 +- Client/src/Pages/Monitors/CreateMonitor/index.jsx | 2 +- Client/src/Utils/NetworkService.js | 2 +- Server/openapi.json | 2 +- Server/routes/monitorRoute.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js index 05ec9b316..81e4d4b1b 100644 --- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js @@ -32,7 +32,7 @@ export const createUptimeMonitor = createAsyncThunk( ); export const checkEndpointResolution = createAsyncThunk( - "monitors/checkEnpoint", + "monitors/checkEndpoint", async(data, thunkApi) => { try{ const { authToken, monitorURL } = data; diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index 50d4e3195..26a233800 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -385,7 +385,7 @@ const CreateMonitor = () => { onClick={handleCreateMonitor} disabled={Object.keys(errors).length !== 0 && true} > - Create Monitor + Create monitor diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index a9dd80731..22717b364 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -107,7 +107,7 @@ class NetworkService { if (monitorURL) params.append("monitorURL", monitorURL); - return this.axiosInstance.get(`/monitors/check-endpoint/url?${params.toString()}`, { + return this.axiosInstance.get(`/monitors/resolution/url?${params.toString()}`, { headers: { Authorization: `Bearer ${authToken}`, "Content-Type": "application/json", diff --git a/Server/openapi.json b/Server/openapi.json index aeb7285a8..ff13498ea 100644 --- a/Server/openapi.json +++ b/Server/openapi.json @@ -845,7 +845,7 @@ ] } }, - "/monitors//check-endpoint/url": { + "/monitors/resolution/url": { "get": { "tags": ["monitors"], "description": "Check DNS resolution for a given URL", diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index c3f695556..67ab8a5e6 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -19,7 +19,7 @@ router.post( ); router.get( - "/check-endpoint/url", + "/resolution/url", isAllowed(["admin", "superadmin"]), monitorController.checkEndpointResolution ) From 0ad94ecefce4156a593fcd1ff6d559522cfe8f2f Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 10:25:08 +0530 Subject: [PATCH 08/37] Bypass the resolution check when Ping Monitoring is selected --- .../src/Pages/Monitors/CreateMonitor/index.jsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index 26a233800..d9cdb725f 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -149,13 +149,15 @@ const CreateMonitor = () => { setErrors(newErrors); createToast({ body: "Error validation data." }); } else { - const checkEndpointAction = await dispatch( - checkEndpointResolution({ authToken, monitorURL: form.url }) - ) - if (checkEndpointAction.meta.requestStatus === "rejected") { - createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." }); - setErrors({ url: "The entered URL is not reachable." }); - return; + if (monitor.type === "http") { + const checkEndpointAction = await dispatch( + checkEndpointResolution({ authToken, monitorURL: form.url }) + ) + if (checkEndpointAction.meta.requestStatus === "rejected") { + createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." }); + setErrors({ url: "The entered URL is not reachable." }); + return; + } } form = { From afe7c9047c1ae904ac30a4ae35b4d69cb647eb5b Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 10:47:34 +0530 Subject: [PATCH 09/37] Handle imports exports format check resolution tests code --- Server/controllers/monitorController.js | 1 + Server/routes/monitorRoute.js | 5 +- .../controllers/monitorController.test.js | 95 ++++++++++--------- 3 files changed, 52 insertions(+), 49 deletions(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 905a4b9bd..93d4e4f1c 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -514,6 +514,7 @@ export { getMonitorsAndSummaryByTeamId, getMonitorsByTeamId, createMonitor, + checkEndpointResolution, deleteMonitor, deleteAllMonitors, editMonitor, diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index f45f3ba33..a0ab11d84 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -7,6 +7,7 @@ import { getMonitorsAndSummaryByTeamId, getMonitorsByTeamId, createMonitor, + checkEndpointResolution, deleteMonitor, deleteAllMonitors, editMonitor, @@ -30,13 +31,13 @@ router.get("/team/:teamId", getMonitorsByTeamId); router.get( "/resolution/url", isAllowed(["admin", "superadmin"]), - monitorController.checkEndpointResolution + checkEndpointResolution ) router.delete( "/:monitorId", isAllowed(["admin", "superadmin"]), - monitorController.deleteMonitor + deleteMonitor ); router.post("/", isAllowed(["admin", "superadmin"]), createMonitor); diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index eb8c8e44a..d05685cdb 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -6,6 +6,7 @@ import { getMonitorsAndSummaryByTeamId, getMonitorsByTeamId, createMonitor, + checkEndpointResolution, deleteMonitor, deleteAllMonitors, editMonitor, @@ -461,53 +462,53 @@ describe("Monitor Controller - createMonitor", () => { }); describe('checkEndpointResolution', () => { - let dnsResolveStub; - beforeEach(() => { - req = { query: { monitorURL: 'https://example.com' } }; - res = { status: sinon.stub().returnsThis(), json: sinon.stub() }; - next = sinon.stub(); - dnsResolveStub = sinon.stub(dns, 'resolve'); - }); - afterEach(() => { - dnsResolveStub.restore(); - }); - it('should resolve the URL successfully', async () => { - dnsResolveStub.callsFake((hostname, callback) => callback(null)); - await checkEndpointResolution(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect(res.json.calledWith({ - success: true, - msg: 'URL resolved successfully', - })).to.be.true; - expect(next.called).to.be.false; - }); - it("should return a 400 error message if DNS resolution fails", async () => { - const dnsError = new Error("DNS resolution failed"); - dnsResolveStub.callsFake((hostname, callback) => callback(dnsError)); - await checkEndpointResolution(req, res, next); - expect(res.status.calledOnceWith(400)).to.be.true; - expect(res.json.calledOnceWith({ - success: false, - msg: "DNS resolution failed", - })).to.be.true; - expect(next.notCalled).to.be.true; - }); - it('should call next with an error for invalid monitorURL', async () => { - req.query.monitorURL = 'invalid-url'; - await checkEndpointResolution(req, res, next); - expect(next.calledOnce).to.be.true; - const error = next.getCall(0).args[0]; - expect(error).to.be.an.instanceOf(Error); - expect(error.message).to.include('Invalid URL'); - }); - it('should handle an edge case where monitorURL is not provided', async () => { - req.query = {}; - await checkEndpointResolution(req, res, next); - expect(next.calledOnce).to.be.true; - const error = next.getCall(0).args[0]; - expect(error).to.be.an.instanceOf(Error); - expect(error.message).to.include('Invalid URL'); - }); + let req, res, next, dnsResolveStub; + beforeEach(() => { + req = { query: { monitorURL: 'https://example.com' } }; + res = { status: sinon.stub().returnsThis(), json: sinon.stub() }; + next = sinon.stub(); + dnsResolveStub = sinon.stub(dns, 'resolve'); + }); + afterEach(() => { + dnsResolveStub.restore(); + }); + it('should resolve the URL successfully', async () => { + dnsResolveStub.callsFake((hostname, callback) => callback(null)); + await checkEndpointResolution(req, res, next); + expect(res.status.calledWith(200)).to.be.true; + expect(res.json.calledWith({ + success: true, + msg: 'URL resolved successfully', + })).to.be.true; + expect(next.called).to.be.false; + }); + it("should return a 400 error message if DNS resolution fails", async () => { + const dnsError = new Error("DNS resolution failed"); + dnsResolveStub.callsFake((hostname, callback) => callback(dnsError)); + await checkEndpointResolution(req, res, next); + expect(res.status.calledOnceWith(400)).to.be.true; + expect(res.json.calledOnceWith({ + success: false, + msg: "DNS resolution failed", + })).to.be.true; + expect(next.notCalled).to.be.true; + }); + it('should call next with an error for invalid monitorURL', async () => { + req.query.monitorURL = 'invalid-url'; + await checkEndpointResolution(req, res, next); + expect(next.calledOnce).to.be.true; + const error = next.getCall(0).args[0]; + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.include('Invalid URL'); + }); + it('should handle an edge case where monitorURL is not provided', async () => { + req.query = {}; + await checkEndpointResolution(req, res, next); + expect(next.calledOnce).to.be.true; + const error = next.getCall(0).args[0]; + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.include('Invalid URL'); + }); }); describe("Monitor Controller - deleteMonitor", () => { From 7ec5e64f6fe84eef97af8db29e845eafbd957046 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 10:50:19 +0530 Subject: [PATCH 10/37] Remove unused imports --- Client/src/Utils/NetworkService.js | 1 - Server/controllers/monitorController.js | 1 - Server/tests/controllers/monitorController.test.js | 2 -- 3 files changed, 4 deletions(-) diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index a91a6cbe6..ff509a0e9 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -3,7 +3,6 @@ const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL; const FALLBACK_BASE_URL = "http://localhost:5000/api/v1"; import { clearAuthState } from "../Features/Auth/authSlice"; import { clearUptimeMonitorState } from "../Features/UptimeMonitors/uptimeMonitorsSlice"; -import { logger } from "./Logger"; class NetworkService { constructor(store, dispatch, navigate) { this.store = store; diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 93d4e4f1c..d72a8254b 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -21,7 +21,6 @@ import logger from "../utils/logger.js"; import { handleError, handleValidationError, - fetchMonitorCertificate, } from "./controllerUtils.js"; import dns from "dns"; diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index d05685cdb..503a2c5cb 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -17,8 +17,6 @@ import jwt from "jsonwebtoken"; import sinon from "sinon"; import { successMessages } from "../../utils/messages.js"; import logger from "../../utils/logger.js"; -// import * as monitorController from "../../controllers/monitorController.js"; -// import { fetchMonitorCertificate } from "../../controllers/controllerUtils.js"; import dns from "dns"; const SERVICE_NAME = "monitorController"; From 0f763e7913778e3ca6b9e57b81b44a7dce4b4639 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 11:06:46 +0530 Subject: [PATCH 11/37] promisify the resolve method to pass the error to the middleware and updat test cases accordingly --- Server/controllers/monitorController.js | 20 +++++++++---------- .../controllers/monitorController.test.js | 10 ++++------ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index d72a8254b..5ee6d2c5d 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -276,18 +276,18 @@ const checkEndpointResolution = async (req, res, next) => { try { let { monitorURL } = req.query; monitorURL = new URL(monitorURL); - dns.resolve(monitorURL.hostname, (error) => { - if (error) { - return res.status(400).json({ - success: false, - msg: `DNS resolution failed`, - }); - } - return res.status(200).json({ - success: true, - msg: `URL resolved successfully`, + await new Promise((resolve, reject) => { + dns.resolve(monitorURL.hostname, (error) => { + if (error) { + reject(error); + } + resolve(); }); }); + return res.status(200).json({ + success: true, + msg: `URL resolved successfully`, + }); } catch (error) { next(handleError(error, SERVICE_NAME, "checkEndpointResolution")); } diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index 503a2c5cb..f83d74396 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -484,12 +484,10 @@ describe('checkEndpointResolution', () => { const dnsError = new Error("DNS resolution failed"); dnsResolveStub.callsFake((hostname, callback) => callback(dnsError)); await checkEndpointResolution(req, res, next); - expect(res.status.calledOnceWith(400)).to.be.true; - expect(res.json.calledOnceWith({ - success: false, - msg: "DNS resolution failed", - })).to.be.true; - expect(next.notCalled).to.be.true; + expect(next.calledOnce).to.be.true; + const errorPassedToNext = next.getCall(0).args[0]; + expect(errorPassedToNext).to.be.an.instanceOf(Error); + expect(errorPassedToNext.message).to.include('DNS resolution failed'); }); it('should call next with an error for invalid monitorURL', async () => { req.query.monitorURL = 'invalid-url'; From 5d3e1796e0bdd4dce73d31cead821c760c0f5406 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 11:09:23 +0530 Subject: [PATCH 12/37] Update the test name to follow the naming convention --- Server/tests/controllers/monitorController.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index f83d74396..26c6a6048 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -459,7 +459,7 @@ describe("Monitor Controller - createMonitor", () => { }); }); -describe('checkEndpointResolution', () => { +describe("Monitor Controllor - checkEndpointResolution", () => { let req, res, next, dnsResolveStub; beforeEach(() => { req = { query: { monitorURL: 'https://example.com' } }; From 7f0d5d42551a8c63ec01f21d79a32875083eb21c Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 11:16:49 +0530 Subject: [PATCH 13/37] Update test case to check for the specific errors and remove test case for empty URL --- Server/tests/controllers/monitorController.test.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index 26c6a6048..690649660 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -482,27 +482,21 @@ describe("Monitor Controllor - checkEndpointResolution", () => { }); it("should return a 400 error message if DNS resolution fails", async () => { const dnsError = new Error("DNS resolution failed"); + dnsError.code = 'ENOTFOUND'; dnsResolveStub.callsFake((hostname, callback) => callback(dnsError)); await checkEndpointResolution(req, res, next); expect(next.calledOnce).to.be.true; const errorPassedToNext = next.getCall(0).args[0]; expect(errorPassedToNext).to.be.an.instanceOf(Error); - expect(errorPassedToNext.message).to.include('DNS resolution failed'); + expect(errorPassedToNext.message).to.include('DNS resolution failed'); + expect(errorPassedToNext.code).to.equal('ENOTFOUND'); }); it('should call next with an error for invalid monitorURL', async () => { req.query.monitorURL = 'invalid-url'; await checkEndpointResolution(req, res, next); expect(next.calledOnce).to.be.true; const error = next.getCall(0).args[0]; - expect(error).to.be.an.instanceOf(Error); - expect(error.message).to.include('Invalid URL'); - }); - it('should handle an edge case where monitorURL is not provided', async () => { - req.query = {}; - await checkEndpointResolution(req, res, next); - expect(next.calledOnce).to.be.true; - const error = next.getCall(0).args[0]; - expect(error).to.be.an.instanceOf(Error); + expect(error).to.be.an.instanceOf(TypeError); expect(error.message).to.include('Invalid URL'); }); }); From aee7e02587aa16d2134fa6a62e7f9a6fff4a58c3 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 12:35:15 +0530 Subject: [PATCH 14/37] Add validation for monitor URL and update test case and api documentation accordingly --- Server/controllers/monitorController.js | 8 ++++++++ Server/openapi.json | 10 ++++++++++ Server/tests/controllers/monitorController.test.js | 8 ++++---- Server/validation/joi.js | 7 ++++++- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 5ee6d2c5d..c56e8851b 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -3,6 +3,7 @@ import { getMonitorByIdQueryValidation, getMonitorsByTeamIdValidation, createMonitorBodyValidation, + getMonitorURLByQueryValidation, editMonitorBodyValidation, getMonitorsAndSummaryByTeamIdParamValidation, getMonitorsAndSummaryByTeamIdQueryValidation, @@ -273,6 +274,13 @@ const createMonitor = async (req, res, next) => { * @throws {Error} If there is an error during the process, especially if there is a validation error (422). */ const checkEndpointResolution = async (req, res, next) => { + try { + await getMonitorURLByQueryValidation.validateAsync(req.query); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } + try { let { monitorURL } = req.query; monitorURL = new URL(monitorURL); diff --git a/Server/openapi.json b/Server/openapi.json index ff13498ea..aaa10a5d3 100644 --- a/Server/openapi.json +++ b/Server/openapi.json @@ -882,6 +882,16 @@ } } }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal Server Error", "content": { diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index 690649660..bd89b3dea 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -480,7 +480,7 @@ describe("Monitor Controllor - checkEndpointResolution", () => { })).to.be.true; expect(next.called).to.be.false; }); - it("should return a 400 error message if DNS resolution fails", async () => { + it("should return an error if DNS resolution fails", async () => { const dnsError = new Error("DNS resolution failed"); dnsError.code = 'ENOTFOUND'; dnsResolveStub.callsFake((hostname, callback) => callback(dnsError)); @@ -491,13 +491,13 @@ describe("Monitor Controllor - checkEndpointResolution", () => { expect(errorPassedToNext.message).to.include('DNS resolution failed'); expect(errorPassedToNext.code).to.equal('ENOTFOUND'); }); - it('should call next with an error for invalid monitorURL', async () => { + it('should reject with an error if query validation fails', async () => { req.query.monitorURL = 'invalid-url'; await checkEndpointResolution(req, res, next); expect(next.calledOnce).to.be.true; const error = next.getCall(0).args[0]; - expect(error).to.be.an.instanceOf(TypeError); - expect(error.message).to.include('Invalid URL'); + expect(next.firstCall.args[0]).to.be.an("error"); + expect(next.firstCall.args[0].status).to.equal(422); }); }); diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 033650a3d..090cb3211 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -236,12 +236,16 @@ const pauseMonitorParamValidation = joi.object({ monitorId: joi.string().required(), }); +const getMonitorURLByQueryValidation = joi.object({ + monitorURL: joi.string().uri().required(), +}); + //**************************************** // Alerts //**************************************** const createAlertParamValidation = joi.object({ - monitorId: joi.string().required(), + monitorId: joi.string().uri().required(), }); const createAlertBodyValidation = joi.object({ @@ -446,6 +450,7 @@ export { getCertificateParamValidation, editMonitorBodyValidation, pauseMonitorParamValidation, + getMonitorURLByQueryValidation, editUserParamValidation, editUserBodyValidation, createAlertParamValidation, From 8819b0ab78fc4758e2f8b3b2f197876823af678d Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 13:00:11 +0530 Subject: [PATCH 15/37] Update Button to show Checking endpoint during the endpoint checking process in Create Monitor as well as PageSpeed --- Client/src/Pages/Monitors/CreateMonitor/index.jsx | 6 +++++- Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index d9cdb725f..e919b1b28 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -43,6 +43,7 @@ const CreateMonitor = () => { }); const [https, setHttps] = useState(true); const [errors, setErrors] = useState({}); + const [isCheckingEndpoint, setIsCheckingEndpoint] = useState(false); useEffect(() => { const fetchMonitor = async () => { @@ -125,6 +126,7 @@ const CreateMonitor = () => { const handleCreateMonitor = async (event) => { event.preventDefault(); + setIsCheckingEndpoint(true); //obj to submit let form = { url: @@ -156,8 +158,10 @@ const CreateMonitor = () => { if (checkEndpointAction.meta.requestStatus === "rejected") { createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." }); setErrors({ url: "The entered URL is not reachable." }); + setIsCheckingEndpoint(false); return; } + setIsCheckingEndpoint(false); } form = { @@ -387,7 +391,7 @@ const CreateMonitor = () => { onClick={handleCreateMonitor} disabled={Object.keys(errors).length !== 0 && true} > - Create monitor + { isCheckingEndpoint ? "Checking endpoint" : "Create monitor" } diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx index 802572782..6bdbaacc9 100644 --- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx @@ -40,6 +40,7 @@ const CreatePageSpeed = () => { }); const [https, setHttps] = useState(true); const [errors, setErrors] = useState({}); + const [isCheckingEndpoint, setIsCheckingEndpoint] = useState(false); const handleChange = (event, name) => { const { value, id } = event.target; @@ -92,6 +93,7 @@ const CreatePageSpeed = () => { const handleCreateMonitor = async (event) => { event.preventDefault(); + setIsCheckingEndpoint(true); //obj to submit let form = { url: `http${https ? "s" : ""}://` + monitor.url, @@ -118,8 +120,10 @@ const CreatePageSpeed = () => { if (checkEndpointAction.meta.requestStatus === "rejected") { createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." }); setErrors({ url: "The entered URL is not reachable." }); + setIsCheckingEndpoint(false); return; } + setIsCheckingEndpoint(false); form = { ...form, @@ -343,7 +347,7 @@ const CreatePageSpeed = () => { onClick={handleCreateMonitor} disabled={Object.keys(errors).length !== 0 && true} > - Create monitor + { isCheckingEndpoint ? "Checking endpoint" : "Create monitor" } From cb64d16d692e734ad5364e67829b4d82d7893b36 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 14:21:47 +0530 Subject: [PATCH 16/37] Remove the extra uri in validation which was added by mistake --- Server/validation/joi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 090cb3211..1aaab0041 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -245,7 +245,7 @@ const getMonitorURLByQueryValidation = joi.object({ //**************************************** const createAlertParamValidation = joi.object({ - monitorId: joi.string().uri().required(), + monitorId: joi.string().required(), }); const createAlertBodyValidation = joi.object({ From ff6051b6b0a7e28237d04bc892447857e9c16029 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 16:59:49 +0530 Subject: [PATCH 17/37] Update test case for url resolution to make it more specific --- Server/controllers/monitorController.js | 2 +- Server/tests/controllers/monitorController.test.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index c56e8851b..302e0ff11 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -264,7 +264,7 @@ const createMonitor = async (req, res, next) => { }; /** - * Checks if the endpoint can be resolved and adds it to the job queue. + * Checks if the endpoint can be resolved * @async * @param {Object} req - The Express request object. * @property {Object} req.query - The query parameters of the request. diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index bd89b3dea..0a9dbafe6 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -490,6 +490,7 @@ describe("Monitor Controllor - checkEndpointResolution", () => { expect(errorPassedToNext).to.be.an.instanceOf(Error); expect(errorPassedToNext.message).to.include('DNS resolution failed'); expect(errorPassedToNext.code).to.equal('ENOTFOUND'); + expect(errorPassedToNext.status).to.equal(500); }); it('should reject with an error if query validation fails', async () => { req.query.monitorURL = 'invalid-url'; @@ -498,6 +499,7 @@ describe("Monitor Controllor - checkEndpointResolution", () => { const error = next.getCall(0).args[0]; expect(next.firstCall.args[0]).to.be.an("error"); expect(next.firstCall.args[0].status).to.equal(422); + expect(error.message).to.equal('"monitorURL" must be a valid uri'); }); }); From 9de2dfe514082a1c932500ee57e23ab630fa585b Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 17:27:40 +0530 Subject: [PATCH 18/37] Remove duplicate api endpoint for deleting monitor --- Server/routes/monitorRoute.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index a0ab11d84..f306a5146 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -42,8 +42,6 @@ router.delete( router.post("/", isAllowed(["admin", "superadmin"]), createMonitor); -router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), deleteMonitor); - router.put("/:monitorId", isAllowed(["admin", "superadmin"]), editMonitor); router.delete("/", isAllowed(["superadmin"]), deleteAllMonitors); From 1c30c309a7651f63a752b72b8f764d6d7ceec777 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 18 Oct 2024 11:12:18 +0800 Subject: [PATCH 19/37] Refactor network service to inject dependencies --- Server/index.js | 7 ++++- Server/service/networkService.js | 53 +++++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/Server/index.js b/Server/index.js index ae9b7241c..71dc42467 100644 --- a/Server/index.js +++ b/Server/index.js @@ -20,7 +20,12 @@ import { fileURLToPath } from "url"; import { connectDbAndRunServer } from "./configs/db.js"; import queueRouter from "./routes/queueRoute.js"; import JobQueue from "./service/jobQueue.js"; + +//Network service and dependencies import NetworkService from "./service/networkService.js"; +import axios from "axios"; +import ping from "ping"; +import http from "http"; // Email service and dependencies import EmailService from "./service/emailService.js"; @@ -149,7 +154,7 @@ const startApp = async () => { nodemailer, logger ); - const networkService = new NetworkService(db, emailService); + const networkService = new NetworkService(db, emailService, axios, ping, logger, http); const jobQueue = await JobQueue.createJobQueue(db, networkService, settingsService); const cleanup = async () => { diff --git a/Server/service/networkService.js b/Server/service/networkService.js index d2964bd25..18cccd6bb 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -1,7 +1,3 @@ -import axios from "axios"; -import ping from "ping"; -import logger from "../utils/logger.js"; -import http from "http"; import { errorMessages, successMessages } from "../utils/messages.js"; /** @@ -12,7 +8,17 @@ import { errorMessages, successMessages } from "../utils/messages.js"; */ class NetworkService { - constructor(db, emailService) { + /** + * Creates an instance of NetworkService. + * + * @param {Object} db - The database service. + * @param {Object} emailService - The email service. + * @param {Object} axios - The axios HTTP client. + * @param {Object} ping - The ping service. + * @param {Object} logger - The logging service. + * @param {Object} http - The HTTP service. + */ + constructor(db, emailService, axios, ping, logger, http) { this.db = db; this.emailService = emailService; this.TYPE_PING = "ping"; @@ -20,9 +26,19 @@ class NetworkService { this.TYPE_PAGESPEED = "pagespeed"; this.SERVICE_NAME = "NetworkService"; this.NETWORK_ERROR = 5000; + this.axios = axios; + this.ping = ping; + this.logger = logger; + this.http = http; } - async handleNotification(monitor, isAlive) { + /** + * Handles the notification process for a monitor. + * + * @param {Object} monitor - The monitor object containing monitor details. + * @param {boolean} isAlive - The status of the monitor (true if up, false if down). + * @returns {Promise} + */ async handleNotification(monitor, isAlive) { try { let template = isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate"; let status = isAlive === true ? "up" : "down"; @@ -47,6 +63,13 @@ class NetworkService { } } + /** + * Handles the status update for a monitor job. + * + * @param {Object} job - The job object containing job details. + * @param {boolean} isAlive - The status of the monitor (true if up, false if down). + * @returns {Promise} + */ async handleStatusUpdate(job, isAlive) { let monitor; // Look up the monitor, if it doesn't exist, it's probably been removed, return @@ -113,7 +136,7 @@ class NetworkService { */ async handlePing(job) { const operation = async () => { - const response = await ping.promise.probe(job.data.url); + const response = await this.ping.promise.probe(job.data.url); return response; }; @@ -153,7 +176,7 @@ class NetworkService { async handleHttp(job) { // Define operation for timing const operation = async () => { - const response = await axios.get(job.data.url); + const response = await this.axios.get(job.data.url); return response; }; @@ -171,12 +194,12 @@ class NetworkService { status: isAlive, responseTime, statusCode: response.status, - message: http.STATUS_CODES[response.status], + message: this.http.STATUS_CODES[response.status], }; return await this.logAndStoreCheck(checkData, this.db.createCheck); } catch (error) { const statusCode = error.response?.status || this.NETWORK_ERROR; - let message = http.STATUS_CODES[statusCode] || "Network Error"; + let message = this.http.STATUS_CODES[statusCode] || "Network Error"; isAlive = false; const checkData = { monitorId: job.data._id, @@ -210,7 +233,7 @@ class NetworkService { let isAlive; try { const url = job.data.url; - const response = await axios.get( + const response = await this.axios.get( `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance` ); const pageSpeedResults = response.data; @@ -236,7 +259,7 @@ class NetworkService { monitorId: job.data._id, status: isAlive, statusCode: response.status, - message: http.STATUS_CODES[response.status], + message: this.http.STATUS_CODES[response.status], accessibility: (categories.accessibility?.score || 0) * 100, bestPractices: (categories["best-practices"]?.score || 0) * 100, seo: (categories.seo?.score || 0) * 100, @@ -254,7 +277,7 @@ class NetworkService { } catch (error) { isAlive = false; const statusCode = error.response?.status || this.NETWORK_ERROR; - const message = http.STATUS_CODES[statusCode] || "Network Error"; + const message = this.http.STATUS_CODES[statusCode] || "Network Error"; const checkData = { monitorId: job.data._id, status: isAlive, @@ -287,7 +310,7 @@ class NetworkService { case this.TYPE_PAGESPEED: return await this.handlePagespeed(job); default: - logger.error(`Unsupported type: ${job.data.type}`, { + this.logger.error(`Unsupported type: ${job.data.type}`, { service: this.SERVICE_NAME, method: "getStatus", jobId: job.id, @@ -312,7 +335,7 @@ class NetworkService { return insertedCheck.status; } } catch (error) { - logger.error(`Error wrtiting check for ${data.monitorId}`, { + this.logger.error(`Error wrtiting check for ${data.monitorId}`, { service: this.SERVICE_NAME, method: "logAndStoreCheck", monitorId: data.monitorId, From f5265b140c2f461a3962ac04a5452cc9c30fe37a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 18 Oct 2024 13:36:39 +0800 Subject: [PATCH 20/37] Minor refactor for correctness --- Server/service/jobQueue.js | 2 +- Server/service/networkService.js | 34 ++++++++++++++------------------ 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index 1e8c7f8e4..f8493911e 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -98,7 +98,7 @@ class JobQueue { }, false); if (!maintenanceWindowActive) { - const res = await this.networkService.getStatus(job); + await this.networkService.getStatus(job); } else { logger.info(`Monitor ${monitorId} is in maintenance window`, { service: SERVICE_NAME, diff --git a/Server/service/networkService.js b/Server/service/networkService.js index 18cccd6bb..9c884a369 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -55,7 +55,7 @@ class NetworkService { } } } catch (error) { - logger.error(error.message, { + this.logger.error(error.message, { method: "handleNotification", service: this.SERVICE_NAME, monitorId: monitor._id, @@ -72,9 +72,10 @@ class NetworkService { */ async handleStatusUpdate(job, isAlive) { let monitor; + const { _id } = job.data; + // Look up the monitor, if it doesn't exist, it's probably been removed, return try { - const { _id } = job.data; monitor = await this.db.getMonitorById(_id); } catch (error) { return; @@ -83,7 +84,7 @@ class NetworkService { // Otherwise, try to update monitor status try { if (monitor === null || monitor === undefined) { - logger.error(`Null Monitor: ${_id}`, { + this.logger.error(`Null Monitor: ${_id}`, { method: "handleStatusUpdate", service: this.SERVICE_NAME, jobId: job.id, @@ -100,7 +101,7 @@ class NetworkService { } } } catch (error) { - logger.error(error.message, { + this.logger.error(error.message, { method: "handleStatusUpdate", service: this.SERVICE_NAME, jobId: job.id, @@ -123,7 +124,7 @@ class NetworkService { } catch (error) { const endTime = Date.now(); error.responseTime = endTime - startTime; - error.service === undefined ? (error.service = SERVICE_NAME) : null; + error.service === undefined ? (error.service = this.SERVICE_NAME) : null; error.method === undefined ? (error.method = "measureResponseTime") : null; throw error; } @@ -135,15 +136,12 @@ class NetworkService { * @returns {Promise<{boolean}} The result of logging and storing the check */ async handlePing(job) { - const operation = async () => { - const response = await this.ping.promise.probe(job.data.url); - return response; - }; - let isAlive; try { - const { responseTime, response } = await this.measureResponseTime(operation); + const { responseTime, response } = await this.measureResponseTime( + this.ping.promise.probe(job.data.url) + ); isAlive = response.alive; const checkData = { monitorId: job.data._id, @@ -153,7 +151,7 @@ class NetworkService { ? successMessages.PING_SUCCESS : errorMessages.PING_CANNOT_RESOLVE, }; - return await this.logAndStoreCheck(checkData, this.db.createCheck); + await this.logAndStoreCheck(checkData, this.db.createCheck); } catch (error) { isAlive = false; const checkData = { @@ -162,7 +160,7 @@ class NetworkService { message: errorMessages.PING_CANNOT_RESOLVE, responseTime: error.responseTime, }; - return await this.logAndStoreCheck(checkData, this.db.createCheck); + await this.logAndStoreCheck(checkData, this.db.createCheck); } finally { this.handleStatusUpdate(job, isAlive); } @@ -196,7 +194,7 @@ class NetworkService { statusCode: response.status, message: this.http.STATUS_CODES[response.status], }; - return await this.logAndStoreCheck(checkData, this.db.createCheck); + await this.logAndStoreCheck(checkData, this.db.createCheck); } catch (error) { const statusCode = error.response?.status || this.NETWORK_ERROR; let message = this.http.STATUS_CODES[statusCode] || "Network Error"; @@ -208,8 +206,6 @@ class NetworkService { responseTime: error.responseTime, message, }; - - return await this.logAndStoreCheck(checkData, this.db.createCheck); } finally { this.handleStatusUpdate(job, isAlive); } @@ -233,6 +229,7 @@ class NetworkService { let isAlive; try { const url = job.data.url; + const response = await this.axios.get( `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance` ); @@ -246,7 +243,6 @@ class NetworkService { "largest-contentful-paint": lcp, "total-blocking-time": tbt, } = audits; - // Weights // First Contentful Paint 10% // Speed Index 10% @@ -272,7 +268,6 @@ class NetworkService { tbt, }, }; - this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck); } catch (error) { isAlive = false; @@ -334,8 +329,9 @@ class NetworkService { if (insertedCheck !== null && insertedCheck !== undefined) { return insertedCheck.status; } + throw new Error(); } catch (error) { - this.logger.error(`Error wrtiting check for ${data.monitorId}`, { + this.logger.error(`Error writing check for ${data.monitorId}`, { service: this.SERVICE_NAME, method: "logAndStoreCheck", monitorId: data.monitorId, From 6c40500153c6686c28b37860b97e00329dc4f598 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Fri, 18 Oct 2024 11:09:52 +0530 Subject: [PATCH 21/37] Add loading button while the url is getting resolved --- Client/src/Pages/Monitors/CreateMonitor/index.jsx | 14 ++++++-------- .../src/Pages/PageSpeed/CreatePageSpeed/index.jsx | 13 ++++++------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index e919b1b28..49e4b09a5 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -1,5 +1,6 @@ import { useState, useEffect } from "react"; import { Box, Button, ButtonGroup, Stack, Typography } from "@mui/material"; +import LoadingButton from '@mui/lab/LoadingButton'; import { useSelector, useDispatch } from "react-redux"; import { monitorValidation } from "../../../Validation/validation"; import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; @@ -20,7 +21,7 @@ import "./index.css"; const CreateMonitor = () => { const MS_PER_MINUTE = 60000; const { user, authToken } = useSelector((state) => state.auth); - const { monitors } = useSelector((state) => state.uptimeMonitors); + const { monitors, isLoading } = useSelector((state) => state.uptimeMonitors); const dispatch = useDispatch(); const navigate = useNavigate(); const theme = useTheme(); @@ -43,7 +44,6 @@ const CreateMonitor = () => { }); const [https, setHttps] = useState(true); const [errors, setErrors] = useState({}); - const [isCheckingEndpoint, setIsCheckingEndpoint] = useState(false); useEffect(() => { const fetchMonitor = async () => { @@ -126,7 +126,6 @@ const CreateMonitor = () => { const handleCreateMonitor = async (event) => { event.preventDefault(); - setIsCheckingEndpoint(true); //obj to submit let form = { url: @@ -158,10 +157,8 @@ const CreateMonitor = () => { if (checkEndpointAction.meta.requestStatus === "rejected") { createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." }); setErrors({ url: "The entered URL is not reachable." }); - setIsCheckingEndpoint(false); return; } - setIsCheckingEndpoint(false); } form = { @@ -385,14 +382,15 @@ const CreateMonitor = () => { - + Create monitor + diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx index 6bdbaacc9..798818d1f 100644 --- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { Box, Button, ButtonGroup, Stack, Typography } from "@mui/material"; +import LoadingButton from '@mui/lab/LoadingButton'; import { useSelector, useDispatch } from "react-redux"; import { monitorValidation } from "../../../Validation/validation"; import { useNavigate } from "react-router-dom"; @@ -19,6 +20,7 @@ import "./index.css"; const CreatePageSpeed = () => { const MS_PER_MINUTE = 60000; const { user, authToken } = useSelector((state) => state.auth); + const { isLoading } = useSelector((state) => state.uptimeMonitors); const dispatch = useDispatch(); const navigate = useNavigate(); const theme = useTheme(); @@ -40,7 +42,6 @@ const CreatePageSpeed = () => { }); const [https, setHttps] = useState(true); const [errors, setErrors] = useState({}); - const [isCheckingEndpoint, setIsCheckingEndpoint] = useState(false); const handleChange = (event, name) => { const { value, id } = event.target; @@ -93,7 +94,6 @@ const CreatePageSpeed = () => { const handleCreateMonitor = async (event) => { event.preventDefault(); - setIsCheckingEndpoint(true); //obj to submit let form = { url: `http${https ? "s" : ""}://` + monitor.url, @@ -120,10 +120,8 @@ const CreatePageSpeed = () => { if (checkEndpointAction.meta.requestStatus === "rejected") { createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." }); setErrors({ url: "The entered URL is not reachable." }); - setIsCheckingEndpoint(false); return; } - setIsCheckingEndpoint(false); form = { ...form, @@ -341,14 +339,15 @@ const CreatePageSpeed = () => { - + Create monitor + From 64d2d9b3ce5c3eb057e775f5d756b323c54c455a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 18 Oct 2024 14:21:22 +0800 Subject: [PATCH 22/37] Move opeartion back to --- Server/service/networkService.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Server/service/networkService.js b/Server/service/networkService.js index 9c884a369..ad372dbce 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -138,10 +138,13 @@ class NetworkService { async handlePing(job) { let isAlive; + const operation = async () => { + const response = await this.ping.promise.probe(job.data.url); + return response; + }; + try { - const { responseTime, response } = await this.measureResponseTime( - this.ping.promise.probe(job.data.url) - ); + const { responseTime, response } = await this.measureResponseTime(operation); isAlive = response.alive; const checkData = { monitorId: job.data._id, @@ -206,6 +209,7 @@ class NetworkService { responseTime: error.responseTime, message, }; + await this.logAndStoreCheck(checkData, this.db.createCheck); } finally { this.handleStatusUpdate(job, isAlive); } From 849b8a1e9bf11d951b549982bee67f3427a63fd4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 18 Oct 2024 14:23:21 +0800 Subject: [PATCH 23/37] Add tests for networkService --- Server/tests/services/networkService.test.js | 750 +++++++++++++++++++ 1 file changed, 750 insertions(+) create mode 100644 Server/tests/services/networkService.test.js diff --git a/Server/tests/services/networkService.test.js b/Server/tests/services/networkService.test.js new file mode 100644 index 000000000..619fa0fc9 --- /dev/null +++ b/Server/tests/services/networkService.test.js @@ -0,0 +1,750 @@ +import sinon from "sinon"; +import NetworkService from "../../service/networkService.js"; +import { errorMessages, successMessages } from "../../utils/messages.js"; +import exp from "constants"; +describe("NetworkService - Constructor", function () { + let dbMock, emailServiceMock, axiosMock, pingMock, loggerMock, httpMock; + + beforeEach(function () { + dbMock = sinon.stub(); + emailServiceMock = sinon.stub(); + axiosMock = sinon.stub(); + pingMock = sinon.stub(); + loggerMock = sinon.stub(); + httpMock = sinon.stub(); + }); + + it("should correctly initialize properties", function () { + const networkService = new NetworkService( + dbMock, + emailServiceMock, + axiosMock, + pingMock, + loggerMock, + httpMock + ); + + expect(networkService.db).to.equal(dbMock); + expect(networkService.emailService).to.equal(emailServiceMock); + expect(networkService.TYPE_PING).to.equal("ping"); + expect(networkService.TYPE_HTTP).to.equal("http"); + expect(networkService.TYPE_PAGESPEED).to.equal("pagespeed"); + expect(networkService.SERVICE_NAME).to.equal("NetworkService"); + expect(networkService.NETWORK_ERROR).to.equal(5000); + expect(networkService.axios).to.equal(axiosMock); + expect(networkService.ping).to.equal(pingMock); + expect(networkService.logger).to.equal(loggerMock); + expect(networkService.http).to.equal(httpMock); + }); +}); + +describe("NetworkService - handleNotification", () => { + let dbMock, emailServiceMock, loggerMock, networkService; + + beforeEach(function () { + dbMock = { + getNotificationsByMonitorId: sinon.stub(), + }; + emailServiceMock = { + buildAndSendEmail: sinon.stub().resolves(), + }; + loggerMock = { + error: sinon.stub(), + }; + + networkService = new NetworkService( + dbMock, + emailServiceMock, + null, + null, + loggerMock, + null + ); + }); + + it("should send email notifications when monitor is down", async function () { + const monitor = { _id: "monitor1", name: "Test Monitor", url: "http://test.com" }; + const notifications = [{ type: "email", address: "test@example.com" }]; + dbMock.getNotificationsByMonitorId.resolves(notifications); + + await networkService.handleNotification(monitor, false); + + expect(emailServiceMock.buildAndSendEmail.calledOnce).to.be.true; + expect( + emailServiceMock.buildAndSendEmail.calledWith( + "serverIsDownTemplate", + { monitorName: monitor.name, monitorUrl: monitor.url }, + "test@example.com", + "Monitor Test Monitor is down" + ) + ).to.be.true; + }); + + it("should send email notifications when monitor is up", async function () { + const monitor = { _id: "monitor1", name: "Test Monitor", url: "http://test.com" }; + const notifications = [{ type: "email", address: "test@example.com" }]; + dbMock.getNotificationsByMonitorId.resolves(notifications); + + await networkService.handleNotification(monitor, true); + + expect(emailServiceMock.buildAndSendEmail.calledOnce).to.be.true; + expect( + emailServiceMock.buildAndSendEmail.calledWith( + "serverIsUpTemplate", + { monitorName: monitor.name, monitorUrl: monitor.url }, + "test@example.com", + "Monitor Test Monitor is up" + ) + ).to.be.true; + }); + + it("should log an error if an exception is thrown", async function () { + const monitor = { _id: "monitor1", name: "Test Monitor", url: "http://test.com" }; + dbMock.getNotificationsByMonitorId.rejects(new Error("Database error")); + + await networkService.handleNotification(monitor, true); + + expect(loggerMock.error.calledOnce).to.be.true; + expect( + loggerMock.error.calledWith("Database error", { + method: "handleNotification", + service: networkService.SERVICE_NAME, + monitorId: monitor._id, + }) + ).to.be.true; + }); +}); + +describe("NetworkService - handleStatusUpdate", function () { + let dbMock, + emailServiceMock, + axiosMock, + pingMock, + loggerMock, + httpMock, + networkService, + monitorMock; + + beforeEach(function () { + dbMock = { getMonitorById: sinon.stub() }; + emailServiceMock = sinon.stub(); + axiosMock = sinon.stub(); + pingMock = sinon.stub(); + loggerMock = { error: sinon.stub() }; + httpMock = sinon.stub(); + networkService = new NetworkService( + dbMock, + emailServiceMock, + axiosMock, + pingMock, + loggerMock, + httpMock + ); + monitorMock = { + status: sinon.stub(), + save: sinon.stub(), + }; + }); + + afterEach(function () { + sinon.restore(); + }); + + it("should return if getMonitorById throws an error", async function () { + dbMock.getMonitorById.throws(new Error("Database error")); + + const res = await networkService.handleStatusUpdate( + { data: { _id: "monitor1" } }, + true + ); + expect(res).to.be.undefined; + }); + + it("should log an error if monitor is null", async function () { + dbMock.getMonitorById.resolves(null); + + await networkService.handleStatusUpdate({ data: { _id: "monitor1" } }, true); + + expect(loggerMock.error.calledOnce).to.be.true; + expect(loggerMock.error.calledWith("Null Monitor: monitor1")).to.be.true; + }); + + it("should update monitor status if different", async function () { + monitorMock.status = false; + dbMock.getMonitorById.resolves(monitorMock); + + await networkService.handleStatusUpdate({ data: { _id: "monitor1" } }, true); + + expect(monitorMock.save.calledOnce).to.be.true; + expect(monitorMock.status).to.be.true; + }); + + it("should not update monitor status if same", async function () { + monitorMock.status = true; + dbMock.getMonitorById.resolves(monitorMock); + + await networkService.handleStatusUpdate({ data: { _id: "monitor1" } }, true); + + expect(monitorMock.save.called).to.be.false; + }); + + it("should log an error if exception is thrown during status update", async function () { + monitorMock.status = false; + dbMock.getMonitorById.resolves(monitorMock); + monitorMock.save.rejects(new Error("Save error")); + + await networkService.handleStatusUpdate({ data: { _id: "monitor1" } }, true); + + expect(loggerMock.error.calledOnce).to.be.true; + expect(loggerMock.error.calledWith("Save error")).to.be.true; + }); +}); + +describe("NetworkService - measureResponseTime", () => { + let mockOperation, + dbMock, + emailServiceMock, + axiosMock, + pingMock, + loggerMock, + httpMock, + networkService; + + beforeEach(() => { + mockOperation = sinon.stub(); + dbMock = { getMonitorById: sinon.stub() }; + emailServiceMock = sinon.stub(); + axiosMock = sinon.stub(); + pingMock = sinon.stub(); + loggerMock = { error: sinon.stub() }; + httpMock = sinon.stub(); + networkService = new NetworkService( + dbMock, + emailServiceMock, + axiosMock, + pingMock, + loggerMock, + httpMock + ); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return response time and response on successful operation", async () => { + const fakeResponse = { data: "test" }; + mockOperation.resolves(fakeResponse); + + const result = await networkService.measureResponseTime(mockOperation); + expect(result).to.have.property("responseTime").that.is.a("number"); + expect(result).to.have.property("response").that.deep.equals(fakeResponse); + }); + + it("should throw an error with response time and additional properties on operation failure", async () => { + const fakeError = new Error("Operation failed"); + mockOperation.rejects(fakeError); + + try { + await networkService.measureResponseTime(mockOperation); + } catch (error) { + expect(error).to.have.property("responseTime").that.is.a("number"); + expect(error).to.have.property("service", networkService.SERVICE_NAME); + expect(error).to.have.property("method", "measureResponseTime"); + expect(error.message).to.equal("Operation failed"); + } + }); + it("should throw an error with response time set properties on operation failure", async () => { + const fakeError = new Error("Operation failed"); + fakeError.service = "test"; + fakeError.method = "testMethod"; + mockOperation.rejects(fakeError); + + try { + await networkService.measureResponseTime(mockOperation); + } catch (error) { + expect(error).to.have.property("responseTime").that.is.a("number"); + expect(error).to.have.property("service", "test"); + expect(error).to.have.property("method", "testMethod"); + expect(error.message).to.equal("Operation failed"); + } + }); +}); + +describe("networkService - handlePing", () => { + let job, + dbMock, + pingMock, + loggerMock, + httpMock, + networkService, + logAndStoreCheckStub, + handleStatusUpdateStub; + beforeEach(function () { + job = { + data: { + url: "http://example.com", + _id: "12345", + }, + }; + + dbMock = { getMonitorById: sinon.stub() }; + pingMock = { promise: { probe: sinon.stub() } }; + loggerMock = { error: sinon.stub() }; + httpMock = sinon.stub(); + networkService = new NetworkService( + dbMock, + null, + null, + pingMock, + loggerMock, + httpMock + ); + logAndStoreCheckStub = sinon.stub(networkService, "logAndStoreCheck"); + handleStatusUpdateStub = sinon.stub(networkService, "handleStatusUpdate"); + }); + + afterEach(function () { + sinon.restore(); + }); + + it("should handle a successful ping response", async function () { + const response = { alive: true }; + const responseTime = 0; + pingMock.promise.probe.resolves(response); + logAndStoreCheckStub.resolves(); + await networkService.handlePing(job); + expect( + logAndStoreCheckStub.calledOnceWith( + { + monitorId: job.data._id, + status: true, + responseTime, + message: successMessages.PING_SUCCESS, + }, + networkService.db.createCheck + ) + ).to.be.true; + expect(handleStatusUpdateStub.calledOnceWith(job, true)).to.be.true; + }); + it("should handle a successful ping response and isAlive === false", async function () { + const response = { alive: false }; + const responseTime = 0; + pingMock.promise.probe.resolves(response); + logAndStoreCheckStub.resolves(); + + await networkService.handlePing(job); + console.log(logAndStoreCheckStub.getCall(0).args[0]); + expect( + logAndStoreCheckStub.calledOnceWith( + { + monitorId: job.data._id, + status: false, + responseTime, + message: errorMessages.PING_CANNOT_RESOLVE, + }, + networkService.db.createCheck + ) + ).to.be.true; + expect(handleStatusUpdateStub.calledOnceWith(job, false)).to.be.true; + }); + + it("should handle a failed ping response", async function () { + const error = new Error("Ping failed"); + error.responseTime = 0; + + pingMock.promise.probe.rejects(error); + + await networkService.handlePing(job); + + expect(handleStatusUpdateStub.calledOnceWith(job, false)).to.be.true; + }); +}); + +describe("NetworkService - handleHttp", () => { + let networkService, + axiosMock, + dbMock, + httpMock, + jobMock, + loggerMock, + logAndStoreCheckStub, + handleStatusUpdateStub; + + beforeEach(() => { + axiosMock = { + get: sinon.stub(), + }; + dbMock = { + createCheck: sinon.stub().resolves(), + }; + httpMock = { + STATUS_CODES: { + 200: "OK", + 500: "Internal Server Error", + }, + }; + loggerMock = { + error: sinon.stub(), + }; + networkService = new NetworkService( + dbMock, + null, + axiosMock, + null, + loggerMock, + httpMock + ); + jobMock = { + data: { + url: "http://example.com", + _id: "monitorId123", + }, + }; + sinon.stub(networkService, "measureResponseTime").callsFake(async (operation) => { + const response = await operation(); + return { responseTime: 100, response }; + }); + logAndStoreCheckStub = sinon.stub(networkService, "logAndStoreCheck").resolves(); + handleStatusUpdateStub = sinon.stub(networkService, "handleStatusUpdate").resolves(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should handle a successful HTTP response", async () => { + const responseMock = { status: 200 }; + axiosMock.get.resolves(responseMock); + + await networkService.handleHttp(jobMock); + + expect(networkService.logAndStoreCheck.calledOnce).to.be.true; + const checkData = networkService.logAndStoreCheck.getCall(0).args[0]; + expect(checkData).to.include({ + monitorId: jobMock.data._id, + status: true, + statusCode: 200, + message: "OK", + }); + expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, true)).to.be.true; + }); + + it("should handle an HTTP error response", async () => { + const errorMock = { response: { status: 500 }, responseTime: 200 }; + axiosMock.get.rejects(errorMock); + + await networkService.handleHttp(jobMock); + + expect(networkService.logAndStoreCheck.calledOnce).to.be.true; + const checkData = networkService.logAndStoreCheck.getCall(0).args[0]; + expect(checkData).to.include({ + monitorId: jobMock.data._id, + status: false, + statusCode: 500, + message: "Internal Server Error", + }); + expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, false)).to.be.true; + }); + it("should handle an HTTP error response with undefined status", async () => { + const errorMock = { response: { status: undefined }, responseTime: 200 }; + axiosMock.get.rejects(errorMock); + + await networkService.handleHttp(jobMock); + + expect(networkService.logAndStoreCheck.calledOnce).to.be.true; + const checkData = networkService.logAndStoreCheck.getCall(0).args[0]; + expect(checkData).to.include({ + monitorId: jobMock.data._id, + status: false, + statusCode: 5000, + message: "Network Error", + }); + expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, false)).to.be.true; + }); +}); + +describe("networkService - handlePagespeed", () => { + let dbMock, + axiosMock, + jobMock, + emailServiceMock, + pingMock, + loggerMock, + httpMock, + networkService, + logAndStoreCheckStub, + handleStatusUpdateStub; + beforeEach(() => { + jobMock = { + data: { + _id: "12345", + url: "http://example.com", + }, + }; + dbMock = { getMonitorById: sinon.stub() }; + axiosMock = { get: sinon.stub() }; + + emailServiceMock = sinon.stub(); + pingMock = { promise: { probe: sinon.stub() } }; + loggerMock = { error: sinon.stub() }; + httpMock = { + STATUS_CODES: { + 200: "OK", + 500: "Internal Server Error", + }, + }; + networkService = new NetworkService( + dbMock, + emailServiceMock, + axiosMock, + pingMock, + loggerMock, + httpMock + ); + logAndStoreCheckStub = sinon.stub(networkService, "logAndStoreCheck").resolves(); + handleStatusUpdateStub = sinon.stub(networkService, "handleStatusUpdate").resolves(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should handle a successful PageSpeed response", async () => { + const responseMock = { + status: 200, + data: { + lighthouseResult: { + categories: { + accessibility: { score: 0.9 }, + "best-practices": { score: 0.8 }, + seo: { score: 0.7 }, + performance: { score: 0.6 }, + }, + audits: { + "cumulative-layout-shift": { score: 0.1 }, + "speed-index": { score: 0.2 }, + "first-contentful-paint": { score: 0.3 }, + "largest-contentful-paint": { score: 0.4 }, + "total-blocking-time": { score: 0.5 }, + }, + }, + }, + }; + axiosMock.get.resolves(responseMock); + + await networkService.handlePagespeed(jobMock); + expect(networkService.logAndStoreCheck.calledOnce).to.be.true; + const checkData = networkService.logAndStoreCheck.getCall(0).args[0]; + expect(checkData).to.include({ + monitorId: jobMock.data._id, + status: true, + statusCode: 200, + message: "OK", + accessibility: 90, + bestPractices: 80, + seo: 70, + performance: 60, + }); + expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, true)).to.be.true; + }); + + it("should handle a successful PageSpeed response with missing data", async () => { + const responseMock = { + status: 200, + data: { + lighthouseResult: { + categories: {}, + audits: { + "cumulative-layout-shift": { score: 0.1 }, + "speed-index": { score: 0.2 }, + "first-contentful-paint": { score: 0.3 }, + "largest-contentful-paint": { score: 0.4 }, + "total-blocking-time": { score: 0.5 }, + }, + }, + }, + }; + axiosMock.get.resolves(responseMock); + + await networkService.handlePagespeed(jobMock); + expect(networkService.logAndStoreCheck.calledOnce).to.be.true; + const checkData = networkService.logAndStoreCheck.getCall(0).args[0]; + expect(checkData).to.include({ + monitorId: jobMock.data._id, + status: true, + statusCode: 200, + message: "OK", + accessibility: 0, + bestPractices: 0, + seo: 0, + performance: 0, + }); + expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, true)).to.be.true; + }); + + it("should handle an error PageSpeed response", async () => { + const errorMock = { response: { status: 500 } }; + axiosMock.get.rejects(errorMock); + + await networkService.handlePagespeed(jobMock); + + expect(networkService.logAndStoreCheck.calledOnce).to.be.true; + const checkData = networkService.logAndStoreCheck.getCall(0).args[0]; + expect(checkData).to.include({ + monitorId: jobMock.data._id, + status: false, + statusCode: 500, + message: "Internal Server Error", + accessibility: 0, + bestPractices: 0, + seo: 0, + performance: 0, + }); + expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, false)).to.be.true; + }); + it("should handle an error PageSpeed response with unknown status", async () => { + const errorMock = { response: { status: undefined } }; + axiosMock.get.rejects(errorMock); + + await networkService.handlePagespeed(jobMock); + + expect(networkService.logAndStoreCheck.calledOnce).to.be.true; + const checkData = networkService.logAndStoreCheck.getCall(0).args[0]; + expect(checkData).to.include({ + monitorId: jobMock.data._id, + status: false, + statusCode: 5000, + message: "Network Error", + accessibility: 0, + bestPractices: 0, + seo: 0, + performance: 0, + }); + expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, false)).to.be.true; + }); +}); + +describe("NetworkService - getStatus", () => { + let dbMock, emailServiceMock, axiosMock, pingMock, loggerMock, httpMock, networkService; + + beforeEach(() => { + dbMock = { getMonitorById: sinon.stub() }; + emailServiceMock = sinon.stub(); + axiosMock = sinon.stub(); + pingMock = sinon.stub(); + loggerMock = { error: sinon.stub() }; + httpMock = sinon.stub(); + networkService = new NetworkService( + dbMock, + emailServiceMock, + axiosMock, + pingMock, + loggerMock, + httpMock + ); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return true if the job type is ping and handlePing is successful", async () => { + const job = { data: { type: networkService.TYPE_PING } }; + sinon.stub(networkService, "handlePing").resolves(true); + const result = await networkService.getStatus(job); + expect(result).to.be.true; + }); + it("should return false if the job type is ping and handlePing is not successful", async () => { + const job = { data: { type: networkService.TYPE_PING } }; + sinon.stub(networkService, "handlePing").resolves(false); + const result = await networkService.getStatus(job); + expect(result).to.be.false; + }); + it("should return true if the job type is http and handleHttp is successful", async () => { + const job = { data: { type: networkService.TYPE_HTTP } }; + sinon.stub(networkService, "handleHttp").resolves(true); + const result = await networkService.getStatus(job); + expect(result).to.be.true; + }); + it("should return false if the job type is http and handleHttp is not successful", async () => { + const job = { data: { type: networkService.TYPE_HTTP } }; + sinon.stub(networkService, "handleHttp").resolves(false); + const result = await networkService.getStatus(job); + expect(result).to.be.false; + }); + it("should return true if the job type is pagespeed and handlePagespeed is successful", async () => { + const job = { data: { type: networkService.TYPE_PAGESPEED } }; + sinon.stub(networkService, "handlePagespeed").resolves(true); + const result = await networkService.getStatus(job); + expect(result).to.be.true; + }); + it("should return false if the job type is pagespeed and handlePagespeed is not successful", async () => { + const job = { data: { type: networkService.TYPE_PAGESPEED } }; + sinon.stub(networkService, "handlePagespeed").resolves(false); + const result = await networkService.getStatus(job); + expect(result).to.be.false; + }); + it("should log an error and return false if the job type is unknown", async () => { + const job = { data: { type: "unknown" } }; + const result = await networkService.getStatus(job); + expect(result).to.be.false; + expect(loggerMock.error.calledOnce).to.be.true; + expect(loggerMock.error.calledWith(`Unsupported type: unknown`)).to.be.true; + }); +}); + +describe("NetworkService - logAndStoreCheck", async () => { + let dbMock, emailServiceMock, axiosMock, pingMock, loggerMock, httpMock, networkService; + + beforeEach(() => { + dbMock = { getMonitorById: sinon.stub() }; + emailServiceMock = sinon.stub(); + axiosMock = sinon.stub(); + pingMock = sinon.stub(); + loggerMock = { error: sinon.stub() }; + httpMock = sinon.stub(); + networkService = new NetworkService( + dbMock, + emailServiceMock, + axiosMock, + pingMock, + loggerMock, + httpMock + ); + }); + const data = { monitorId: "12345" }; + const writeToDB = sinon.stub(); + afterEach(() => { + sinon.restore(); + }); + + it("should log an error if `writeToDb` throws an error", async () => { + writeToDB.rejects(new Error("Database error")); + await networkService.logAndStoreCheck(data, writeToDB); + expect(loggerMock.error.calledOnce).to.be.true; + expect(loggerMock.error.calledWith(`Error writing check for ${data.monitorId}`)).to.be + .true; + }); + + it("should return the status of the inserted check if successful", async () => { + writeToDB.resolves({ status: true }); + const result = await networkService.logAndStoreCheck(data, writeToDB); + expect(result).to.be.true; + }); + + it("should thrown an error if the check is not inserted (null)", async () => { + writeToDB.resolves(null); + await networkService.logAndStoreCheck(data, writeToDB); + expect(loggerMock.error.calledOnce).to.be.true; + expect(loggerMock.error.calledWith(`Error writing check for ${data.monitorId}`)).to.be + .true; + }); + it("should thrown an error if the check is not inserted (undefined)", async () => { + writeToDB.resolves(undefined); + await networkService.logAndStoreCheck(data, writeToDB); + expect(loggerMock.error.calledOnce).to.be.true; + expect(loggerMock.error.calledWith(`Error writing check for ${data.monitorId}`)).to.be + .true; + }); +}); From 7255df0939d72cbcad90e226d725cc0f21a38808 Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Fri, 18 Oct 2024 20:33:23 -0400 Subject: [PATCH 24/37] Update README.md --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 06c248c4b..c0b2c41cf 100644 --- a/README.md +++ b/README.md @@ -44,13 +44,12 @@ We've just launched our [Discussions](https://github.com/bluewave-labs/bluewave- **Roadmap (short term):** -- [ ] Memory, disk and CPU monitoring -- [ ] 3rd party integrations -- [ ] DNS monitoring -- [ ] SSL monitoring +We are actively developing **infrastructure monitoring** features for Uptime Manager, which will include comprehensive monitoring of memory, disk usage, and CPU performance. Our goal is to build a lightweight agent that runs on Linux servers, continuously collecting and transmitting health metrics to Uptime Manager, where the data will be visualized for real-time insights. **Roadmap (long term):** +- [ ] DNS monitoring +- [ ] SSL monitoring - [ ] Status pages ## Tech stack From b6d98a9e40cf8d0a20415af45d1b1517edaf1a1f Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Fri, 18 Oct 2024 20:39:22 -0400 Subject: [PATCH 25/37] Fixes --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c0b2c41cf..aacaf4ea8 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ We are actively developing **infrastructure monitoring** features for Uptime Man **Roadmap (long term):** -- [ ] DNS monitoring -- [ ] SSL monitoring -- [ ] Status pages +- DNS monitoring +- SSL monitoring +- Status pages ## Tech stack From eccf4a8111fb49ec4c23f9d272eb10d437556604 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Sat, 19 Oct 2024 09:57:18 +0530 Subject: [PATCH 26/37] Change the loading state of page speed monitors from create monitor redux store to page speed monitor redux store --- Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx index 390fde36c..e0eed4472 100644 --- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx @@ -20,7 +20,7 @@ import "./index.css"; const CreatePageSpeed = () => { const MS_PER_MINUTE = 60000; const { user, authToken } = useSelector((state) => state.auth); - const { isLoading } = useSelector((state) => state.uptimeMonitors); + const { isLoading } = useSelector((state) => state.pageSpeedMonitors); const dispatch = useDispatch(); const navigate = useNavigate(); const theme = useTheme(); From 9c0a2b5f35968ee79bac4b24f8e8af99c0cc7a45 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Sat, 19 Oct 2024 11:05:36 +0530 Subject: [PATCH 27/37] Seperate check resolution for Page Speed Monitors --- .../PageSpeedMonitor/pageSpeedMonitorSlice.js | 43 ++++++++++++++++++- .../Pages/PageSpeed/CreatePageSpeed/index.jsx | 3 +- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js index 67d2c14f2..6cd91ac28 100644 --- a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js +++ b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js @@ -31,6 +31,30 @@ export const createPageSpeed = createAsyncThunk( } ); +export const checkEndpointResolution = createAsyncThunk( + "monitors/checkEndpoint", + async (data, thunkApi) => { + try { + const { authToken, monitorURL } = data; + + const res = await networkService.checkEndpointResolution({ + authToken: authToken, + monitorURL: monitorURL, + }) + return res.data; + } catch (error) { + if (error.response && error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); + } + } +) + export const getPagespeedMonitorById = createAsyncThunk( "monitors/getMonitorById", async (data, thunkApi) => { @@ -222,7 +246,24 @@ const pageSpeedMonitorSlice = createSlice({ ? action.payload.msg : "Failed to create page speed monitor"; }) - + // ***************************************************** + // Resolve Endpoint + // ***************************************************** + .addCase(checkEndpointResolution.pending, (state) => { + state.isLoading = true; + }) + .addCase(checkEndpointResolution.fulfilled, (state, action) => { + state.isLoading = false; + state.success = action.payload.success; + state.msg = action.payload.msg; + }) + .addCase(checkEndpointResolution.rejected, (state, action) => { + state.isLoading = false; + state.success = false; + state.msg = action.payload + ? action.payload.msg + : "Failed to check endpoint resolution"; + }) // ***************************************************** // Update Monitor // ***************************************************** diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx index e0eed4472..162b231a2 100644 --- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx @@ -5,8 +5,7 @@ import { useSelector, useDispatch } from "react-redux"; import { monitorValidation } from "../../../Validation/validation"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; -import { createPageSpeed } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; -import { checkEndpointResolution } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice" +import { createPageSpeed, checkEndpointResolution } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; import { ConfigBox } from "../../Monitors/styled"; From cbd3aed290ee06fa0dc9ea76f15d57ee2303e212 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sat, 19 Oct 2024 16:38:18 +0800 Subject: [PATCH 28/37] Remove log statements from --- Server/tests/utils/dataUtils.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/Server/tests/utils/dataUtils.test.js b/Server/tests/utils/dataUtils.test.js index 4af193d74..293291b5e 100644 --- a/Server/tests/utils/dataUtils.test.js +++ b/Server/tests/utils/dataUtils.test.js @@ -62,7 +62,6 @@ describe("calculatePercentile", () => { const percentile = 100; const result = calculatePercentile(checks, percentile); const expected = 50; - console.log(result); expect(result).to.equal(expected); }); }); From 0058ba219e60a3e26fea07f7be7a2e625d5970c7 Mon Sep 17 00:00:00 2001 From: abhayjoshi201 Date: Sat, 19 Oct 2024 14:28:58 +0530 Subject: [PATCH 29/37] Optimize Docker build script --- Docker/dev/build_images.sh | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/Docker/dev/build_images.sh b/Docker/dev/build_images.sh index 2de827dfe..1f9e86ba3 100755 --- a/Docker/dev/build_images.sh +++ b/Docker/dev/build_images.sh @@ -1,24 +1,25 @@ - #!/bin/bash # Change directory to root Server directory for correct Docker Context cd ../.. -#Client -client="./Docker/dev/client.Dockerfile" +# Define an array of services and their Dockerfiles +declare -A services=( + ["uptime_client"]="./Docker/dev/client.Dockerfile" + ["uptime_database_mongo"]="./Docker/dev/mongoDB.Dockerfile" + ["uptime_redis"]="./Docker/dev/redis.Dockerfile" + ["uptime_server"]="./Docker/dev/server.Dockerfile" +) -# MongoDB -mongoDB="./Docker/dev/mongoDB.Dockerfile" +# Loop through each service and build the corresponding image +for service in "${!services[@]}"; do + docker build -f "${services[$service]}" -t "$service" . + + # Check if the build succeeded + if [ $? -ne 0 ]; then + echo "Error building $service image. Exiting..." + exit 1 + fi +done -# Redis -redis="./Docker/dev/redis.Dockerfile" - -# Server -server="./Docker/dev/server.Dockerfile" - -docker build -f $client -t uptime_client . -docker build -f $mongoDB -t uptime_database_mongo . -docker build -f $redis -t uptime_redis . -docker build -f $server -t uptime_server . - -echo "All images built" \ No newline at end of file +echo "All images built successfully" From d28d049bcb25a361e06b1781998e4255d6ecd4e1 Mon Sep 17 00:00:00 2001 From: abhayjoshi201 Date: Sat, 19 Oct 2024 14:33:56 +0530 Subject: [PATCH 30/37] optimized-docker-build-script --- Docker/dev/build_images.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docker/dev/build_images.sh b/Docker/dev/build_images.sh index 1f9e86ba3..58bfc34f5 100755 --- a/Docker/dev/build_images.sh +++ b/Docker/dev/build_images.sh @@ -15,7 +15,7 @@ declare -A services=( for service in "${!services[@]}"; do docker build -f "${services[$service]}" -t "$service" . - # Check if the build succeeded + ## Check if the build succeeded if [ $? -ne 0 ]; then echo "Error building $service image. Exiting..." exit 1 From c8b8128aef7a9ce87c5e4759f14f235b4bb35258 Mon Sep 17 00:00:00 2001 From: abhayjoshi201 Date: Sat, 19 Oct 2024 15:18:00 +0530 Subject: [PATCH 31/37] made directory-independent --- Docker/dev/build_images.sh | 1 + git | 0 2 files changed, 1 insertion(+) create mode 100644 git diff --git a/Docker/dev/build_images.sh b/Docker/dev/build_images.sh index 58bfc34f5..f6171089c 100755 --- a/Docker/dev/build_images.sh +++ b/Docker/dev/build_images.sh @@ -1,6 +1,7 @@ #!/bin/bash # Change directory to root Server directory for correct Docker Context +cd "$(dirname "$0")" cd ../.. # Define an array of services and their Dockerfiles diff --git a/git b/git new file mode 100644 index 000000000..e69de29bb From dc1cc072dd44651549e5f6e6477f5e8d331a2c5e Mon Sep 17 00:00:00 2001 From: abhayjoshi201 Date: Sat, 19 Oct 2024 15:32:51 +0530 Subject: [PATCH 32/37] changes suggested by coderabbitai --- Server/controllers/authController.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 4ce5a0e2b..a01b4e78d 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -85,9 +85,7 @@ const registerUser = async (req, res, next) => { userId: newUser._id, }); - const userForToken = { ...newUser._doc }; - delete userForToken.profileImage; - delete userForToken.avatarImage; + const { password, avatarImage, ...userWithoutPassword } = user._doc; const appSettings = await req.settingsService.getSettings(); @@ -208,6 +206,7 @@ const editUser = async (req, res, next) => { try { // Change Password check + const { email } = jwt.verify(token, (await req.settingsService.getSettings()).jwtSecret); if (req.body.password && req.body.newPassword) { // Get token from headers const token = getTokenFromHeaders(req.headers); @@ -215,7 +214,17 @@ const editUser = async (req, res, next) => { const { jwtSecret } = req.settingsService.getSettings(); const { email } = jwt.verify(token, jwtSecret); // Add user email to body for DB operation + const monitors = teamMonitors?.monitors ?? []; + await Promise.all( + monitors.map(async (monitor) => { + await req.jobQueue.deleteJob(monitor); + await req.db.deleteChecks(monitor._id); + await req.db.deletePageSpeedChecksByMonitorId(monitor._id); + await req.db.deleteNotificationsByMonitorId(monitor._id); + }) + ); req.body.email = email; + // Get user const user = await req.db.getUserByEmail(email); // Compare passwords @@ -397,9 +406,6 @@ const deleteUser = async (req, res, next) => { try { const token = getTokenFromHeaders(req.headers); const decodedToken = jwt.decode(token); - const { email } = decodedToken; - - // Check if the user exists const user = await req.db.getUserByEmail(email); if (!user) { next(new Error(errorMessages.DB_USER_NOT_FOUND)); From 7b8ecbc84edc8938af44a2cdf63a10ced3d5ac81 Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Sat, 19 Oct 2024 22:55:15 -0400 Subject: [PATCH 33/37] Update pull_request_template.md --- .github/PULL_REQUEST_TEMPLATE/pull_request_template.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md index c884b2e12..b9700b622 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -1,9 +1,10 @@ ## Describe your changes -## Issue Number if applicable +## Issue number if applicable -## Checklist before requesting a review +## Fill the checklist before requesting a review - [ ] I have performed a self-review of my code -- [ ] I have inlcuded the issue # in the PR if applicable +- [ ] I have included the issue # in the PR (if applicable) - [ ] I have labelled the PR correctly +- [ ] My PR is granular and targeted to one specific feature only From f4d6903501bfd5420c823ec6c4d59838ee22aa72 Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Sat, 19 Oct 2024 23:06:33 -0400 Subject: [PATCH 34/37] More explanatory pull_request_template.md --- .../pull_request_template.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md index b9700b622..3d9b31e2d 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -1,10 +1,14 @@ ## Describe your changes -## Issue number if applicable +Provide a brief description of the changes you’ve made and their purpose. -## Fill the checklist before requesting a review +## Issue number -- [ ] I have performed a self-review of my code -- [ ] I have included the issue # in the PR (if applicable) -- [ ] I have labelled the PR correctly -- [ ] My PR is granular and targeted to one specific feature only +Mention the issue number(s) this PR addresses (e.g., #123). + +## Please ensure all items are checked off before requesting a review: + +- [ ] I have performed a self-review of my code. +- [ ] I have included the issue # in the PR. +- [ ] I have labelled the PR correctly. +- [ ] My PR is granular and targeted to one specific feature only. From 8d906913c675926fd2f7f605fbb22e34879e7733 Mon Sep 17 00:00:00 2001 From: abhayjoshi201 Date: Sun, 20 Oct 2024 12:54:46 +0530 Subject: [PATCH 35/37] reverted the authcontroler changes --- Server/controllers/authController.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index a01b4e78d..4ce5a0e2b 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -85,7 +85,9 @@ const registerUser = async (req, res, next) => { userId: newUser._id, }); - const { password, avatarImage, ...userWithoutPassword } = user._doc; + const userForToken = { ...newUser._doc }; + delete userForToken.profileImage; + delete userForToken.avatarImage; const appSettings = await req.settingsService.getSettings(); @@ -206,7 +208,6 @@ const editUser = async (req, res, next) => { try { // Change Password check - const { email } = jwt.verify(token, (await req.settingsService.getSettings()).jwtSecret); if (req.body.password && req.body.newPassword) { // Get token from headers const token = getTokenFromHeaders(req.headers); @@ -214,17 +215,7 @@ const editUser = async (req, res, next) => { const { jwtSecret } = req.settingsService.getSettings(); const { email } = jwt.verify(token, jwtSecret); // Add user email to body for DB operation - const monitors = teamMonitors?.monitors ?? []; - await Promise.all( - monitors.map(async (monitor) => { - await req.jobQueue.deleteJob(monitor); - await req.db.deleteChecks(monitor._id); - await req.db.deletePageSpeedChecksByMonitorId(monitor._id); - await req.db.deleteNotificationsByMonitorId(monitor._id); - }) - ); req.body.email = email; - // Get user const user = await req.db.getUserByEmail(email); // Compare passwords @@ -406,6 +397,9 @@ const deleteUser = async (req, res, next) => { try { const token = getTokenFromHeaders(req.headers); const decodedToken = jwt.decode(token); + const { email } = decodedToken; + + // Check if the user exists const user = await req.db.getUserByEmail(email); if (!user) { next(new Error(errorMessages.DB_USER_NOT_FOUND)); From 0dad5c282cd1c64731ae09855d36114776118e6b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 21 Oct 2024 10:07:02 +0800 Subject: [PATCH 36/37] Added missing var declaration --- Server/db/mongo/modules/checkModule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js index 09868dc43..c01dceb07 100644 --- a/Server/db/mongo/modules/checkModule.js +++ b/Server/db/mongo/modules/checkModule.js @@ -157,7 +157,7 @@ const getTeamChecks = async (req) => { // Default sort order is newest -> oldest sortOrder = sortOrder === "asc" ? 1 : -1; - checksQuery = { monitorId: { $in: userMonitors } }; + const checksQuery = { monitorId: { $in: userMonitors } }; if (filter !== undefined) { checksQuery.status = false; From a332caf3426179007304d1001d006c1db96e2305 Mon Sep 17 00:00:00 2001 From: Alexander Holliday Date: Mon, 21 Oct 2024 10:09:49 +0800 Subject: [PATCH 37/37] Delete empty git file --- git | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 git diff --git a/git b/git deleted file mode 100644 index e69de29bb..000000000