diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
index c884b2e12..3d9b31e2d 100644
--- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
+++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
@@ -1,9 +1,14 @@
## Describe your changes
-## Issue Number if applicable
+Provide a brief description of the changes you’ve made and their purpose.
-## Checklist before requesting a review
+## Issue number
-- [ ] I have performed a self-review of my code
-- [ ] I have inlcuded the issue # in the PR if applicable
-- [ ] I have labelled the PR correctly
+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.
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/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
index e761361f8..601dc016a 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/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 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 b5a6ff5dd..bfa62c7fa 100644
--- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx
+++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx
@@ -1,8 +1,10 @@
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";
+import { checkEndpointResolution } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"
import { useNavigate, useParams } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { createToast } from "../../../Utils/toastUtils";
@@ -17,12 +19,12 @@ import { getUptimeMonitorById } from "../../../Features/UptimeMonitors/uptimeMon
import "./index.css";
const CreateMonitor = () => {
- const MS_PER_MINUTE = 60000;
- const { user, authToken } = useSelector((state) => state.auth);
- const { monitors } = useSelector((state) => state.uptimeMonitors);
- const dispatch = useDispatch();
- const navigate = useNavigate();
- const theme = useTheme();
+ const MS_PER_MINUTE = 60000;
+ const { user, authToken } = useSelector((state) => state.auth);
+ const { monitors, isLoading } = useSelector((state) => state.uptimeMonitors);
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const theme = useTheme();
const idMap = {
"monitor-url": "url",
@@ -146,6 +148,17 @@ const CreateMonitor = () => {
setErrors(newErrors);
createToast({ body: "Error validation data." });
} else {
+ 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 = {
...form,
description: form.name,
@@ -373,18 +386,19 @@ const CreateMonitor = () => {
direction="row"
justifyContent="flex-end"
>
-
+
+ Create monitor
+
);
};
-export default CreateMonitor;
+export default CreateMonitor;
\ No newline at end of file
diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
index 40e42cf70..162b231a2 100644
--- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
+++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
@@ -1,10 +1,11 @@
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";
import { useTheme } from "@emotion/react";
-import { createPageSpeed } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
+import { createPageSpeed, checkEndpointResolution } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import { ConfigBox } from "../../Monitors/styled";
@@ -16,11 +17,12 @@ import Breadcrumbs from "../../../Components/Breadcrumbs";
import "./index.css";
const CreatePageSpeed = () => {
- const MS_PER_MINUTE = 60000;
- const { user, authToken } = useSelector((state) => state.auth);
- const dispatch = useDispatch();
- const navigate = useNavigate();
- const theme = useTheme();
+ const MS_PER_MINUTE = 60000;
+ const { user, authToken } = useSelector((state) => state.auth);
+ const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const theme = useTheme();
const idMap = {
"monitor-url": "url",
@@ -103,248 +105,252 @@ const CreatePageSpeed = () => {
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(createPageSpeed({ authToken, monitor: form }));
- if (action.meta.requestStatus === "fulfilled") {
- createToast({ body: "Monitor created successfully!" });
- navigate("/pagespeed");
- } else {
- createToast({ body: "Failed to create monitor." });
- }
- }
- };
+ if (error) {
+ const newErrors = {};
+ error.details.forEach((err) => {
+ newErrors[err.path[0]] = err.message;
+ });
+ 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;
+ }
- //select values
- const frequencies = [
- { _id: 3, name: "3 minutes" },
- { _id: 5, name: "5 minutes" },
- { _id: 10, name: "10 minutes" },
- { _id: 20, name: "20 minutes" },
- { _id: 60, name: "1 hour" },
- { _id: 1440, name: "1 day" },
- { _id: 10080, name: "1 week" },
- ];
- return (
-
-
-
-
-
- Create your{" "}
-
-
- pagespeed monitor
-
-
-
-
- General settings
-
- Here you can select the URL of the host, together with the type of monitor.
-
-
-
-
-
-
-
-
-
- Checks to perform
-
- You can always add or remove checks after adding your site.
-
-
-
-
- handleChange(event)}
- />
-
-
-
-
-
- {errors["type"] ? (
-
-
- {errors["type"]}
-
-
- ) : (
- ""
- )}
-
-
-
-
- Incident notifications
-
- When there is an incident, notify users.
-
-
-
- When there is a new incident,
- logger.warn("disabled")}
- isDisabled={true}
- />
- notification.type === "email"
- )}
- value={user?.email}
- onChange={(event) => handleChange(event)}
- />
- logger.warn("disabled")}
- isDisabled={true}
- />
- {monitor.notifications.some(
- (notification) => notification.type === "emails"
- ) ? (
-
- logger.warn("disabled")}
- />
-
- You can separate multiple emails with a comma
-
-
- ) : (
- ""
- )}
-
-
-
-
- Advanced settings
-
-
-
-
-
-
-
-
-
- );
+ form = {
+ ...form,
+ description: form.name,
+ teamId: user.teamId,
+ userId: user._id,
+ notifications: monitor.notifications,
+ };
+ const action = await dispatch(
+ createPageSpeed({ authToken, monitor: form })
+ );
+ if (action.meta.requestStatus === "fulfilled") {
+ createToast({ body: "Monitor created successfully!" });
+ navigate("/pagespeed");
+ } else {
+ createToast({ body: "Failed to create monitor." });
+ }
+ }
+ };
+
+ //select values
+ const frequencies = [
+ { _id: 3, name: "3 minutes" },
+ { _id: 5, name: "5 minutes" },
+ { _id: 10, name: "10 minutes" },
+ { _id: 20, name: "20 minutes" },
+ { _id: 60, name: "1 hour" },
+ { _id: 1440, name: "1 day" },
+ { _id: 10080, name: "1 week" },
+ ];
+ return (
+
+
+
+
+
+ Create your{" "}
+
+
+ pagespeed monitor
+
+
+
+
+ General settings
+
+ Here you can select the URL of the host, together with the type of
+ monitor.
+
+
+
+
+
+
+
+
+
+ Checks to perform
+
+ You can always add or remove checks after adding your site.
+
+
+
+
+ handleChange(event)}
+ />
+
+
+
+
+
+ {errors["type"] ? (
+
+
+ {errors["type"]}
+
+
+ ) : (
+ ""
+ )}
+
+
+
+
+ Incident notifications
+
+ When there is an incident, notify users.
+
+
+
+ When there is a new incident,
+ logger.warn("disabled")}
+ isDisabled={true}
+ />
+ notification.type === "email"
+ )}
+ value={user?.email}
+ onChange={(event) => handleChange(event)}
+ />
+ logger.warn("disabled")}
+ isDisabled={true}
+ />
+ {monitor.notifications.some(
+ (notification) => notification.type === "emails"
+ ) ? (
+
+ logger.warn("disabled")}
+ />
+
+ You can separate multiple emails with a comma
+
+
+ ) : (
+ ""
+ )}
+
+
+
+
+ Advanced settings
+
+
+
+
+
+
+ Create monitor
+
+
+
+
+ );
};
export default CreatePageSpeed;
diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js
index e000b2c08..5571d40ff 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;
@@ -88,22 +87,48 @@ 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);
- /**
- *
- * ************************************
- * Gets monitors and summary of stats by TeamID
- * ************************************
- *
- * @async
- * @param {Object} config - The configuration object.
- * @param {string} config.authToken - The authorization token to be used in the request header.
- * @param {string} config.teamId - Team ID
- * @param {Array} config.types - Array of monitor types
- * @returns {Promise} The response from the axios POST request.
- */
- async getMonitorsAndSummaryByTeamId(config) {
- const params = new URLSearchParams();
+ return this.axiosInstance.get(`/monitors/resolution/url?${params.toString()}`, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json",
+ }
+ })
+ }
+
+ /**
+ *
+ * ************************************
+ * Gets monitors and summary of stats by TeamID
+ * ************************************
+ *
+ * @async
+ * @param {Object} config - The configuration object.
+ * @param {string} config.authToken - The authorization token to be used in the request header.
+ * @param {string} config.teamId - Team ID
+ * @param {Array} config.types - Array of monitor types
+ * @returns {Promise} The response from the axios POST request.
+ */
+ async getMonitorsAndSummaryByTeamId(config) {
+ const params = new URLSearchParams();
if (config.types) {
config.types.forEach((type) => {
diff --git a/Docker/dev/build_images.sh b/Docker/dev/build_images.sh
index 2de827dfe..f6171089c 100755
--- a/Docker/dev/build_images.sh
+++ b/Docker/dev/build_images.sh
@@ -1,24 +1,26 @@
-
#!/bin/bash
# Change directory to root Server directory for correct Docker Context
+cd "$(dirname "$0")"
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"
diff --git a/README.md b/README.md
index 06c248c4b..aacaf4ea8 100644
--- a/README.md
+++ b/README.md
@@ -44,14 +44,13 @@ 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):**
-- [ ] Status pages
+- DNS monitoring
+- SSL monitoring
+- Status pages
## Tech stack
diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js
index 91c1e3f74..24761089f 100644
--- a/Server/controllers/monitorController.js
+++ b/Server/controllers/monitorController.js
@@ -3,6 +3,7 @@ import {
getMonitorByIdQueryValidation,
getMonitorsByTeamIdValidation,
createMonitorBodyValidation,
+ getMonitorURLByQueryValidation,
editMonitorBodyValidation,
getMonitorsAndSummaryByTeamIdParamValidation,
getMonitorsAndSummaryByTeamIdQueryValidation,
@@ -13,13 +14,14 @@ import {
getCertificateParamValidation,
} from "../validation/joi.js";
import sslChecker from "ssl-checker";
-
-const SERVICE_NAME = "monitorController";
import { errorMessages, successMessages } from "../utils/messages.js";
import jwt from "jsonwebtoken";
import { getTokenFromHeaders } from "../utils/utils.js";
import logger from "../utils/logger.js";
import { handleError, handleValidationError } from "./controllerUtils.js";
+import dns from "dns";
+
+const SERVICE_NAME = "monitorController";
/**
* Returns all monitors
@@ -257,6 +259,44 @@ const createMonitor = async (req, res, next) => {
}
};
+/**
+ * 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.
+ * @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 {
+ await getMonitorURLByQueryValidation.validateAsync(req.query);
+ } catch (error) {
+ next(handleValidationError(error, SERVICE_NAME));
+ return;
+ }
+
+ try {
+ let { monitorURL } = req.query;
+ monitorURL = new URL(monitorURL);
+ 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"));
+ }
+}
+
/**
* Deletes a monitor by its ID and also deletes associated checks, alerts, and notifications.
* @async
@@ -477,6 +517,7 @@ export {
getMonitorsAndSummaryByTeamId,
getMonitorsByTeamId,
createMonitor,
+ checkEndpointResolution,
deleteMonitor,
deleteAllMonitors,
editMonitor,
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;
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/openapi.json b/Server/openapi.json
index fd7eae94f..da5eed94f 100644
--- a/Server/openapi.json
+++ b/Server/openapi.json
@@ -22,12 +22,16 @@
"variables": {
"PORT": {
"description": "API Port",
- "enum": ["5000"],
+ "enum": [
+ "5000"
+ ],
"default": "5000"
},
"API_PATH": {
"description": "API Base Path",
- "enum": ["api/v1"],
+ "enum": [
+ "api/v1"
+ ],
"default": "api/v1"
}
}
@@ -38,7 +42,9 @@
"variables": {
"API_PATH": {
"description": "API Base Path",
- "enum": ["api/v1"],
+ "enum": [
+ "api/v1"
+ ],
"default": "api/v1"
}
}
@@ -49,12 +55,16 @@
"variables": {
"PORT": {
"description": "API Port",
- "enum": ["5000"],
+ "enum": [
+ "5000"
+ ],
"default": "5000"
},
"API_PATH": {
"description": "API Base Path",
- "enum": ["api/v1"],
+ "enum": [
+ "api/v1"
+ ],
"default": "api/v1"
}
}
@@ -89,7 +99,9 @@
"paths": {
"/auth/register": {
"post": {
- "tags": ["auth"],
+ "tags": [
+ "auth"
+ ],
"description": "Register a new user",
"requestBody": {
"content": {
@@ -125,8 +137,23 @@
},
"role": {
"type": "array",
- "enum": [["user"], ["admin"], ["superadmin"], ["Demo"]],
- "default": ["superadmin"]
+ "enum": [
+ [
+ "user"
+ ],
+ [
+ "admin"
+ ],
+ [
+ "superadmin"
+ ],
+ [
+ "Demo"
+ ]
+ ],
+ "default": [
+ "superadmin"
+ ]
},
"teamId": {
"type": "string",
@@ -173,14 +200,19 @@
},
"/auth/login": {
"post": {
- "tags": ["auth"],
+ "tags": [
+ "auth"
+ ],
"description": "Login with credentials",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
- "required": ["email", "password"],
+ "required": [
+ "email",
+ "password"
+ ],
"properties": {
"email": {
"type": "string",
@@ -231,7 +263,9 @@
},
"/auth/user/{userId}": {
"put": {
- "tags": ["auth"],
+ "tags": [
+ "auth"
+ ],
"description": "Change user information",
"parameters": [
{
@@ -292,7 +326,9 @@
]
},
"delete": {
- "tags": ["auth"],
+ "tags": [
+ "auth"
+ ],
"description": "Delete user",
"parameters": [
{
@@ -345,7 +381,9 @@
},
"/auth/users/superadmin": {
"get": {
- "tags": ["auth"],
+ "tags": [
+ "auth"
+ ],
"description": "Checks to see if an admin account exists",
"responses": {
"200": {
@@ -388,7 +426,9 @@
},
"/auth/users": {
"get": {
- "tags": ["auth"],
+ "tags": [
+ "auth"
+ ],
"description": "Get all users",
"responses": {
"200": {
@@ -431,14 +471,18 @@
},
"/auth/recovery/request": {
"post": {
- "tags": ["auth"],
+ "tags": [
+ "auth"
+ ],
"description": "Request a recovery token",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
- "required": ["email"],
+ "required": [
+ "email"
+ ],
"properties": {
"email": {
"type": "string",
@@ -485,14 +529,18 @@
},
"/auth/recovery/validate": {
"post": {
- "tags": ["auth"],
+ "tags": [
+ "auth"
+ ],
"description": "Validate recovery token",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
- "required": ["recoveryToken"],
+ "required": [
+ "recoveryToken"
+ ],
"properties": {
"recoveryToken": {
"type": "string"
@@ -538,14 +586,19 @@
},
"/auth/recovery/reset": {
"post": {
- "tags": ["auth"],
+ "tags": [
+ "auth"
+ ],
"description": "Password reset",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
- "required": ["recoveryToken", "password"],
+ "required": [
+ "recoveryToken",
+ "password"
+ ],
"properties": {
"recoveryToken": {
"type": "string"
@@ -594,14 +647,19 @@
},
"/invite": {
"post": {
- "tags": ["invite"],
+ "tags": [
+ "invite"
+ ],
"description": "Request an invitation",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
- "required": ["email", "role"],
+ "required": [
+ "email",
+ "role"
+ ],
"properties": {
"email": {
"type": "string"
@@ -655,14 +713,18 @@
},
"/invite/verify": {
"post": {
- "tags": ["invite"],
+ "tags": [
+ "invite"
+ ],
"description": "Request an invitation",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
- "required": ["token"],
+ "required": [
+ "token"
+ ],
"properties": {
"token": {
"type": "string"
@@ -713,7 +775,9 @@
},
"/monitors": {
"get": {
- "tags": ["monitors"],
+ "tags": [
+ "monitors"
+ ],
"description": "Get all monitors",
"responses": {
"200": {
@@ -754,7 +818,9 @@
]
},
"post": {
- "tags": ["monitors"],
+ "tags": [
+ "monitors"
+ ],
"description": "Create a new monitor",
"requestBody": {
"content": {
@@ -804,7 +870,9 @@
]
},
"delete": {
- "tags": ["monitors"],
+ "tags": [
+ "monitors"
+ ],
"description": "Delete all monitors",
"responses": {
"200": {
@@ -845,9 +913,78 @@
]
}
},
+ "/monitors/resolution/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"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "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"],
+ "tags": [
+ "monitors"
+ ],
"description": "Get monitor by id",
"parameters": [
{
@@ -898,7 +1035,9 @@
]
},
"put": {
- "tags": ["monitors"],
+ "tags": [
+ "monitors"
+ ],
"description": "Update monitor by id",
"parameters": [
{
@@ -958,7 +1097,9 @@
]
},
"delete": {
- "tags": ["monitors"],
+ "tags": [
+ "monitors"
+ ],
"description": "Delete monitor by id",
"parameters": [
{
@@ -1011,7 +1152,9 @@
},
"/monitors/stats/{monitorId}": {
"get": {
- "tags": ["monitors"],
+ "tags": [
+ "monitors"
+ ],
"description": "Get monitor stats",
"parameters": [
{
@@ -1064,7 +1207,9 @@
},
"/monitors/certificate/{monitorId}": {
"get": {
- "tags": ["monitors"],
+ "tags": [
+ "monitors"
+ ],
"description": "Get monitor certificate",
"parameters": [
{
@@ -1117,7 +1262,9 @@
},
"/monitors/team/summary/{teamId}": {
"get": {
- "tags": ["monitors"],
+ "tags": [
+ "monitors"
+ ],
"description": "Get monitors and summary by teamId",
"parameters": [
{
@@ -1134,7 +1281,11 @@
"required": false,
"schema": {
"type": "array",
- "enum": ["http", "ping", "pagespeed"]
+ "enum": [
+ "http",
+ "ping",
+ "pagespeed"
+ ]
}
}
],
@@ -1179,7 +1330,9 @@
},
"/monitors/team/{teamId}": {
"get": {
- "tags": ["monitors"],
+ "tags": [
+ "monitors"
+ ],
"description": "Get monitors by teamId",
"parameters": [
{
@@ -1206,7 +1359,10 @@
"required": false,
"schema": {
"type": "string",
- "enum": ["asc", "desc"]
+ "enum": [
+ "asc",
+ "desc"
+ ]
}
},
{
@@ -1225,7 +1381,11 @@
"required": false,
"schema": {
"type": "string",
- "enum": ["http", "ping", "pagespeed"]
+ "enum": [
+ "http",
+ "ping",
+ "pagespeed"
+ ]
}
},
{
@@ -1269,7 +1429,11 @@
"required": false,
"schema": {
"type": "string",
- "enum": ["http", "ping", "pagespeed"]
+ "enum": [
+ "http",
+ "ping",
+ "pagespeed"
+ ]
}
}
],
@@ -1314,7 +1478,9 @@
},
"/monitors/pause/{monitorId}": {
"post": {
- "tags": ["monitors"],
+ "tags": [
+ "monitors"
+ ],
"description": "Pause monitor",
"parameters": [
{
@@ -1367,7 +1533,9 @@
},
"/monitors/demo": {
"post": {
- "tags": ["monitors"],
+ "tags": [
+ "monitors"
+ ],
"description": "Create a demo monitor",
"requestBody": {
"content": {
@@ -1419,7 +1587,9 @@
},
"/checks/{monitorId}": {
"get": {
- "tags": ["checks"],
+ "tags": [
+ "checks"
+ ],
"description": "Get all checks for a monitor",
"parameters": [
{
@@ -1470,7 +1640,9 @@
]
},
"post": {
- "tags": ["checks"],
+ "tags": [
+ "checks"
+ ],
"description": "Create a new check",
"parameters": [
{
@@ -1530,7 +1702,9 @@
]
},
"delete": {
- "tags": ["checks"],
+ "tags": [
+ "checks"
+ ],
"description": "Delete all checks for a monitor",
"parameters": [
{
@@ -1583,7 +1757,9 @@
},
"/checks/team/{teamId}": {
"get": {
- "tags": ["checks"],
+ "tags": [
+ "checks"
+ ],
"description": "Get all checks for a team",
"parameters": [
{
@@ -1634,7 +1810,9 @@
]
},
"delete": {
- "tags": ["checks"],
+ "tags": [
+ "checks"
+ ],
"description": "Delete all checks for a team",
"parameters": [
{
@@ -1687,7 +1865,9 @@
},
"/checks/team/ttl": {
"put": {
- "tags": ["checks"],
+ "tags": [
+ "checks"
+ ],
"description": "Update check TTL",
"requestBody": {
"content": {
@@ -1739,7 +1919,9 @@
},
"/maintenance-window/monitor/{monitorId}": {
"get": {
- "tags": ["maintenance-window"],
+ "tags": [
+ "maintenance-window"
+ ],
"description": "Get maintenance window for monitor",
"parameters": [
{
@@ -1790,7 +1972,9 @@
]
},
"post": {
- "tags": ["maintenance-window"],
+ "tags": [
+ "maintenance-window"
+ ],
"description": "Create maintenance window for monitor",
"parameters": [
{
@@ -1852,7 +2036,9 @@
},
"/maintenance-window/user/{userId}": {
"get": {
- "tags": ["maintenance-window"],
+ "tags": [
+ "maintenance-window"
+ ],
"description": "Get maintenance window for user",
"parameters": [
{
@@ -1905,7 +2091,9 @@
},
"/queue/jobs": {
"get": {
- "tags": ["queue"],
+ "tags": [
+ "queue"
+ ],
"description": "Get all jobs in queue",
"responses": {
"200": {
@@ -1946,7 +2134,9 @@
]
},
"post": {
- "tags": ["queue"],
+ "tags": [
+ "queue"
+ ],
"description": "Create a new job. Useful for testing scaling workers",
"responses": {
"200": {
@@ -1989,7 +2179,9 @@
},
"/queue/metrics": {
"get": {
- "tags": ["queue"],
+ "tags": [
+ "queue"
+ ],
"description": "Get queue metrics",
"responses": {
"200": {
@@ -2032,7 +2224,9 @@
},
"/queue/obliterate": {
"post": {
- "tags": ["queue"],
+ "tags": [
+ "queue"
+ ],
"description": "Obliterate job queue",
"responses": {
"200": {
@@ -2074,7 +2268,6 @@
}
}
},
-
"components": {
"securitySchemes": {
"bearerAuth": {
@@ -2113,7 +2306,14 @@
},
"UserUpdateRequest": {
"type": "object",
- "required": ["firstName", "lastName", "email", "password", "role", "teamId"],
+ "required": [
+ "firstName",
+ "lastName",
+ "email",
+ "password",
+ "role",
+ "teamId"
+ ],
"properties": {
"firstName": {
"type": "string"
@@ -2135,8 +2335,23 @@
},
"role": {
"type": "array",
- "enum": [["user"], ["admin"], ["superadmin"], ["Demo"]],
- "default": ["superadmin"]
+ "enum": [
+ [
+ "user"
+ ],
+ [
+ "admin"
+ ],
+ [
+ "superadmin"
+ ],
+ [
+ "Demo"
+ ]
+ ],
+ "default": [
+ "superadmin"
+ ]
},
"deleteProfileImage": {
"type": "boolean"
@@ -2145,7 +2360,14 @@
},
"CreateMonitorBody": {
"type": "object",
- "required": ["userId", "teamId", "name", "description", "type", "url"],
+ "required": [
+ "userId",
+ "teamId",
+ "name",
+ "description",
+ "type",
+ "url"
+ ],
"properties": {
"_id": {
"type": "string"
@@ -2164,7 +2386,11 @@
},
"type": {
"type": "string",
- "enum": ["http", "ping", "pagespeed"]
+ "enum": [
+ "http",
+ "ping",
+ "pagespeed"
+ ]
},
"url": {
"type": "string"
@@ -2205,7 +2431,13 @@
},
"CreateCheckBody": {
"type": "object",
- "required": ["monitorId", "status", "responseTime", "statusCode", "message"],
+ "required": [
+ "monitorId",
+ "status",
+ "responseTime",
+ "statusCode",
+ "message"
+ ],
"properties": {
"monitorId": {
"type": "string"
@@ -2226,7 +2458,9 @@
},
"UpdateCheckTTLBody": {
"type": "object",
- "required": ["ttl"],
+ "required": [
+ "ttl"
+ ],
"properties": {
"ttl": {
"type": "integer"
@@ -2235,7 +2469,13 @@
},
"CreateMaintenanceWindowBody": {
"type": "object",
- "required": ["userId", "active", "oneTime", "start", "end"],
+ "required": [
+ "userId",
+ "active",
+ "oneTime",
+ "start",
+ "end"
+ ],
"properties": {
"userId": {
"type": "string"
@@ -2262,4 +2502,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js
index d6ba03ece..f306a5146 100644
--- a/Server/routes/monitorRoute.js
+++ b/Server/routes/monitorRoute.js
@@ -1,17 +1,18 @@
import { Router } from "express";
import {
- getAllMonitors,
- getMonitorStatsById,
- getMonitorCertificate,
- getMonitorById,
- getMonitorsAndSummaryByTeamId,
- getMonitorsByTeamId,
- createMonitor,
- deleteMonitor,
- deleteAllMonitors,
- editMonitor,
- pauseMonitor,
- addDemoMonitors,
+ getAllMonitors,
+ getMonitorStatsById,
+ getMonitorCertificate,
+ getMonitorById,
+ getMonitorsAndSummaryByTeamId,
+ getMonitorsByTeamId,
+ createMonitor,
+ checkEndpointResolution,
+ deleteMonitor,
+ deleteAllMonitors,
+ editMonitor,
+ pauseMonitor,
+ addDemoMonitors,
} from "../controllers/monitorController.js";
import { isAllowed } from "../middleware/isAllowed.js";
import { fetchMonitorCertificate } from "../controllers/controllerUtils.js";
@@ -21,15 +22,25 @@ const router = Router();
router.get("/", getAllMonitors);
router.get("/stats/:monitorId", getMonitorStatsById);
router.get("/certificate/:monitorId", (req, res, next) => {
- getMonitorCertificate(req, res, next, fetchMonitorCertificate);
+ getMonitorCertificate(req, res, next, fetchMonitorCertificate);
});
router.get("/:monitorId", getMonitorById);
router.get("/team/summary/:teamId", getMonitorsAndSummaryByTeamId);
router.get("/team/:teamId", getMonitorsByTeamId);
-router.post("/", isAllowed(["admin", "superadmin"]), createMonitor);
+router.get(
+ "/resolution/url",
+ isAllowed(["admin", "superadmin"]),
+ checkEndpointResolution
+)
-router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), deleteMonitor);
+router.delete(
+ "/:monitorId",
+ isAllowed(["admin", "superadmin"]),
+ deleteMonitor
+);
+
+router.post("/", isAllowed(["admin", "superadmin"]), createMonitor);
router.put("/:monitorId", isAllowed(["admin", "superadmin"]), editMonitor);
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 d2964bd25..ad372dbce 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";
@@ -39,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,
@@ -47,11 +63,19 @@ 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;
+ 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;
@@ -60,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,
@@ -77,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,
@@ -100,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;
}
@@ -112,13 +136,13 @@ class NetworkService {
* @returns {Promise<{boolean}} The result of logging and storing the check
*/
async handlePing(job) {
+ let isAlive;
+
const operation = async () => {
- const response = await ping.promise.probe(job.data.url);
+ const response = await this.ping.promise.probe(job.data.url);
return response;
};
- let isAlive;
-
try {
const { responseTime, response } = await this.measureResponseTime(operation);
isAlive = response.alive;
@@ -130,7 +154,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 = {
@@ -139,7 +163,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);
}
@@ -153,7 +177,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 +195,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);
+ 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,
@@ -185,8 +209,7 @@ class NetworkService {
responseTime: error.responseTime,
message,
};
-
- return await this.logAndStoreCheck(checkData, this.db.createCheck);
+ await this.logAndStoreCheck(checkData, this.db.createCheck);
} finally {
this.handleStatusUpdate(job, isAlive);
}
@@ -210,7 +233,8 @@ 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;
@@ -223,7 +247,6 @@ class NetworkService {
"largest-contentful-paint": lcp,
"total-blocking-time": tbt,
} = audits;
-
// Weights
// First Contentful Paint 10%
// Speed Index 10%
@@ -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,
@@ -249,12 +272,11 @@ class NetworkService {
tbt,
},
};
-
this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck);
} 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 +309,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,
@@ -311,8 +333,9 @@ class NetworkService {
if (insertedCheck !== null && insertedCheck !== undefined) {
return insertedCheck.status;
}
+ throw new Error();
} catch (error) {
- 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,
diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js
index b14626204..0a9dbafe6 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,
@@ -16,9 +17,7 @@ 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";
describe("Monitor Controller - getAllMonitors", () => {
@@ -460,6 +459,50 @@ describe("Monitor Controller - createMonitor", () => {
});
});
+describe("Monitor Controllor - checkEndpointResolution", () => {
+ 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 an error 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.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';
+ await checkEndpointResolution(req, res, next);
+ expect(next.calledOnce).to.be.true;
+ 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');
+ });
+});
+
describe("Monitor Controller - deleteMonitor", () => {
let req, res, next;
beforeEach(() => {
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;
+ });
+});
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);
});
});
diff --git a/Server/validation/joi.js b/Server/validation/joi.js
index ad5c480ae..d1ba107bc 100644
--- a/Server/validation/joi.js
+++ b/Server/validation/joi.js
@@ -236,6 +236,10 @@ const pauseMonitorParamValidation = joi.object({
monitorId: joi.string().required(),
});
+const getMonitorURLByQueryValidation = joi.object({
+ monitorURL: joi.string().uri().required(),
+});
+
//****************************************
// Alerts
//****************************************
@@ -446,6 +450,7 @@ export {
getCertificateParamValidation,
editMonitorBodyValidation,
pauseMonitorParamValidation,
+ getMonitorURLByQueryValidation,
editUserParamValidation,
editUserBodyValidation,
createAlertParamValidation,