From ca2c269047836324e42c0934253557993cb5fd24 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 25 Oct 2024 11:55:24 +0800 Subject: [PATCH 01/31] Add build-dev script for dev mode --- Client/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Client/package.json b/Client/package.json index 2aa5fb463..d7bd7aa9b 100644 --- a/Client/package.json +++ b/Client/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "vite build", + "build-dev": "vite build --mode development", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, From b33faa568c876fe039e35ecbef39c6730393dc4f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 25 Oct 2024 11:56:49 +0800 Subject: [PATCH 02/31] Update dev client Dockerfile to use dev mode --- Docker/dev/client.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docker/dev/client.Dockerfile b/Docker/dev/client.Dockerfile index a650e0f72..37b2bae74 100644 --- a/Docker/dev/client.Dockerfile +++ b/Docker/dev/client.Dockerfile @@ -8,7 +8,7 @@ RUN npm install COPY ../../Client . -RUN npm run build +RUN npm run build-dev RUN npm install -g serve From 327cb48fada02a78ac9ef35ce48b64acd5a9eeb0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 25 Oct 2024 11:57:21 +0800 Subject: [PATCH 03/31] Fix docker image names --- Docker/dist/build_images.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Docker/dist/build_images.sh b/Docker/dist/build_images.sh index a5ec3eec9..33af5efdf 100755 --- a/Docker/dist/build_images.sh +++ b/Docker/dist/build_images.sh @@ -6,10 +6,10 @@ cd ../.. # Define an array of services and their Dockerfiles declare -A services=( - ["bluewave/uptime_client"]="./Docker/dist/client.Dockerfile" - ["bluewave/database_mongo"]="./Docker/dist/mongoDB.Dockerfile" - ["bluewave/uptime_redis"]="./Docker/dist/redis.Dockerfile" - ["bluewave/uptime_server"]="./Docker/dist/server.Dockerfile" + ["bluewaveuptime/uptime_client"]="./Docker/dist/client.Dockerfile" + ["bluewaveuptime/uptime_database_mongo"]="./Docker/dist/mongoDB.Dockerfile" + ["bluewaveuptime/uptime_redis"]="./Docker/dist/redis.Dockerfile" + ["bluewaveuptime/uptime_server"]="./Docker/dist/server.Dockerfile" ) # Loop through each service and build the corresponding image From 7755dcd16a06e3bd2385b693943a0e61bf01912f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 25 Oct 2024 11:58:34 +0800 Subject: [PATCH 04/31] Add script to inject prefixed vars at runtime to dist client.Dockerfile --- Docker/dist/client.Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Docker/dist/client.Dockerfile b/Docker/dist/client.Dockerfile index 1edbfa522..3c4f577c0 100644 --- a/Docker/dist/client.Dockerfile +++ b/Docker/dist/client.Dockerfile @@ -15,5 +15,7 @@ FROM nginx:1.27.1-alpine COPY ./Docker/dist/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf COPY --from=build /app/dist /usr/share/nginx/html +COPY --from=build /app/env.sh /docker-entrypoint.d/env.sh +RUN chmod +x /docker-entrypoint.d/env.sh CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file From ff20373fb481d97bc7ee5ed33078175458834e88 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 25 Oct 2024 11:59:20 +0800 Subject: [PATCH 05/31] Add env var to dist docker compose file --- Docker/dist/docker-compose.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Docker/dist/docker-compose.yaml b/Docker/dist/docker-compose.yaml index 56a7a3c78..c62fb2ea6 100644 --- a/Docker/dist/docker-compose.yaml +++ b/Docker/dist/docker-compose.yaml @@ -1,6 +1,8 @@ services: client: image: bluewaveuptime/uptime_client:latest + environment: + UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1" ports: - "80:80" - "443:443" From dce50aee0fd24f4c32f7ad16ee7c38f739ea3328 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 25 Oct 2024 12:00:01 +0800 Subject: [PATCH 06/31] Add env.production file for var injection. This is OK to commit --- Client/.env.production | 1 + 1 file changed, 1 insertion(+) create mode 100644 Client/.env.production diff --git a/Client/.env.production b/Client/.env.production new file mode 100644 index 000000000..a5bce84e1 --- /dev/null +++ b/Client/.env.production @@ -0,0 +1 @@ +VITE_APP_API_BASE_URL=UPTIME_APP_API_BASE_URL From ed9b7dac8b74bbbcf549960ab2bb681f2d4c3b6c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 25 Oct 2024 12:01:39 +0800 Subject: [PATCH 07/31] Add var replacement script to prod client dockerfile --- Docker/prod/client.Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Docker/prod/client.Dockerfile b/Docker/prod/client.Dockerfile index cbdb75aa8..1619b5e41 100644 --- a/Docker/prod/client.Dockerfile +++ b/Docker/prod/client.Dockerfile @@ -13,5 +13,6 @@ RUN npm run build FROM nginx:1.27.1-alpine COPY --from=build /app/dist /usr/share/nginx/html - +COPY --from=build /app/env.sh /docker-entrypoint.d/env.sh +RUN chmod +x /docker-entrypoint.d/env.sh CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file From 3178af7d4912993b821455310df374339e28516e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 25 Oct 2024 12:03:38 +0800 Subject: [PATCH 08/31] Add env var to docker-compose file --- Docker/prod/docker-compose.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Docker/prod/docker-compose.yaml b/Docker/prod/docker-compose.yaml index fa29a0d4d..69202c8d7 100644 --- a/Docker/prod/docker-compose.yaml +++ b/Docker/prod/docker-compose.yaml @@ -1,6 +1,8 @@ services: client: image: uptime_client:latest + environment: + UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1" ports: - "80:80" - "443:443" From bad552974ebc825aea8b7d1a3db3300fe84e052d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 25 Oct 2024 12:10:19 +0800 Subject: [PATCH 09/31] Update quickstart directions --- guides/users-guide/quickstart.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/guides/users-guide/quickstart.md b/guides/users-guide/quickstart.md index 6bc6975c4..55e23f286 100644 --- a/guides/users-guide/quickstart.md +++ b/guides/users-guide/quickstart.md @@ -12,6 +12,15 @@ icon: sign-posts-wrench --- +## Quickstart for users (remote server) + +1. Download our [Docker compose file](https://github.com/bluewave-labs/bluewave-uptime/blob/develop/Docker/dist/docker-compose.yaml) +2. Edit the `UPTIME_APP_API_BASE_URL` variable in the docker-compose file to point to your remote server. +3. Run `docker compose up` to start the application +4. Now the application is running at `http://` + +--- + ## Quickstart for developers {% hint style="info" %} From d430f1d13ebcbc7e1dc1d7dd3fb0ebb0225b8a24 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 25 Oct 2024 12:21:55 +0800 Subject: [PATCH 10/31] Update gitignore to allow env.sh --- Client/.gitignore | 2 ++ Client/env.sh | 8 ++++++++ 2 files changed, 10 insertions(+) create mode 100755 Client/env.sh diff --git a/Client/.gitignore b/Client/.gitignore index 50c8dda2a..ebf01f87a 100644 --- a/Client/.gitignore +++ b/Client/.gitignore @@ -24,3 +24,5 @@ dist-ssr *.sw? .env + +!env.sh \ No newline at end of file diff --git a/Client/env.sh b/Client/env.sh new file mode 100755 index 000000000..d95f8ee8f --- /dev/null +++ b/Client/env.sh @@ -0,0 +1,8 @@ +#!/bin/sh +for i in $(env | grep UPTIME_APP_) +do + key=$(echo $i | cut -d '=' -f 1) + value=$(echo $i | cut -d '=' -f 2-) + echo $key=$value + find /usr/share/nginx/html -type f \( -name '*.js' -o -name '*.css' \) -exec sed -i "s|${key}|${value}|g" '{}' + +done \ No newline at end of file From 39802365803eb3cb7861f6a4343473c76eed0300 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 14:17:10 +0800 Subject: [PATCH 11/31] Remove empty notification validation --- 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 fece02926..0b85a4557 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -234,7 +234,7 @@ const editMonitorBodyValidation = joi.object({ name: joi.string(), description: joi.string(), interval: joi.number(), - notifications: joi.array().items(joi.object()).min(1), + notifications: joi.array().items(joi.object()), }); const pauseMonitorParamValidation = joi.object({ From b171b540b9b7e97ace9d2dbade3d57d38626c2d4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 14:17:38 +0800 Subject: [PATCH 12/31] remove empty notification tests --- Server/tests/controllers/monitorController.test.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index 8c36653ae..0d4deb742 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -419,12 +419,6 @@ describe("Monitor Controller - createMonitor", () => { expect(next.firstCall.args[0]).to.be.an("error"); expect(next.firstCall.args[0].message).to.equal("Job error"); }); - it("should fail validation with empty notifications", async () => { - req.body.notifications = []; - await createMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); it("should return success message and data if all operations succeed", async () => { const monitor = { _id: "123", save: sinon.stub() }; req.db.createMonitor.returns(monitor); @@ -790,12 +784,6 @@ describe("Monitor Controller - editMonitor", () => { expect(next.firstCall.args[0]).to.be.an("error"); expect(next.firstCall.args[0].message).to.equal("Add Job error"); }); - it("should fail validation with empty notifications", async () => { - req.body.notifications = []; - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); it("should return success message with data if all operations succeed", async () => { const monitor = { _id: "123" }; req.db.getMonitorById.returns({ teamId: "123" }); From 25b555f261ed13cc86535c2c157dcce86a7c8c60 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 14:18:03 +0800 Subject: [PATCH 13/31] Fix email templates --- Server/templates/serverIsDown.mjml | 116 +++++++++++------------------ Server/templates/serverIsUp.mjml | 112 ++++++++++------------------ 2 files changed, 85 insertions(+), 143 deletions(-) diff --git a/Server/templates/serverIsDown.mjml b/Server/templates/serverIsDown.mjml index ffe6ffc0e..c8782311e 100644 --- a/Server/templates/serverIsDown.mjml +++ b/Server/templates/serverIsDown.mjml @@ -1,73 +1,45 @@ - - - - - - - - - - - - - Message from BlueWave Uptime Service - - - - - Google.com is down - - - - - - - -

Hello {{name}}!

-

- We detected an incident on one of your monitors. Your service is currently - down. We'll send a message to you once it is up again. -

-

Monitor name: {{monitor}}

-

URL: {{url}}

-

Problem: {{problem}}

-

Start date: {{startDate}}

-
-
- - - View incident details - -

This email was sent by BlueWave Uptime.

-
-
-
-
-
+ + + + + + + + + + + + + Message from BlueWave Uptime Service + + + + + Google.com is down + + + + + + + +

Hello {{name}}!

+

+ We detected an incident on one of your monitors. Your service is currently + down. We'll send a message to you once it is up again. +

+

Monitor name: {{monitor}}

+

URL: {{url}}

+
+
+ + + View incident details + +

This email was sent by BlueWave Uptime.

+
+
+
+
+ \ No newline at end of file diff --git a/Server/templates/serverIsUp.mjml b/Server/templates/serverIsUp.mjml index 154ad44d9..32d578b19 100644 --- a/Server/templates/serverIsUp.mjml +++ b/Server/templates/serverIsUp.mjml @@ -1,72 +1,42 @@ - - - - - - - - - - - - - Message from BlueWave Uptime Service - - - - - {{monitor}} is up - - - - - - - -

Hello {{name}}!

-

Your latest incident is resolved and your monitored service is up again.

-

Monitor name: {{monitor}}

-

URL: {{url}}

-

Problem: {{problem}}

-

Start date: {{startDate}}

-

Resolved date: {{resolvedDate}}

-

Duration:{{duration}}

-
-
- - - View incident details - -

This email was sent by BlueWave Uptime.

-
-
-
-
-
+ + + + + + + + + + + + + Message from BlueWave Uptime Service + + + + + {{monitor}} is up + + + + + + + +

Hello {{name}}!

+

Your latest incident is resolved and your monitored service is up again.

+

Monitor name: {{monitor}}

+

URL: {{url}}

+
+
+ + + View incident details + +

This email was sent by BlueWave Uptime.

+
+
+
+
+ \ No newline at end of file From cb5c7b2c73f414bad3b0caf73f6ffe9ab20b47e7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 14:18:27 +0800 Subject: [PATCH 14/31] Refactor network service --- Server/service/networkService.js | 585 ++++++++----------------------- 1 file changed, 150 insertions(+), 435 deletions(-) diff --git a/Server/service/networkService.js b/Server/service/networkService.js index 157a29006..efdd2e865 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -1,32 +1,22 @@ import { errorMessages, successMessages } from "../utils/messages.js"; /** - * NetworkService + * Constructs a new NetworkService instance. * - * This service handles all network requests on the back end - * This includes pings, http requests, and pagespeed checks + * @param {Object} axios - The axios instance for HTTP requests. + * @param {Object} ping - The ping utility for network checks. + * @param {Object} logger - The logger instance for logging. + * @param {Object} http - The HTTP utility for network operations. */ - class NetworkService { - /** - * 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; + constructor(axios, ping, logger, http) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; this.TYPE_PAGESPEED = "pagespeed"; this.TYPE_HARDWARE = "hardware"; this.SERVICE_NAME = "NetworkService"; this.NETWORK_ERROR = 5000; + this.PING_ERROR = 5001; this.axios = axios; this.ping = ping; this.logger = logger; @@ -34,459 +24,184 @@ class NetworkService { } /** - * Handles notifications for a given monitor configuration. + * Times the execution of an asynchronous operation. * - * @param {Object} config - The configuration object for the notification. - * @param {Object} config.monitor - The monitor object containing the monitor ID. - * @param {string} config.template - The email template to be used. - * @param {Object} config.context - The context for the email template. - * @param {string} config.subject - The subject of the email. + * @param {Function} operation - The asynchronous operation to be timed. + * @returns {Promise} An object containing the response, response time, and optionally an error. + * @property {Object|null} response - The response from the operation, or null if an error occurred. + * @property {number} responseTime - The time taken for the operation to complete, in milliseconds. + * @property {Error} [error] - The error object if an error occurred during the operation. */ - async handleNotification(config) { - try { - const notifications = await this.db.getNotificationsByMonitorId(config.monitor._id); - for (const notification of notifications) { - if (notification.type === "email") { - await this.emailService.buildAndSendEmail( - config.template, - config.context, - notification.address, - config.subject - ); - } - } - } catch (error) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "handleNotification", - details: `notification error for monitor: ${monitor._id}`, - stack: error.stack, - }); - } - } - - /** - * 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). - * @param {Object} [hardwareData] - The hardware data for the monitor. - * @returns {Promise} - */ - async handleStatusUpdate(job, isAlive, hardwareData) { - let monitor; - const { _id } = job.data; - - // Look up the monitor, if it doesn't exist, it's probably been removed, return - try { - monitor = await this.db.getMonitorById(_id); - } catch (error) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "handleStatusUpdate", - stack: error.stack, - details: `monitor lookup error for monitor: ${_id}`, - }); - return; - } - - // Otherwise, try to update monitor status - try { - if (monitor.status === undefined || monitor.status !== isAlive) { - const oldStatus = monitor.status; - monitor.status = isAlive; - await monitor.save(); - - if (oldStatus !== undefined && oldStatus !== isAlive) { - const config = { - monitor: monitor, - template: isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate", - context: { monitorName: monitor.name, monitorUrl: monitor.url }, - subject: `Monitor ${monitor.name} is ${isAlive === true ? "up" : "down"}`, - }; - this.handleNotification(config); - } - } - if (monitor.type === this.TYPE_HARDWARE) { - this.handleHardwareUpdate(monitor, hardwareData); - } - } catch (error) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "handleStatusUpdate", - stack: error.stack, - details: `status update error for monitor: ${_id}`, - }); - } - } - - async handleHardwareUpdate(monitor, hardwareData) { - //Get Thresholds - const thresholds = monitor?.thresholds; - if (thresholds === undefined) { - return; - } - //Get Values - const cpuUsage = hardwareData?.cpu?.usage_percent; - const memoryUsage = hardwareData?.memory?.usage_percent; - const diskUsage = hardwareData?.disk[0]?.usage_percent; - - // Return early if no values - if (cpuUsage === undefined && memoryUsage === undefined && diskUsage === undefined) { - return; - } - - // Return early if all values are below thresholds - if ( - cpuUsage < thresholds.usage_cpu && - memoryUsage < thresholds.usage_memory && - diskUsage < thresholds.usage_disk - ) { - return; - } - - let context = { - message: "Threshold(s) exceeded:", - }; - if (cpuUsage > thresholds.usage_cpu) { - context.cpu = `CPU USAGE: ${cpuUsage * 100}% > ${thresholds.usage_cpu * 100}%`; - } - - if (memoryUsage > thresholds.usage_memory) { - context.memory = `MEMORY USAGE: ${memoryUsage * 100}% > ${thresholds.usage_memory * 100}%`; - } - - if (diskUsage > thresholds.usage_disk) { - context.disk = `DISK USAGE: ${diskUsage * 100}% > ${thresholds.usage_disk * 100}%`; - } - - const config = { - monitor: monitor, - template: "thresholdViolatedTemplate", - context: context, - subject: `Threshold Violated for ${monitor.name}`, - }; - this.handleNotification(config); - } - - /** - * Measures the response time of an asynchronous operation. - * @param {Function} operation - An asynchronous operation to measure. - * @returns {Promise<{responseTime: number, response: any}>} An object containing the response time in milliseconds and the response from the operation. - * @throws {Error} The error object from the operation, contains response time. - */ - async measureResponseTime(operation) { + async timeRequest(operation) { const startTime = Date.now(); try { const response = await operation(); const endTime = Date.now(); - return { responseTime: endTime - startTime, response }; + const responseTime = endTime - startTime; + return { response, responseTime }; } catch (error) { const endTime = Date.now(); - error.responseTime = endTime - startTime; - error.service === undefined ? (error.service = this.SERVICE_NAME) : null; - error.method === undefined ? (error.method = "measureResponseTime") : null; - throw error; + const responseTime = endTime - startTime; + return { response: null, responseTime, error }; } } /** - * Handles the ping operation for a given job, measures its response time, and logs the result. - * @param {Object} job - The job object containing data for the ping operation. - * @returns {Promise<{boolean}} The result of logging and storing the check + * Sends a ping request to the specified URL and returns the response. + * + * @param {Object} job - The job object containing the data for the ping request. + * @param {Object} job.data - The data object within the job. + * @param {string} job.data.url - The URL to ping. + * @param {string} job.data._id - The monitor ID for the ping request. + * @returns {Promise} An object containing the ping response details. + * @property {string} monitorId - The monitor ID for the ping request. + * @property {string} type - The type of request, which is "ping". + * @property {number} responseTime - The time taken for the ping request to complete, in milliseconds. + * @property {Object} payload - The response payload from the ping request. + * @property {boolean} status - The status of the ping request (true if successful, false otherwise). + * @property {number} code - The response code (200 if successful, error code otherwise). + * @property {string} message - The message indicating the result of the ping request. */ - 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(operation); - isAlive = response.alive; - const checkData = { - monitorId: job.data._id, - status: isAlive, - responseTime, - message: isAlive - ? successMessages.PING_SUCCESS - : errorMessages.PING_CANNOT_RESOLVE, - }; - await this.logAndStoreCheck(checkData, this.db.createCheck); - } catch (error) { - isAlive = false; - const checkData = { - monitorId: job.data._id, - status: isAlive, - message: errorMessages.PING_CANNOT_RESOLVE, - responseTime: error.responseTime, - }; - await this.logAndStoreCheck(checkData, this.db.createCheck); - } finally { - this.handleStatusUpdate(job, isAlive); - } - } - - /** - * Handles the http operation for a given job, measures its response time, and logs the result. - * @param {Object} job - The job object containing data for the ping operation. - * @returns {Promise<{boolean}} The result of logging and storing the check - */ - async handleHttp(job) { - // Define operation for timing - const operation = async () => { - const response = await this.axios.get(job.data.url); - return response; - }; - - let isAlive; - - // attempt connection - try { - const { responseTime, response } = await this.measureResponseTime(operation); - // check if response is in the 200 range, if so, service is up - isAlive = response.status >= 200 && response.status < 300; - - //Create a check with relevant data - const checkData = { - monitorId: job.data._id, - status: isAlive, - responseTime, - statusCode: response.status, - message: this.http.STATUS_CODES[response.status], - }; - 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"; - isAlive = false; - const checkData = { - monitorId: job.data._id, - status: isAlive, - statusCode, - responseTime: error.responseTime, - message, - }; - await this.logAndStoreCheck(checkData, this.db.createCheck); - } finally { - this.handleStatusUpdate(job, isAlive); - } - } - - /** - * Handles PageSpeed job types by fetching and processing PageSpeed insights. - * - * This method sends a request to the Google PageSpeed Insights API to get performance metrics - * for the specified URL, then logs and stores the check results. - * - * @param {Object} job - The job object containing data related to the PageSpeed check. - * @param {string} job.data.url - The URL to be analyzed by the PageSpeed Insights API. - * @param {string} job.data._id - The unique identifier for the monitor associated with the check. - * - * @returns {Promise} A promise that resolves when the check results have been logged and stored. - * - * @throws {Error} Throws an error if there is an issue with fetching or processing the PageSpeed insights. - */ - async handlePagespeed(job) { - 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` - ); - const pageSpeedResults = response.data; - const categories = pageSpeedResults.lighthouseResult?.categories; - const audits = pageSpeedResults.lighthouseResult?.audits; - const { - "cumulative-layout-shift": cls, - "speed-index": si, - "first-contentful-paint": fcp, - "largest-contentful-paint": lcp, - "total-blocking-time": tbt, - } = audits; - // Weights - // First Contentful Paint 10% - // Speed Index 10% - // Largest Contentful Paint 25% - // Total Blocking Time 30% - // Cumulative Layout Shift 25% - - isAlive = true; - const checkData = { - monitorId: job.data._id, - status: isAlive, - statusCode: 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, - performance: (categories.performance?.score || 0) * 100, - audits: { - cls, - si, - fcp, - lcp, - tbt, - }, - }; - this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck); - } catch (error) { - isAlive = false; - const statusCode = error.response?.status || this.NETWORK_ERROR; - const message = this.http.STATUS_CODES[statusCode] || "Network Error"; - const checkData = { - monitorId: job.data._id, - status: isAlive, - statusCode, - message, - accessibility: 0, - bestPractices: 0, - seo: 0, - performance: 0, - }; - this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck); - } finally { - this.handleStatusUpdate(job, isAlive); - } - } - - async handleHardware(job) { + async requestPing(job) { const url = job.data.url; - let isAlive; - //TODO Fetch hardware data - //For now, fake hardware data: + const { response, responseTime, error } = await this.timeRequest(() => + this.ping.promise.probe(url) + ); - const hardwareData = { + const pingResponse = { monitorId: job.data._id, - cpu: { - physical_core: 1, - logical_core: 1, - frequency: 266, - temperature: null, - free_percent: null, - usage_percent: 1, - }, - memory: { - total_bytes: 4, - available_bytes: 4, - used_bytes: 2, - usage_percent: 1, - }, - disk: [ - { - read_speed_bytes: 3, - write_speed_bytes: 3, - total_bytes: 10, - free_bytes: 2, - usage_percent: 1, - }, - ], - host: { - os: "Linux", - platform: "Ubuntu", - kernel_version: "24.04", - }, + type: "ping", + responseTime, + payload: response, }; - try { - isAlive = true; - this.logAndStoreCheck(hardwareData, this.db.createHardwareCheck); - } catch (error) { - isAlive = false; - const nullData = { - monitorId: job.data._id, - cpu: { - physical_core: 0, - logical_core: 0, - frequency: 0, - temperature: 0, - free_percent: 0, - usage_percent: 0, - }, - memory: { - total_bytes: 0, - available_bytes: 0, - used_bytes: 0, - usage_percent: 0, - }, - disk: [ - { - read_speed_bytes: 0, - write_speed_bytes: 0, - total_bytes: 0, - free_bytes: 0, - usage_percent: 0, - }, - ], - host: { - os: "", - platform: "", - kernel_version: "", - }, - }; - this.logAndStoreCheck(nullData, this.db.createHardwareCheck); - } finally { - this.handleStatusUpdate(job, isAlive, hardwareData); + if (error) { + pingResponse.status = false; + pingResponse.code = this.PING_ERROR; + pingResponse.message = errorMessages.PING_CANNOT_RESOLVE; + return pingResponse; } + + pingResponse.code = 200; + pingResponse.status = response.alive; + pingResponse.message = successMessages.PING_SUCCESS; + return pingResponse; } /** - * Retrieves the status of a given job based on its type. - * For unsupported job types, it logs an error and returns false. + * Sends an HTTP GET request to the specified URL and returns the response. * - * @param {Object} job - The job object containing data necessary for processing. - * @returns {Promise} The status of the job if it is supported and processed successfully, otherwise false. + * @param {Object} job - The job object containing the data for the HTTP request. + * @param {Object} job.data - The data object within the job. + * @param {string} job.data.url - The URL to send the HTTP GET request to. + * @param {string} job.data._id - The monitor ID for the HTTP request. + * @returns {Promise} An object containing the HTTP response details. + * @property {string} monitorId - The monitor ID for the HTTP request. + * @property {string} type - The type of request, which is "http". + * @property {number} responseTime - The time taken for the HTTP request to complete, in milliseconds. + * @property {Object} payload - The response payload from the HTTP request. + * @property {boolean} status - The status of the HTTP request (true if successful, false otherwise). + * @property {number} code - The response code (200 if successful, error code otherwise). + * @property {string} message - The message indicating the result of the HTTP request. + */ + async requestHttp(job) { + const url = job.data.url; + const { response, responseTime, error } = await this.timeRequest(() => + this.axios.get(url) + ); + + const httpResponse = { + monitorId: job.data._id, + type: "http", + responseTime, + payload: response?.data, + }; + + if (error) { + const code = error.response?.status || this.NETWORK_ERROR; + httpResponse.code = code; + httpResponse.status = false; + httpResponse.message = this.http.STATUS_CODES[code] || "Network Error"; + return httpResponse; + } + httpResponse.status = true; + httpResponse.code = response.status; + httpResponse.message = this.http.STATUS_CODES[response.status]; + return httpResponse; + } + + /** + * Sends a request to the Google PageSpeed Insights API for the specified URL and returns the response. + * + * @param {Object} job - The job object containing the data for the PageSpeed request. + * @param {Object} job.data - The data object within the job. + * @param {string} job.data.url - The URL to analyze with PageSpeed Insights. + * @param {string} job.data._id - The monitor ID for the PageSpeed request. + * @returns {Promise} An object containing the PageSpeed response details. + * @property {string} monitorId - The monitor ID for the PageSpeed request. + * @property {string} type - The type of request, which is "pagespeed". + * @property {number} responseTime - The time taken for the PageSpeed request to complete, in milliseconds. + * @property {Object} payload - The response payload from the PageSpeed request. + * @property {boolean} status - The status of the PageSpeed request (true if successful, false otherwise). + * @property {number} code - The response code (200 if successful, error code otherwise). + * @property {string} message - The message indicating the result of the PageSpeed request. + */ + async requestPagespeed(job) { + const url = job.data.url; + const { response, responseTime, error } = await this.timeRequest(() => + this.axios.get( + `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance` + ) + ); + + const pagespeedResponse = { + monitorId: job.data._id, + type: "pagespeed", + responseTime, + payload: response?.data, + }; + + if (error) { + const code = error.response?.status || this.NETWORK_ERROR; + pagespeedResponse.code = code; + pagespeedResponse.status = false; + pagespeedResponse.message = this.http.STATUS_CODES[code] || "Network Error"; + return pagespeedResponse; + } + + pagespeedResponse.status = true; + pagespeedResponse.code = response.status; + pagespeedResponse.message = this.http.STATUS_CODES[response.status]; + return pagespeedResponse; + } + + async requestHandleHardware(job) {} + + /** + * Gets the status of a job based on its type and returns the appropriate response. + * + * @param {Object} job - The job object containing the data for the status request. + * @param {Object} job.data - The data object within the job. + * @param {string} job.data.type - The type of the job (e.g., "ping", "http", "pagespeed", "hardware"). + * @returns {Promise} The response object from the appropriate request method. + * @throws {Error} Throws an error if the job type is unsupported. */ async getStatus(job) { switch (job.data.type) { case this.TYPE_PING: - return await this.handlePing(job); + return await this.requestPing(job); case this.TYPE_HTTP: - return await this.handleHttp(job); + return await this.requestHttp(job); case this.TYPE_PAGESPEED: - return await this.handlePagespeed(job); + return await this.requestPagespeed(job); case this.TYPE_HARDWARE: - return await this.handleHardware(job); + return await this.requestHandleHardware(job); default: this.logger.error({ message: `Unsupported type: ${job.data.type}`, service: this.SERVICE_NAME, method: "getStatus", }); - - return false; - } - } - - /** - * Logs and stores the result of a check for a specific job. - * - * @param {Object} data - Data to be written - * @param {function} writeToDB - DB write method - * - * @returns {Promise} The status of the inserted check if successful, otherwise false. - */ - - async logAndStoreCheck(data, writeToDB) { - try { - const insertedCheck = await writeToDB(data); - if (insertedCheck !== null && insertedCheck !== undefined) { - return insertedCheck.status; - } - throw new Error(); - } catch (error) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "logAndStoreCheck", - details: `Error writing check for ${data.monitorId}`, - stack: error.stack, - }); + return {}; } } } From b49ce1bd6da4769d5a5e561b7fd96f792005a2b0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 14:18:45 +0800 Subject: [PATCH 15/31] Add tests for refactored networkService --- Server/tests/services/networkService.test.js | 1019 +++--------------- 1 file changed, 179 insertions(+), 840 deletions(-) diff --git a/Server/tests/services/networkService.test.js b/Server/tests/services/networkService.test.js index 7e5ed7861..dbc2986ee 100644 --- a/Server/tests/services/networkService.test.js +++ b/Server/tests/services/networkService.test.js @@ -1,855 +1,194 @@ import sinon from "sinon"; import NetworkService from "../../service/networkService.js"; -import { errorMessages, successMessages } from "../../utils/messages.js"; +import { expect } from "chai"; +import http from "http"; 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.firstCall.args[0].message).to.equal("Database error"); - }); -}); - -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 finding monitor fails", async function () { - dbMock.getMonitorById.throws(new Error("Database error")); - - await networkService.handleStatusUpdate({ data: { _id: "monitor1" } }, true); - - expect(loggerMock.error.calledOnce).to.be.true; - expect(loggerMock.error.firstCall.args[0].message).to.equal("Database error"); - }); - - 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.firstCall.args[0].message).to.equal("Save error"); - }); -}); - -describe("NetworkService - measureResponseTime", () => { - let mockOperation, - dbMock, - emailServiceMock, - axiosMock, - pingMock, - loggerMock, - httpMock, - networkService; +describe("Network Service", () => { + let axios, ping, logger, 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 - ); + axios = { + get: sinon.stub().resolves({ + data: { foo: "bar" }, + status: 200, + }), + }; + ping = { + promise: { + probe: sinon + .stub() + .resolves({ response: { alive: true }, responseTime: 100, alive: true }), + }, + }; + logger = { error: sinon.stub() }; + networkService = new NetworkService(axios, ping, logger, http); + }); + describe("constructor", () => { + it("should create a new NetworkService instance", () => { + const networkService = new NetworkService(); + expect(networkService).to.be.an.instanceOf(NetworkService); + }); }); - afterEach(() => { - sinon.restore(); + describe("timeRequest", () => { + it("should time an asynchronous operation", async () => { + const operation = sinon.stub().resolves("success"); + const { response, responseTime } = await networkService.timeRequest(operation); + expect(response).to.equal("success"); + expect(responseTime).to.be.a("number"); + }); + it("should handle errors if operation throws error", async () => { + const error = new Error("Test error"); + const operation = sinon.stub().throws(error); + const { response, responseTime } = await networkService.timeRequest(operation); + expect(response).to.be.null; + expect(responseTime).to.be.a("number"); + expect(error.message).to.equal("Test error"); + }); }); - 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); + describe("requestPing", () => { + it("should return a response object if ping successful", async () => { + const pingResult = await networkService.requestPing({ + data: { url: "http://test.com", _id: "123" }, + }); + expect(pingResult.monitorId).to.equal("123"); + expect(pingResult.type).to.equal("ping"); + expect(pingResult.responseTime).to.be.a("number"); + expect(pingResult.status).to.be.true; + }); + it("should return a response object if ping unsuccessful", async () => { + const error = new Error("Test error"); + networkService.timeRequest = sinon + .stub() + .resolves({ response: null, responseTime: 1, error }); + const pingResult = await networkService.requestPing({ + data: { url: "http://test.com", _id: "123" }, + }); + expect(pingResult.monitorId).to.equal("123"); + expect(pingResult.type).to.equal("ping"); + expect(pingResult.responseTime).to.be.a("number"); + expect(pingResult.status).to.be.false; + expect(pingResult.code).to.equal(networkService.PING_ERROR); + }); + }); + describe("requestHttp", () => { + it("should return a response object if http successful", async () => { + const job = { data: { url: "http://test.com", _id: "123" } }; + const httpResult = await networkService.requestHttp(job); + expect(httpResult.monitorId).to.equal("123"); + expect(httpResult.type).to.equal("http"); + expect(httpResult.responseTime).to.be.a("number"); + expect(httpResult.status).to.be.true; + }); + it("should return a response object if http unsuccessful", async () => { + const error = new Error("Test error"); + error.response = { status: 404 }; + networkService.timeRequest = sinon + .stub() + .resolves({ response: null, responseTime: 1, error }); + const job = { data: { url: "http://test.com", _id: "123" } }; + const httpResult = await networkService.requestHttp(job); + expect(httpResult.monitorId).to.equal("123"); + expect(httpResult.type).to.equal("http"); + expect(httpResult.responseTime).to.be.a("number"); + expect(httpResult.status).to.be.false; + expect(httpResult.code).to.equal(404); + }); + it("should return a response object if http unsuccessful with unknown code", async () => { + const error = new Error("Test error"); + error.response = {}; + networkService.timeRequest = sinon + .stub() + .resolves({ response: null, responseTime: 1, error }); + const job = { data: { url: "http://test.com", _id: "123" } }; + const httpResult = await networkService.requestHttp(job); + expect(httpResult.monitorId).to.equal("123"); + expect(httpResult.type).to.equal("http"); + expect(httpResult.responseTime).to.be.a("number"); + expect(httpResult.status).to.be.false; + expect(httpResult.code).to.equal(networkService.NETWORK_ERROR); + }); }); - 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"); - } + describe("requestPagespeed", () => { + it("should return a response object if pagespeed successful", async () => { + const job = { data: { url: "http://test.com", _id: "123" } }; + const pagespeedResult = await networkService.requestPagespeed(job); + expect(pagespeedResult.monitorId).to.equal("123"); + expect(pagespeedResult.type).to.equal("pagespeed"); + expect(pagespeedResult.responseTime).to.be.a("number"); + expect(pagespeedResult.status).to.be.true; + }); + it("should return a response object if pagespeed unsuccessful", async () => { + const error = new Error("Test error"); + error.response = { status: 404 }; + networkService.timeRequest = sinon + .stub() + .resolves({ response: null, responseTime: 1, error }); + const job = { data: { url: "http://test.com", _id: "123" } }; + const pagespeedResult = await networkService.requestPagespeed(job); + expect(pagespeedResult.monitorId).to.equal("123"); + expect(pagespeedResult.type).to.equal("pagespeed"); + expect(pagespeedResult.responseTime).to.be.a("number"); + expect(pagespeedResult.status).to.be.false; + expect(pagespeedResult.code).to.equal(404); + }); + it("should return a response object if pagespeed unsuccessful with an unknown code", async () => { + const error = new Error("Test error"); + error.response = {}; + networkService.timeRequest = sinon + .stub() + .resolves({ response: null, responseTime: 1, error }); + const job = { data: { url: "http://test.com", _id: "123" } }; + const pagespeedResult = await networkService.requestPagespeed(job); + expect(pagespeedResult.monitorId).to.equal("123"); + expect(pagespeedResult.type).to.equal("pagespeed"); + expect(pagespeedResult.responseTime).to.be.a("number"); + expect(pagespeedResult.status).to.be.false; + expect(pagespeedResult.code).to.equal(networkService.NETWORK_ERROR); + }); }); - 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 () => { - const response = { alive: true }; - const responseTime = 0; - pingMock.promise.probe.returns(response); - logAndStoreCheckStub.resolves(); - await networkService.handlePing(job); - expect( - logAndStoreCheckStub.calledOnceWith({ - monitorId: job.data._id, - status: response.alive, - responseTime, - message: successMessages.PING_SUCCESS, - }) - ).to.be.true; - expect(handleStatusUpdateStub.calledOnceWith(job, true)).to.be.true; - }); - it("should handle a successful response and isAlive === false", async function () { - const response = { alive: false }; - const responseTime = 0; - pingMock.promise.probe.resolves(response); - logAndStoreCheckStub.resolves(); - - await networkService.handlePing(job); - expect( - logAndStoreCheckStub.calledOnceWith({ - monitorId: job.data._id, - status: false, - responseTime, - message: errorMessages.PING_CANNOT_RESOLVE, - }) - ).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 - handleHardware", () => { - 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 Hardware response", async () => { - const responseMock = { - monitorId: jobMock.data._id, - cpu: { - physical_core: 1, - logical_core: 1, - frequency: 266, - temperature: null, - free_percent: null, - usage_percent: null, - }, - memory: { - total_bytes: 4, - available_bytes: 4, - used_bytes: 2, - usage_percent: 0.5, - }, - disk: [ - { - read_speed_bytes: 3, - write_speed_bytes: 3, - total_bytes: 10, - free_bytes: 2, - usage_percent: 0.8, - }, - ], - host: { - os: "Linux", - platform: "Ubuntu", - kernel_version: "24.04", - }, - }; - axiosMock.get.resolves(responseMock); - - await networkService.handleHardware(jobMock); - expect(networkService.logAndStoreCheck.calledOnce).to.be.true; - const hardwareData = networkService.logAndStoreCheck.getCall(0).args[0]; - expect(hardwareData.cpu).to.include({ - ...responseMock.cpu, - }); - expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, true)).to.be.true; - }); - - it("should handle an error Hardware response", async () => { - logAndStoreCheckStub.throws(new Error("Hardware error")); - try { - await networkService.handleHardware(jobMock); - } catch (error) { - expect(error.message).to.equal("Hardware error"); - } - }); -}); - -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 return true if the job type is hardware and handleHardware is successful", async () => { - const job = { data: { type: networkService.TYPE_HARDWARE } }; - sinon.stub(networkService, "handleHardware").resolves(true); - const result = await networkService.getStatus(job); - expect(result).to.be.true; - }); - it("should return false if the job type is hardware and handleHardware is not successful", async () => { - const job = { data: { type: networkService.TYPE_HARDWARE } }; - sinon.stub(networkService, "handleHardware").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.firstCall.args[0].message).to.equal( - "Unsupported type: unknown" - ); - }); -}); - -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.firstCall.args[0].details).to.equal( - `Error writing check for ${data.monitorId}` - ); - }); - - 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.firstCall.args[0].details).to.equal( - `Error writing check for ${data.monitorId}` - ); - }); - 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.firstCall.args[0].details).to.equal( - `Error writing check for ${data.monitorId}` - ); + describe("getStatus", () => { + beforeEach(() => { + networkService.requestPing = sinon.stub(); + networkService.requestHttp = sinon.stub(); + networkService.requestPagespeed = sinon.stub(); + networkService.requestHardware = sinon.stub(); + }); + + afterEach(() => { + sinon.restore(); + }); + it("should call requestPing if type is ping", () => { + networkService.getStatus({ data: { type: "ping" } }); + expect(networkService.requestPing.calledOnce).to.be.true; + expect(networkService.requestHttp.notCalled).to.be.true; + expect(networkService.requestPagespeed.notCalled).to.be.true; + }); + it("should call requestHttp if type is http", () => { + networkService.getStatus({ data: { type: "http" } }); + expect(networkService.requestPing.notCalled).to.be.true; + expect(networkService.requestHttp.calledOnce).to.be.true; + expect(networkService.requestPagespeed.notCalled).to.be.true; + }); + it("should call requestPagespeed if type is pagespeed", () => { + networkService.getStatus({ data: { type: "pagespeed" } }); + expect(networkService.requestPing.notCalled).to.be.true; + expect(networkService.requestHttp.notCalled).to.be.true; + expect(networkService.requestPagespeed.calledOnce).to.be.true; + }); + it("should call requestHandleHardware if type is hardware", () => { + networkService.getStatus({ data: { type: "hardware" } }); + expect(networkService.requestPing.notCalled).to.be.true; + expect(networkService.requestHttp.notCalled).to.be.true; + expect(networkService.requestPagespeed.notCalled).to.be.true; + }); + it("should log an error if an unknown type is provided", () => { + networkService.getStatus({ data: { type: "unknown" } }); + expect(logger.error.calledOnce).to.be.true; + expect(logger.error.args[0][0].message).to.equal("Unsupported type: unknown"); + }); }); }); From d85dc50220fe3a41f4ae8bb5c649313cbe1c7337 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 14:19:14 +0800 Subject: [PATCH 16/31] Add statusService --- Server/service/statusService.js | 139 ++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 Server/service/statusService.js diff --git a/Server/service/statusService.js b/Server/service/statusService.js new file mode 100644 index 000000000..7d71fab7f --- /dev/null +++ b/Server/service/statusService.js @@ -0,0 +1,139 @@ +class StatusService { + /** + * Creates an instance of StatusService. + * + * @param {Object} db - The database instance. + * @param {Object} logger - The logger instance. + */ + constructor(db, logger) { + this.db = db; + this.logger = logger; + this.SERVICE_NAME + "StatusService"; + } + + /** + * Updates the status of a monitor based on the network response. + * + * @param {Object} networkResponse - The network response containing monitorId and status. + * @param {string} networkResponse.monitorId - The ID of the monitor. + * @param {string} networkResponse.status - The new status of the monitor. + * @returns {Promise} - A promise that resolves to an object containing the monitor, statusChanged flag, and previous status if the status changed, or false if an error occurred. + * @returns {Promise} returnObject - The object returned by the function. + * @returns {Object} returnObject.monitor - The monitor object. + * @returns {boolean} returnObject.statusChanged - Flag indicating if the status has changed. + * @returns {boolean} returnObject.prevStatus - The previous status of the monitor + */ + updateStatus = async (networkResponse) => { + this.insertCheck(networkResponse); + try { + const { monitorId, status } = networkResponse; + const monitor = await this.db.getMonitorById(monitorId); + // No change in monitor status, return early + if (monitor.status === status) return { statusChanged: false }; + // Monitor status changed, save prev status and update monitor + + this.logger.info({ + service: this.SERVICE_NAME, + message: `${monitor.name} went from ${monitor.status === true ? "up" : "down"} to ${status === true ? "up" : "down"}`, + prevStatus: monitor.status, + newStatus: status, + }); + + const prevStatus = monitor.status; + monitor.status = status; + monitor.save(); + + return { + monitor, + statusChanged: true, + prevStatus: prevStatus, + }; + // + } catch (error) { + this.logger.error({ + service: this.SERVICE_NAME, + message: error.message, + method: "updateStatus", + stack: error.stack, + }); + throw error; + } + }; + + /** + * Builds a check object from the network response. + * + * @param {Object} networkResponse - The network response object. + * @param {string} networkResponse.monitorId - The monitor ID. + * @param {string} networkResponse.type - The type of the response. + * @param {string} networkResponse.status - The status of the response. + * @param {number} networkResponse.responseTime - The response time. + * @param {number} networkResponse.code - The status code. + * @param {string} networkResponse.message - The message. + * @param {Object} networkResponse.payload - The payload of the response. + * @returns {Object} The check object. + */ + buildCheck = (networkResponse) => { + const { monitorId, type, status, responseTime, code, message, payload } = + networkResponse; + const check = { + monitorId, + status, + statusCode: code, + responseTime, + message, + }; + if (type === "pagespeed") { + const categories = payload.lighthouseResult?.categories; + const audits = payload.lighthouseResult?.audits; + const { + "cumulative-layout-shift": cls = 0, + "speed-index": si = 0, + "first-contentful-paint": fcp = 0, + "largest-contentful-paint": lcp = 0, + "total-blocking-time": tbt = 0, + } = audits; + check.accessibility = (categories.accessibility?.score || 0) * 100; + check.bestPractices = (categories["best-practices"]?.score || 0) * 100; + check.seo = (categories.seo?.score || 0) * 100; + check.performance = (categories.performance?.score || 0) * 100; + check.audits = { cls, si, fcp, lcp, tbt }; + } + return check; + }; + + /** + * Inserts a check into the database based on the network response. + * + * @param {Object} networkResponse - The network response object. + * @param {string} networkResponse.monitorId - The monitor ID. + * @param {string} networkResponse.type - The type of the response. + * @param {string} networkResponse.status - The status of the response. + * @param {number} networkResponse.responseTime - The response time. + * @param {number} networkResponse.code - The status code. + * @param {string} networkResponse.message - The message. + * @param {Object} networkResponse.payload - The payload of the response. + * @returns {Promise} A promise that resolves when the check is inserted. + */ + insertCheck = async (networkResponse) => { + try { + const operationMap = { + http: this.db.createCheck, + ping: this.db.createCheck, + pagespeed: this.db.createPageSpeedCheck, + }; + const operation = operationMap[networkResponse.type]; + const check = this.buildCheck(networkResponse); + await operation(check); + } catch (error) { + this.logger.error({ + message: error.message, + service: this.SERVICE_NAME, + method: "insertCheck", + details: `Error inserting check for monitor: ${networkResponse.monitorId}`, + stack: error.stack, + }); + } + }; +} +export default StatusService; From 432accaa0b30b898a5b271793c543d3d77933608 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 14:19:37 +0800 Subject: [PATCH 17/31] Add tests for statusService --- Server/tests/services/statusService.test.js | 193 ++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 Server/tests/services/statusService.test.js diff --git a/Server/tests/services/statusService.test.js b/Server/tests/services/statusService.test.js new file mode 100644 index 000000000..df74ef715 --- /dev/null +++ b/Server/tests/services/statusService.test.js @@ -0,0 +1,193 @@ +import sinon from "sinon"; +import StatusService from "../../service/statusService.js"; +import { afterEach, describe } from "node:test"; + +describe("StatusService", () => { + let db, logger, statusService; + beforeEach(() => { + db = { + getMonitorById: sinon.stub(), + createCheck: sinon.stub(), + createPagespeedCheck: sinon.stub(), + }; + logger = { + info: sinon.stub(), + error: sinon.stub(), + }; + statusService = new StatusService(db, logger); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe("constructor", () => { + it("should create an instance of StatusService", () => { + expect(statusService).to.be.an.instanceOf(StatusService); + }); + }); + + describe("updateStatus", async () => { + beforeEach(() => { + // statusService.insertCheck = sinon.stub().resolves; + }); + afterEach(() => { + sinon.restore(); + }); + it("should throw an error if an error occurs", async () => { + const error = new Error("Test error"); + statusService.db.getMonitorById = sinon.stub().throws(error); + try { + await statusService.updateStatus({ monitorId: "test", status: true }); + } catch (error) { + expect(error.message).to.equal("Test error"); + } + // expect(statusService.insertCheck.calledOnce).to.be.true; + }); + it("should return {statusChanged: false} if status hasn't changed", async () => { + statusService.db.getMonitorById = sinon.stub().returns({ status: true }); + const result = await statusService.updateStatus({ + monitorId: "test", + status: true, + }); + expect(result).to.deep.equal({ statusChanged: false }); + // expect(statusService.insertCheck.calledOnce).to.be.true; + }); + it("should return {statusChanged: true} if status has changed from down to up", async () => { + statusService.db.getMonitorById = sinon + .stub() + .returns({ status: false, save: sinon.stub() }); + const result = await statusService.updateStatus({ + monitorId: "test", + status: true, + }); + expect(result.statusChanged).to.be.true; + expect(result.monitor.status).to.be.true; + expect(result.prevStatus).to.be.false; + // expect(statusService.insertCheck.calledOnce).to.be.true; + }); + it("should return {statusChanged: true} if status has changed from up to down", async () => { + statusService.db.getMonitorById = sinon + .stub() + .returns({ status: true, save: sinon.stub() }); + const result = await statusService.updateStatus({ + monitorId: "test", + status: false, + }); + expect(result.statusChanged).to.be.true; + expect(result.monitor.status).to.be.false; + expect(result.prevStatus).to.be.true; + // expect(statusService.insertCheck.calledOnce).to.be.true; + }); + }); + + describe("buildCheck", () => { + it("should build a check object", () => { + const check = statusService.buildCheck({ + monitorId: "test", + type: "test", + status: true, + responseTime: 100, + code: 200, + message: "Test message", + payload: { test: "test" }, + }); + expect(check.monitorId).to.equal("test"); + expect(check.status).to.be.true; + expect(check.statusCode).to.equal(200); + expect(check.responseTime).to.equal(100); + expect(check.message).to.equal("Test message"); + }); + it("should build a check object for pagespeed type", () => { + const check = statusService.buildCheck({ + monitorId: "test", + type: "pagespeed", + status: true, + responseTime: 100, + code: 200, + message: "Test message", + payload: { + lighthouseResult: { + categories: { + accessibility: { score: 1 }, + "best-practices": { score: 1 }, + performance: { score: 1 }, + seo: { score: 1 }, + }, + audits: { + "cumulative-layout-shift": { score: 1 }, + "speed-index": { score: 1 }, + "first-contentful-paint": { score: 1 }, + "largest-contentful-paint": { score: 1 }, + "total-blocking-time": { score: 1 }, + }, + }, + }, + }); + expect(check.monitorId).to.equal("test"); + expect(check.status).to.be.true; + expect(check.statusCode).to.equal(200); + expect(check.responseTime).to.equal(100); + expect(check.message).to.equal("Test message"); + expect(check.accessibility).to.equal(100); + expect(check.bestPractices).to.equal(100); + expect(check.performance).to.equal(100); + expect(check.seo).to.equal(100); + expect(check.audits).to.deep.equal({ + cls: { score: 1 }, + si: { score: 1 }, + fcp: { score: 1 }, + lcp: { score: 1 }, + tbt: { score: 1 }, + }); + }); + it("should build a check object for pagespeed type with missing data", () => { + const check = statusService.buildCheck({ + monitorId: "test", + type: "pagespeed", + status: true, + responseTime: 100, + code: 200, + message: "Test message", + payload: { + lighthouseResult: { + categories: {}, + audits: {}, + }, + }, + }); + expect(check.monitorId).to.equal("test"); + expect(check.status).to.be.true; + expect(check.statusCode).to.equal(200); + expect(check.responseTime).to.equal(100); + expect(check.message).to.equal("Test message"); + expect(check.accessibility).to.equal(0); + expect(check.bestPractices).to.equal(0); + expect(check.performance).to.equal(0); + expect(check.seo).to.equal(0); + expect(check.audits).to.deep.equal({ + cls: 0, + si: 0, + fcp: 0, + lcp: 0, + tbt: 0, + }); + }); + }); + describe("insertCheck", () => { + it("should log an error if one is thrown", async () => { + const testError = new Error("Test error"); + statusService.db.createCheck = sinon.stub().throws(testError); + try { + await statusService.insertCheck({ monitorId: "test" }); + } catch (error) { + expect(error.message).to.equal(testError.message); + } + expect(statusService.logger.error.calledOnce).to.be.true; + }); + it("should insert a check into the database", async () => { + await statusService.insertCheck({ monitorId: "test", type: "http" }); + expect(statusService.db.createCheck.calledOnce).to.be.true; + }); + }); +}); From 903ce58b39d1007c2ab9000411b62211373b22a2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 14:20:05 +0800 Subject: [PATCH 18/31] Add notificationService --- Server/service/notificationService.js | 63 +++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 Server/service/notificationService.js diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js new file mode 100644 index 000000000..f464c08a7 --- /dev/null +++ b/Server/service/notificationService.js @@ -0,0 +1,63 @@ +class NotificationService { + /** + * Creates an instance of NotificationService. + * + * @param {Object} emailService - The email service used for sending notifications. + * @param {Object} db - The database instance for storing notification data. + * @param {Object} logger - The logger instance for logging activities. + */ + constructor(emailService, db, logger) { + this.SERVICE_NAME = "NotificationService"; + this.emailService = emailService; + this.db = db; + this.logger = logger; + } + + /** + * Sends an email notification based on the network response. + * + * @param {Object} networkResponse - The response from the network monitor. + * @param {Object} networkResponse.monitor - The monitor object containing details about the monitored service. + * @param {string} networkResponse.monitor.name - The name of the monitor. + * @param {string} networkResponse.monitor.url - The URL of the monitor. + * @param {boolean} networkResponse.status - The current status of the monitor (true for up, false for down). + * @param {boolean} networkResponse.prevStatus - The previous status of the monitor (true for up, false for down). + * @param {string} address - The email address to send the notification to. + */ + async sendEmail(networkResponse, address) { + const { monitor, status, prevStatus } = networkResponse; + const template = prevStatus === false ? "serverIsUpTemplate" : "serverIsDownTemplate"; + const context = { monitor: monitor.name, url: monitor.url }; + const subject = `Monitor ${monitor.name} is ${status === true ? "up" : "down"}`; + this.emailService.buildAndSendEmail(template, context, address, subject); + } + + /** + * Handles notifications based on the network response. + * + * @param {Object} networkResponse - The response from the network monitor. + * @param {string} networkResponse.monitorId - The ID of the monitor. + */ + async handleNotifications(networkResponse) { + try { + const notifications = await this.db.getNotificationsByMonitorId( + networkResponse.monitorId + ); + for (const notification of notifications) { + if (notification.type === "email") { + this.sendEmail(networkResponse, notification.address); + } + // Handle other types of notifications here + } + } catch (error) { + this.logger.warn({ + message: error.message, + service: this.SERVICE_NAME, + method: "handleNotifications", + stack: error.stack, + }); + } + } +} + +export default NotificationService; From 378dba0b5756d0167cd4d1df0d9ada50ba8d5dc2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 14:20:22 +0800 Subject: [PATCH 19/31] Add tests for notificationService --- .../services/notificationService.test.js | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 Server/tests/services/notificationService.test.js diff --git a/Server/tests/services/notificationService.test.js b/Server/tests/services/notificationService.test.js new file mode 100644 index 000000000..b33bd21ed --- /dev/null +++ b/Server/tests/services/notificationService.test.js @@ -0,0 +1,95 @@ +import sinon from "sinon"; +import NotificationService from "../../service/notificationService.js"; +import { expect } from "chai"; + +describe("NotificationService", () => { + let emailService, db, logger, notificationService; + beforeEach(() => { + db = { + getNotificationsByMonitorId: sinon.stub(), + }; + emailService = { + buildAndSendEmail: sinon.stub(), + }; + logger = { + warn: sinon.stub(), + }; + + notificationService = new NotificationService(emailService, db, logger); + }); + afterEach(() => { + sinon.restore(); + }); + + describe("constructor", () => { + it("should create a new instance of NotificationService", () => { + expect(notificationService).to.be.an.instanceOf(NotificationService); + }); + }); + + describe("sendEmail", async () => { + it("should send an email notification with Up Template", async () => { + const networkResponse = { + monitor: { + name: "Test Monitor", + url: "http://test.com", + }, + status: true, + prevStatus: false, + }; + const address = "test@test.com"; + await notificationService.sendEmail(networkResponse, address); + expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true; + expect( + notificationService.emailService.buildAndSendEmail.calledWith( + "serverIsUpTemplate", + { monitor: "Test Monitor", url: "http://test.com" } + ) + ); + }); + it("should send an email notification with Down Template", async () => { + const networkResponse = { + monitor: { + name: "Test Monitor", + url: "http://test.com", + }, + status: false, + prevStatus: true, + }; + const address = "test@test.com"; + await notificationService.sendEmail(networkResponse, address); + expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true; + }); + it("should send an email notification with Up Template", async () => { + const networkResponse = { + monitor: { + name: "Test Monitor", + url: "http://test.com", + }, + status: true, + prevStatus: false, + }; + const address = "test@test.com"; + await notificationService.sendEmail(networkResponse, address); + expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true; + }); + }); + + describe("handleNotifications", async () => { + it("should handle notifications based on the network response", async () => { + notificationService.sendEmail = sinon.stub(); + notificationService.db.getNotificationsByMonitorId.resolves([ + { type: "email", address: "www.google.com" }, + ]); + await notificationService.handleNotifications({ monitorId: "123" }); + expect(notificationService.sendEmail.calledOnce).to.be.true; + }); + + it("should handle an error when getting notifications", async () => { + const testError = new Error("Test Error"); + notificationService.db.getNotificationsByMonitorId.rejects(testError); + await notificationService.handleNotifications({ monitorId: "123" }); + expect(notificationService.logger.warn.calledOnce).to.be.true; + }); + }); +}); From 5d6cfb16bcf0c2cb9df9f5104a9cc4665367df27 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 14:20:40 +0800 Subject: [PATCH 20/31] Refactor jobQueue to use new services --- Server/service/jobQueue.js | 157 +++++++++++++++++++++++++------------ 1 file changed, 105 insertions(+), 52 deletions(-) diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index 51e27fdb2..70f63e74f 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -16,7 +16,14 @@ class JobQueue { * @param {SettingsService} settingsService - The settings service * @throws {Error} */ - constructor(settingsService, logger, Queue, Worker) { + constructor( + statusService, + notificationService, + settingsService, + logger, + Queue, + Worker + ) { const settings = settingsService.getSettings() || {}; const { redisHost = "127.0.0.1", redisPort = 6379 } = settings; @@ -31,6 +38,8 @@ class JobQueue { this.workers = []; this.db = null; this.networkService = null; + this.statusService = statusService; + this.notificationService = notificationService; this.settingsService = settingsService; this.logger = logger; this.Worker = Worker; @@ -46,12 +55,21 @@ class JobQueue { static async createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, Queue, Worker ) { - const queue = new JobQueue(settingsService, logger, Queue, Worker); + const queue = new JobQueue( + statusService, + notificationService, + settingsService, + logger, + Queue, + Worker + ); try { queue.db = db; queue.networkService = networkService; @@ -71,62 +89,97 @@ class JobQueue { } } + async isInMaintenanceWindow(monitorId) { + const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(monitorId); + // Check for active maintenance window: + const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => { + if (window.active) { + const start = new Date(window.start); + const end = new Date(window.end); + const now = new Date(); + const repeatInterval = window.repeat || 0; + + // If start is < now and end > now, we're in maintenance + if (start <= now && end >= now) return true; + + // If maintenance window was set in the past with a repeat, + // we need to advance start and end to see if we are in range + + while (start < now && repeatInterval !== 0) { + start.setTime(start.getTime() + repeatInterval); + end.setTime(end.getTime() + repeatInterval); + if (start <= now && end >= now) { + return true; + } + } + return false; + } + return acc; + }, false); + return maintenanceWindowIsActive; + } + + /** + * Creates a job handler function for processing jobs. + * + * @returns {Function} An async function that processes a job. + */ + createJobHandler() { + return async (job) => { + try { + // Get all maintenance windows for this monitor + const monitorId = job.data._id; + const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId); + // If a maintenance window is active, we're done + + if (maintenanceWindowActive) { + this.logger.info({ + message: `Monitor ${monitorId} is in maintenance window`, + service: SERVICE_NAME, + method: "createWorker", + }); + return; + } + + // Get the current status + const networkResponse = await this.networkService.getStatus(job); + // Handle status change + + const { monitor, statusChanged, prevStatus } = + await this.statusService.updateStatus(networkResponse); + + //If status hasn't changed, we're done + if (statusChanged == false) return; + + // if prevStatus is undefined, monitor is resuming, we're done + if (prevStatus === undefined) return; + + this.notificationService.handleNotifications({ + ...networkResponse, + monitor, + prevStatus, + }); + } catch (error) { + this.logger.error({ + message: error.message, + service: SERVICE_NAME, + method: "createWorker", + details: `Error processing job ${job.id}: ${error.message}`, + stack: error.stack, + }); + } + }; + } + /** * Creates a worker for the queue * Operations are carried out in the async callback * @returns {Worker} The newly created worker */ createWorker() { - const worker = new this.Worker( - QUEUE_NAME, - async (job) => { - try { - // Get all maintenance windows for this monitor - const monitorId = job.data._id; - const maintenanceWindows = - await this.db.getMaintenanceWindowsByMonitorId(monitorId); - // Check for active maintenance window: - const maintenanceWindowActive = maintenanceWindows.reduce((acc, window) => { - if (window.active) { - const start = new Date(window.start); - const end = new Date(window.end); - const now = new Date(); - const repeatInterval = window.repeat || 0; - - while ((start < now) & (repeatInterval !== 0)) { - start.setTime(start.getTime() + repeatInterval); - end.setTime(end.getTime() + repeatInterval); - } - - if (start < now && end > now) { - return true; - } - } - return acc; - }, false); - if (!maintenanceWindowActive) { - await this.networkService.getStatus(job); - } else { - this.logger.info({ - message: `Monitor ${monitorId} is in maintenance window`, - service: SERVICE_NAME, - method: "createWorker", - }); - } - } catch (error) { - this.logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "createWorker", - details: `Error processing job ${job.id}: ${error.message}`, - stack: error.stack, - }); - } - }, - { - connection: this.connection, - } - ); + const worker = new this.Worker(QUEUE_NAME, this.createJobHandler(), { + connection: this.connection, + }); return worker; } From 3a2bdbbffaa03084adfd1378e049676c8f8fb564 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 14:20:51 +0800 Subject: [PATCH 21/31] Update tests for jobQueue --- Server/tests/services/jobQueue.test.js | 410 ++++++++++++++----------- 1 file changed, 236 insertions(+), 174 deletions(-) diff --git a/Server/tests/services/jobQueue.test.js b/Server/tests/services/jobQueue.test.js index 4a22821ff..a680a2c64 100644 --- a/Server/tests/services/jobQueue.test.js +++ b/Server/tests/services/jobQueue.test.js @@ -44,21 +44,41 @@ class WorkerStub { } describe("JobQueue", () => { - let settingsService, logger, db, networkService; + let settingsService, + logger, + db, + networkService, + statusService, + notificationService, + jobQueue; - beforeEach(() => { + beforeEach(async () => { settingsService = { getSettings: sinon.stub() }; + statusService = { updateStatus: sinon.stub() }; + notificationService = { handleNotifications: sinon.stub() }; + logger = { error: sinon.stub(), info: sinon.stub() }; db = { getAllMonitors: sinon.stub().returns([]), getMaintenanceWindowsByMonitorId: sinon.stub().returns([]), }; networkService = { getStatus: sinon.stub() }; + jobQueue = await JobQueue.createJobQueue( + db, + networkService, + statusService, + notificationService, + settingsService, + logger, + QueueStub, + WorkerStub + ); }); afterEach(() => { sinon.restore(); }); + describe("createJobQueue", () => { it("should create a new JobQueue and add jobs for active monitors", async () => { db.getAllMonitors.returns([ @@ -68,6 +88,8 @@ describe("JobQueue", () => { const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, QueueStub, @@ -81,9 +103,11 @@ describe("JobQueue", () => { it("should reject with an error if an error occurs", async () => { db.getAllMonitors.throws("Error"); try { - await JobQueue.createJobQueue( + const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, QueueStub, @@ -94,6 +118,7 @@ describe("JobQueue", () => { expect(error.method).to.equal("createJobQueue"); } }); + it("should reject with an error if an error occurs, should not overwrite error data", async () => { const error = new Error("Error"); error.service = "otherService"; @@ -101,9 +126,11 @@ describe("JobQueue", () => { db.getAllMonitors.throws(error); try { - await JobQueue.createJobQueue( + const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, QueueStub, @@ -115,58 +142,53 @@ describe("JobQueue", () => { } }); }); + describe("Constructor", () => { - it("should construct a new JobQueue with default port and host if not provided", () => { + it("should construct a new JobQueue with default port and host if not provided", async () => { settingsService.getSettings.returns({}); - const jobQueue = new JobQueue(settingsService, logger, QueueStub, WorkerStub); + expect(jobQueue.connection.host).to.equal("127.0.0.1"); expect(jobQueue.connection.port).to.equal(6379); }); - it("should construct a new JobQueue with provided port and host", () => { + it("should construct a new JobQueue with provided port and host", async () => { settingsService.getSettings.returns({ redisHost: "localhost", redisPort: 1234 }); - const jobQueue = new JobQueue(settingsService, logger, QueueStub, WorkerStub); + const jobQueue = await JobQueue.createJobQueue( + db, + networkService, + statusService, + notificationService, + settingsService, + logger, + QueueStub, + WorkerStub + ); expect(jobQueue.connection.host).to.equal("localhost"); expect(jobQueue.connection.port).to.equal(1234); }); }); - describe("createWorker", () => { - it("should create a new worker", async () => { - const jobQueue = new JobQueue(settingsService, logger, QueueStub, WorkerStub); - const worker = jobQueue.createWorker(); - expect(worker).to.be.instanceOf(WorkerStub); - }); - it("worker should handle a maintenanceWindow error", async () => { + describe("isMaintenanceWindow", () => { + it("should throw an error if error occurs", async () => { db.getMaintenanceWindowsByMonitorId.throws("Error"); const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, QueueStub, WorkerStub ); - const worker = jobQueue.createWorker(); - await worker.workerTask(); - expect(logger.error.calledOnce).to.be.true; + try { + jobQueue.isInMaintenanceWindow(1); + } catch (error) { + expect(error.service).to.equal("JobQueue"); + expect(error.method).to.equal("createWorker"); + } }); - it("worker should handle a maintenanceWindow that is not active", async () => { - db.getMaintenanceWindowsByMonitorId.returns([ - { start: 123, end: 123, repeat: 123456 }, - ]); - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - const worker = jobQueue.createWorker(); - await worker.workerTask(); - expect(networkService.getStatus.calledOnce).to.be.true; - }); - it("worker should handle a maintenanceWindow that is active", async () => { + + it("should return true if in maintenance window with no repeat", async () => { db.getMaintenanceWindowsByMonitorId.returns([ { active: true, @@ -178,42 +200,193 @@ describe("JobQueue", () => { const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, QueueStub, WorkerStub ); - const worker = jobQueue.createWorker(); - await worker.workerTask(); - expect(networkService.getStatus.calledOnce).to.be.false; + const inWindow = await jobQueue.isInMaintenanceWindow(1); + expect(inWindow).to.be.true; }); - it("worker should handle a maintenanceWindow that is active, has a repeat, but is not in maintenance zone", async () => { + + it("should return true if in maintenance window with repeat", async () => { db.getMaintenanceWindowsByMonitorId.returns([ { active: true, start: new Date(Date.now() - 10000).toISOString(), - end: new Date(Date.now() + 5000).toISOString(), - repeat: 10000, + end: new Date(Date.now() - 5000).toISOString(), + repeat: 1000, }, ]); const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, QueueStub, WorkerStub ); - const worker = jobQueue.createWorker(); - await worker.workerTask(); - expect(networkService.getStatus.calledOnce).to.be.true; + const inWindow = await jobQueue.isInMaintenanceWindow(1); + expect(inWindow).to.be.true; + }); + + it("should return false if in end < start", async () => { + db.getMaintenanceWindowsByMonitorId.returns([ + { + active: true, + start: new Date(Date.now() - 5000).toISOString(), + end: new Date(Date.now() - 10000).toISOString(), + repeat: 1000, + }, + ]); + const jobQueue = await JobQueue.createJobQueue( + db, + networkService, + statusService, + notificationService, + settingsService, + logger, + QueueStub, + WorkerStub + ); + const inWindow = await jobQueue.isInMaintenanceWindow(1); + expect(inWindow).to.be.false; + }); + it("should return false if not in maintenance window", async () => { + db.getMaintenanceWindowsByMonitorId.returns([ + { + active: false, + start: new Date(Date.now() - 5000).toISOString(), + end: new Date(Date.now() - 10000).toISOString(), + repeat: 1000, + }, + ]); + const jobQueue = await JobQueue.createJobQueue( + db, + networkService, + statusService, + notificationService, + settingsService, + logger, + QueueStub, + WorkerStub + ); + const inWindow = await jobQueue.isInMaintenanceWindow(1); + expect(inWindow).to.be.false; }); }); + + describe("createJobHandler", () => { + it("resolve to an error if an error is thrown within", async () => { + const jobQueue = await JobQueue.createJobQueue( + db, + networkService, + statusService, + notificationService, + settingsService, + logger, + QueueStub, + WorkerStub + ); + jobQueue.isInMaintenanceWindow = sinon.stub().throws("Error"); + try { + const handler = jobQueue.createJobHandler(); + await handler({ data: { _id: 1 } }); + } catch (error) { + expect(error.service).to.equal("JobQueue"); + expect(error.details).to.equal(`Error processing job 1: Error`); + } + }); + + it("should log info if job is in maintenance window", async () => { + const jobQueue = await JobQueue.createJobQueue( + db, + networkService, + statusService, + notificationService, + settingsService, + logger, + QueueStub, + WorkerStub + ); + jobQueue.isInMaintenanceWindow = sinon.stub().returns(true); + const handler = jobQueue.createJobHandler(); + await handler({ data: { _id: 1 } }); + expect(logger.info.calledOnce).to.be.true; + expect(logger.info.firstCall.args[0].message).to.equal( + "Monitor 1 is in maintenance window" + ); + }); + + it("should return if status has not changed", async () => { + const jobQueue = await JobQueue.createJobQueue( + db, + networkService, + statusService, + notificationService, + settingsService, + logger, + QueueStub, + WorkerStub + ); + jobQueue.isInMaintenanceWindow = sinon.stub().returns(false); + statusService.updateStatus = sinon.stub().returns({ statusChanged: false }); + const handler = jobQueue.createJobHandler(); + await handler({ data: { _id: 1 } }); + expect(jobQueue.notificationService.handleNotifications.notCalled).to.be.true; + }); + + it("should return if status has changed, but prevStatus was undefined (monitor paused)", async () => { + const jobQueue = await JobQueue.createJobQueue( + db, + networkService, + statusService, + notificationService, + settingsService, + logger, + QueueStub, + WorkerStub + ); + jobQueue.isInMaintenanceWindow = sinon.stub().returns(false); + statusService.updateStatus = sinon + .stub() + .returns({ statusChanged: true, prevStatus: undefined }); + const handler = jobQueue.createJobHandler(); + await handler({ data: { _id: 1 } }); + expect(jobQueue.notificationService.handleNotifications.notCalled).to.be.true; + }); + it("should call notification service if status changed and monitor was not paused", async () => { + const jobQueue = await JobQueue.createJobQueue( + db, + networkService, + statusService, + notificationService, + settingsService, + logger, + QueueStub, + WorkerStub + ); + jobQueue.isInMaintenanceWindow = sinon.stub().returns(false); + statusService.updateStatus = sinon + .stub() + .returns({ statusChanged: true, prevStatus: false }); + const handler = jobQueue.createJobHandler(); + await handler({ data: { _id: 1 } }); + expect(jobQueue.notificationService.handleNotifications.calledOnce).to.be.true; + }); + }); + describe("getWorkerStats", () => { it("should throw an error if getRepeatable Jobs fails", async () => { const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, QueueStub, @@ -233,6 +406,8 @@ describe("JobQueue", () => { const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, QueueStub, @@ -252,11 +427,14 @@ describe("JobQueue", () => { } }); }); + describe("scaleWorkers", () => { it("should scale workers to 5 if no workers", async () => { const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, QueueStub, @@ -268,6 +446,8 @@ describe("JobQueue", () => { const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, QueueStub, @@ -286,6 +466,8 @@ describe("JobQueue", () => { const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, QueueStub, @@ -309,6 +491,8 @@ describe("JobQueue", () => { const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, QueueStub, @@ -329,6 +513,8 @@ describe("JobQueue", () => { const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, QueueStub, @@ -345,6 +531,8 @@ describe("JobQueue", () => { const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, QueueStub, @@ -357,6 +545,8 @@ describe("JobQueue", () => { const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, QueueStub, @@ -377,6 +567,8 @@ describe("JobQueue", () => { const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, QueueStub, @@ -400,26 +592,10 @@ describe("JobQueue", () => { describe("getJobStats", async () => { it("should return job stats for no jobs", async () => { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); const jobStats = await jobQueue.getJobStats(); expect(jobStats).to.deep.equal({ jobs: [], workers: 5 }); }); it("should return job stats for jobs", async () => { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); jobQueue.queue.getJobs = async () => { return [{ data: { url: "test" }, getState: async () => "completed" }]; }; @@ -430,14 +606,6 @@ describe("JobQueue", () => { }); }); it("should reject with an error if mapping jobs fails", async () => { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); jobQueue.queue.getJobs = async () => { return [ { @@ -457,14 +625,6 @@ describe("JobQueue", () => { } }); it("should reject with an error if mapping jobs fails but respect existing error data", async () => { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); jobQueue.queue.getJobs = async () => { return [ { @@ -490,26 +650,10 @@ describe("JobQueue", () => { describe("addJob", async () => { it("should add a job to the queue", async () => { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); jobQueue.addJob("test", { url: "test" }); expect(jobQueue.queue.jobs.length).to.equal(1); }); it("should reject with an error if adding fails", async () => { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); jobQueue.queue.add = async () => { throw new Error("Error adding job"); }; @@ -522,14 +666,6 @@ describe("JobQueue", () => { } }); it("should reject with an error if adding fails but respect existing error data", async () => { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); jobQueue.queue.add = async () => { const error = new Error("Error adding job"); error.service = "otherService"; @@ -547,14 +683,6 @@ describe("JobQueue", () => { }); describe("deleteJob", async () => { it("should delete a job from the queue", async () => { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); jobQueue.getWorkerStats = sinon.stub().returns({ load: 1, jobs: [{}] }); jobQueue.scaleWorkers = sinon.stub(); const monitor = { _id: 1 }; @@ -567,14 +695,6 @@ describe("JobQueue", () => { // expect(jobQueue.scaleWorkers.calledOnce).to.be.true; }); it("should log an error if job is not found", async () => { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); jobQueue.getWorkerStats = sinon.stub().returns({ load: 1, jobs: [{}] }); jobQueue.scaleWorkers = sinon.stub(); const monitor = { _id: 1 }; @@ -584,14 +704,6 @@ describe("JobQueue", () => { expect(logger.error.calledOnce).to.be.true; }); it("should reject with an error if removeRepeatable fails", async () => { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); jobQueue.queue.removeRepeatable = async () => { const error = new Error("removeRepeatable error"); throw error; @@ -606,14 +718,6 @@ describe("JobQueue", () => { } }); it("should reject with an error if removeRepeatable fails but respect existing error data", async () => { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); jobQueue.queue.removeRepeatable = async () => { const error = new Error("removeRepeatable error"); error.service = "otherService"; @@ -632,14 +736,6 @@ describe("JobQueue", () => { }); describe("getMetrics", () => { it("should return metrics for the job queue", async () => { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); jobQueue.queue.getWaitingCount = async () => 1; jobQueue.queue.getActiveCount = async () => 2; jobQueue.queue.getCompletedCount = async () => 3; @@ -657,14 +753,6 @@ describe("JobQueue", () => { }); }); it("should log an error if metrics operations fail", async () => { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); jobQueue.queue.getWaitingCount = async () => { throw new Error("Error"); }; @@ -676,14 +764,6 @@ describe("JobQueue", () => { describe("obliterate", () => { it("should return true if obliteration is successful", async () => { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); jobQueue.queue.pause = async () => true; jobQueue.getJobs = async () => [{ key: 1, id: 1 }]; jobQueue.queue.removeRepeatableByKey = async () => true; @@ -693,15 +773,6 @@ describe("JobQueue", () => { expect(obliteration).to.be.true; }); it("should throw an error if obliteration fails", async () => { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.getMetrics = async () => { throw new Error("Error"); }; @@ -714,15 +785,6 @@ describe("JobQueue", () => { } }); it("should throw an error if obliteration fails but respect existing error data", async () => { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.getMetrics = async () => { const error = new Error("Error"); error.service = "otherService"; From 013d9fb6070eacd374b11e2622746fdd6b9a5c05 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 14:21:11 +0800 Subject: [PATCH 22/31] update index.js to use new services --- Server/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Server/index.js b/Server/index.js index 6ee0e0249..b83a3d5ce 100644 --- a/Server/index.js +++ b/Server/index.js @@ -40,6 +40,9 @@ import mjml2html from "mjml"; import SettingsService from "./service/settingsService.js"; import AppSettings from "./db/models/AppSettings.js"; +import StatusService from "./service/statusService.js"; +import NotificationService from "./service/notificationService.js"; + import db from "./db/mongo/MongoDB.js"; const SERVICE_NAME = "Server"; @@ -125,10 +128,14 @@ const startApp = async () => { nodemailer, logger ); - const networkService = new NetworkService(db, emailService, axios, ping, logger, http); + const networkService = new NetworkService(axios, ping, logger, http); + const statusService = new StatusService(db, logger); + const notificationService = new NotificationService(emailService, db, logger); const jobQueue = await JobQueue.createJobQueue( db, networkService, + statusService, + notificationService, settingsService, logger, Queue, From 8b126d4d8a5a791f73bc3714ea5053c9f38b213c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 14:33:08 +0800 Subject: [PATCH 23/31] Fix StatusService SERVICE_NAME --- Server/service/statusService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/service/statusService.js b/Server/service/statusService.js index 7d71fab7f..0ec5d681a 100644 --- a/Server/service/statusService.js +++ b/Server/service/statusService.js @@ -8,7 +8,7 @@ class StatusService { constructor(db, logger) { this.db = db; this.logger = logger; - this.SERVICE_NAME + "StatusService"; + this.SERVICE_NAME = "StatusService"; } /** From acdd737a5ead84e1d2fe93895f535a4ed7c91fcc Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 15:34:19 +0800 Subject: [PATCH 24/31] await saving monitor --- Server/service/statusService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/service/statusService.js b/Server/service/statusService.js index 0ec5d681a..c8089db98 100644 --- a/Server/service/statusService.js +++ b/Server/service/statusService.js @@ -41,7 +41,7 @@ class StatusService { const prevStatus = monitor.status; monitor.status = status; - monitor.save(); + await monitor.save(); return { monitor, From 8730ee9de25243ace97af38ec893384489c14fca Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 15:37:21 +0800 Subject: [PATCH 25/31] Fix concurrency error with updating monitor, uncomment out code in pageSpeedMonitorSlice --- Server/db/mongo/modules/monitorModule.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 6aefefad5..b5abf19d1 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -296,9 +296,12 @@ const getMonitorById = async (monitorId) => { const notifications = await Notification.find({ monitorId: monitorId, }); - monitor.notifications = notifications; - const monitorWithNotifications = await monitor.save(); - return monitorWithNotifications; + const updatedMonitor = await Monitor.findByIdAndUpdate( + monitorId, + { notifications }, + { new: true } + ).populate("notifications"); + return updatedMonitor; } catch (error) { error.service = SERVICE_NAME; error.method = "getMonitorById"; From 2fda99dbeff2ba04fcc0b65335f1b83bf72a921e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 18:09:00 +0800 Subject: [PATCH 26/31] Update jobQueue JSdocs --- Server/service/jobQueue.js | 47 +++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index 70f63e74f..3369d815a 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -11,10 +11,15 @@ const SERVICE_NAME = "JobQueue"; */ class JobQueue { /** - * Constructs a new JobQueue - * @constructor - * @param {SettingsService} settingsService - The settings service - * @throws {Error} + * @class JobQueue + * @classdesc Manages job queue and workers. + * + * @param {Object} statusService - Service for handling status updates. + * @param {Object} notificationService - Service for handling notifications. + * @param {Object} settingsService - Service for retrieving settings. + * @param {Object} logger - Logger for logging information. + * @param {Function} Queue - Queue constructor. + * @param {Function} Worker - Worker constructor. */ constructor( statusService, @@ -46,11 +51,18 @@ class JobQueue { } /** - * Static factory method to create a JobQueue - * @static - * @async - * @returns {Promise} - Returns a new JobQueue + * Creates and initializes a JobQueue instance. * + * @param {Object} db - Database service for accessing monitors. + * @param {Object} networkService - Service for network operations. + * @param {Object} statusService - Service for handling status updates. + * @param {Object} notificationService - Service for handling notifications. + * @param {Object} settingsService - Service for retrieving settings. + * @param {Object} logger - Logger for logging information. + * @param {Function} Queue - Queue constructor. + * @param {Function} Worker - Worker constructor. + * @returns {Promise} - The initialized JobQueue instance. + * @throws {Error} - Throws an error if initialization fails. */ static async createJobQueue( db, @@ -89,6 +101,13 @@ class JobQueue { } } + /** + * Checks if the given monitor is in a maintenance window. + * + * @param {string} monitorId - The ID of the monitor to check. + * @returns {Promise} - Returns true if the monitor is in a maintenance window, otherwise false. + * @throws {Error} - Throws an error if the database query fails. + */ async isInMaintenanceWindow(monitorId) { const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(monitorId); // Check for active maintenance window: @@ -286,6 +305,12 @@ class JobQueue { } } + /** + * Retrieves the statistics of jobs and workers. + * + * @returns {Promise} - An object containing job statistics and the number of workers. + * @throws {Error} - Throws an error if the job statistics retrieval fails. + */ async getJobStats() { try { const jobs = await this.queue.getJobs(); @@ -366,6 +391,12 @@ class JobQueue { } } + /** + * Retrieves the metrics of the job queue. + * + * @returns {Promise} - An object containing various job queue metrics. + * @throws {Error} - Throws an error if the metrics retrieval fails. + */ async getMetrics() { try { const metrics = { From 9863aa48e26537b09848d7843c040301d0331ac3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 18:11:08 +0800 Subject: [PATCH 27/31] remove accidental async from test descriptions --- Server/tests/services/jobQueue.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/tests/services/jobQueue.test.js b/Server/tests/services/jobQueue.test.js index a680a2c64..87851983b 100644 --- a/Server/tests/services/jobQueue.test.js +++ b/Server/tests/services/jobQueue.test.js @@ -526,7 +526,7 @@ describe("JobQueue", () => { }); }); - describe("getJobs", async () => { + describe("getJobs", () => { it("should return jobs", async () => { const jobQueue = await JobQueue.createJobQueue( db, @@ -590,7 +590,7 @@ describe("JobQueue", () => { }); }); - describe("getJobStats", async () => { + describe("getJobStats", () => { it("should return job stats for no jobs", async () => { const jobStats = await jobQueue.getJobStats(); expect(jobStats).to.deep.equal({ jobs: [], workers: 5 }); From eed6f8dc83b0b12469fbc90dc9070c0c6a652147 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 18:12:46 +0800 Subject: [PATCH 28/31] remove accidental async from test descriptions --- Server/tests/services/jobQueue.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/tests/services/jobQueue.test.js b/Server/tests/services/jobQueue.test.js index 87851983b..bc798ff02 100644 --- a/Server/tests/services/jobQueue.test.js +++ b/Server/tests/services/jobQueue.test.js @@ -648,7 +648,7 @@ describe("JobQueue", () => { }); }); - describe("addJob", async () => { + describe("addJob", () => { it("should add a job to the queue", async () => { jobQueue.addJob("test", { url: "test" }); expect(jobQueue.queue.jobs.length).to.equal(1); @@ -681,7 +681,7 @@ describe("JobQueue", () => { } }); }); - describe("deleteJob", async () => { + describe("deleteJob", () => { it("should delete a job from the queue", async () => { jobQueue.getWorkerStats = sinon.stub().returns({ load: 1, jobs: [{}] }); jobQueue.scaleWorkers = sinon.stub(); From 8c21b01264d8c14f4dea298dafb30436943bbed2 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 28 Oct 2024 17:15:11 +0000 Subject: [PATCH 29/31] fix(deps): update dependency chart.js to v4.4.6 --- Client/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Client/package-lock.json b/Client/package-lock.json index c2790af71..cd1760e83 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -2805,9 +2805,9 @@ } }, "node_modules/chart.js": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.5.tgz", - "integrity": "sha512-CVVjg1RYTJV9OCC8WeJPMx8gsV8K6WIyIEQUE3ui4AR9Hfgls9URri6Ja3hyMVBbTF8Q2KFa19PE815gWcWhng==", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz", + "integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==", "license": "MIT", "dependencies": { "@kurkle/color": "^0.3.0" From 9559e8178f04d5c90109c785c9016f56ded00814 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 29 Oct 2024 10:51:57 +0800 Subject: [PATCH 30/31] Fix typo == -> === --- Server/service/jobQueue.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index 3369d815a..fcc9cc2b1 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -168,7 +168,7 @@ class JobQueue { await this.statusService.updateStatus(networkResponse); //If status hasn't changed, we're done - if (statusChanged == false) return; + if (statusChanged === false) return; // if prevStatus is undefined, monitor is resuming, we're done if (prevStatus === undefined) return; From c9027f019a37ba80cf8f1e5eea4e20ca5142e0eb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 29 Oct 2024 12:08:47 +0800 Subject: [PATCH 31/31] Fix typo in incident table leading to unapplied filter 'sitler -> filter' --- Client/src/Pages/Incidents/IncidentTable/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index da533c7ba..258bc47ff 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -74,7 +74,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { sortOrder: "desc", limit: null, dateRange: null, - sitler: filter, + filter: filter, page: paginationController.page, rowsPerPage: paginationController.rowsPerPage, });