From 0005b195b1c97d1a9c49a114465a4bdcb3b2da01 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Tue, 22 Oct 2024 09:31:00 -0400 Subject: [PATCH 01/92] - update based review --- Client/src/Validation/validation.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index a6ec7688c..8978f5ffa 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -152,8 +152,8 @@ const advancedSettingsValidation = joi.object({ "string.empty": "Redis host is required", }), redisPort: joi.string().allow('').custom((value, helpers) => { - if(value && isNaN(parseInt(value))){ - return helpers.message("Redit port must be a number") + if(value && Number.isNaN(parseInt(value))){ + return helpers.message("Redis port must be a number") } return value }), From 4966f3fd38e43960e2e6d3fbea78afb205c08883 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Tue, 22 Oct 2024 14:15:11 -0400 Subject: [PATCH 02/92] - Update jwt ttl validation --- Client/src/Pages/AdvancedSettings/index.jsx | 26 ++++++++++- .../Maintenance/CreateMaintenance/index.jsx | 27 ++---------- Client/src/Validation/validation.js | 44 ++++++++++++------- 3 files changed, 57 insertions(+), 40 deletions(-) diff --git a/Client/src/Pages/AdvancedSettings/index.jsx b/Client/src/Pages/AdvancedSettings/index.jsx index ce1c89250..b71123ca7 100644 --- a/Client/src/Pages/AdvancedSettings/index.jsx +++ b/Client/src/Pages/AdvancedSettings/index.jsx @@ -12,6 +12,8 @@ import { useNavigate } from "react-router"; import { getAppSettings, updateAppSettings } from "../../Features/Settings/settingsSlice"; import { useState, useEffect } from "react"; import Select from "../../Components/Inputs/Select"; +import { advancedSettingsValidation } from "../../Validation/validation"; +import { buildErrors, hasValidationErrors } from "../../Validation/error"; const AdvancedSettings = ({ isAdmin }) => { const navigate = useNavigate(); @@ -21,7 +23,7 @@ const AdvancedSettings = ({ isAdmin }) => { navigate("/"); } }, [navigate, isAdmin]); - + const [errors, setErrors] = useState({}); const theme = useTheme(); const { authToken } = useSelector((state) => state.auth); const dispatch = useDispatch(); @@ -75,9 +77,20 @@ const AdvancedSettings = ({ isAdmin }) => { const handleChange = (event) => { const { value, id } = event.target; setLocalSettings({ ...localSettings, [id]: value }); + const { error } = advancedSettingsValidation.validate( + { [id]: value }, + { + abortEarly: false, + } + ); + setErrors((prev) => { + return buildErrors(prev, id, error); + }); }; const handleSave = async () => { + if (hasValidationErrors(localSettings, advancedSettingsValidation, setErrors)) + return; const action = await dispatch( updateAppSettings({ settings: localSettings, authToken }) ); @@ -118,6 +131,7 @@ const AdvancedSettings = ({ isAdmin }) => { label="API URL Host" value={localSettings.apiBaseUrl} onChange={handleChange} + error={errors.apiBaseUrl} /> { items={logItems} value={logItemLookup[localSettings.logLevel]} onChange={handleLogLevel} + onBlur={handleBlur} error={errors.logLevel} /> @@ -160,6 +167,7 @@ const AdvancedSettings = ({ isAdmin }) => { name="systemEmailHost" value={localSettings.systemEmailHost} onChange={handleChange} + onBlur={handleBlur} error={errors.systemEmailHost} /> { name="systemEmailPort" value={localSettings.systemEmailPort.toString()} onChange={handleChange} + onBlur={handleBlur} error={errors.systemEmailPort} /> { name="systemEmailPassword" value={localSettings.systemEmailPassword} onChange={handleChange} + onBlur={handleBlur} error={errors.systemEmailPassword} /> @@ -206,6 +216,7 @@ const AdvancedSettings = ({ isAdmin }) => { name="jwtTTL" value={localSettings.jwtTTL} onChange={handleChange} + onBlur={handleBlur} error={errors.jwtTTL} /> { name="dbType" value={localSettings.dbType} onChange={handleChange} + onBlur={handleBlur} error={errors.dbType} /> { name="redisHost" value={localSettings.redisHost} onChange={handleChange} + onBlur={handleBlur} error={errors.redisHost} /> { name="redisPort" value={localSettings.redisPort.toString()} onChange={handleChange} + onBlur={handleBlur} error={errors.redisPort} /> { name="pagespeedApiKey" value={localSettings.pagespeedApiKey} onChange={handleChange} + onBlur={handleBlur} error={errors.pagespeedApiKey} /> diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index a5c0e029f..83f94db4e 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -147,7 +147,7 @@ const advancedSettingsValidation = joi.object({ .custom((value, helpers) => { // test against regx start with a number followed by number or ., //ends with d or h - const regex = /^[1-9]+\d*(.)*(d|h)$/; + const regex = /^[1-9]+\d*(d|h)$/; const found = value.match(regex); if (!found) { return helpers.message( From a4344b89a23692d4d7cb2a534b90d99044d5ae24 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Wed, 23 Oct 2024 21:03:40 -0400 Subject: [PATCH 11/92] - Add select dropdown for jwt ttl units --- Client/src/Pages/AdvancedSettings/index.jsx | 43 +++++++++++++++++---- Client/src/Validation/validation.js | 20 ++++------ 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/Client/src/Pages/AdvancedSettings/index.jsx b/Client/src/Pages/AdvancedSettings/index.jsx index d5ac22b26..836722db6 100644 --- a/Client/src/Pages/AdvancedSettings/index.jsx +++ b/Client/src/Pages/AdvancedSettings/index.jsx @@ -36,6 +36,7 @@ const AdvancedSettings = ({ isAdmin }) => { systemEmailAddress: "", systemEmailPassword: "", jwtTTL: "", + jwtTTLUnits: "days", dbType: "", redisHost: "", redisPort: "", @@ -68,12 +69,30 @@ const AdvancedSettings = ({ isAdmin }) => { warn: 4, }; + const unitItems = [ + { _id: 1, name: "days" }, + { _id: 2, name: "weeks" }, + { _id: 3, name: "months" } + ]; + + const unitItemLookup = { + days: 1, + weeks: 2, + months: 3, + }; + const handleLogLevel = (e) => { const id = e.target.value; const newLogLevel = logItems.find((item) => item._id === id).name; setLocalSettings({ ...localSettings, logLevel: newLogLevel }); }; + const handleJWTTTLUnits = (e) => { + const id = e.target.value; + const newUnits = unitItems.find((item) => item._id === id).name; + setLocalSettings({ ...localSettings, jwtTTLUnits: newUnits }); + }; + const handleBlur = (event) => { const { value, id } = event.target; const { error } = advancedSettingsValidation.validate( @@ -82,11 +101,9 @@ const AdvancedSettings = ({ isAdmin }) => { abortEarly: false, } ); - if (error) { - setErrors((prev) => { - return buildErrors(prev, id, error); - }); - } + setErrors((prev) => { + return buildErrors(prev, id, error); + }); }; const handleChange = (event) => { const { value, id } = event.target; @@ -209,8 +226,8 @@ const AdvancedSettings = ({ isAdmin }) => { - { onBlur={handleBlur} error={errors.jwtTTL} /> + { value={unitItemLookup[localSettings.jwtTTLUnits]} onChange={handleJWTTTLUnits} onBlur={handleBlur} - error={errors.jwtttlUnits} + error={errors.jwtTTLUnits} /> { if (error) { const newErrors = {}; error.details.forEach((err) => { - newErrors[err.path[0]] = err.message?? "Validation error"; + if ( + ![ + "clientHost", + "refreshTokenSecret", + "dbConnectionString", + "refreshTokenTTL", + "jwtTTL", + ].includes(err.path[0]) + ) { + newErrors[err.path[0]] = err.message ?? "Validation error"; + } }); - setErrors(newErrors); - return true; + if (Object.keys(newErrors).length > 0) { + setErrors(newErrors); + return true; + } else return false; } return false; }; diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 43f3904b9..dfdacd3b1 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -138,18 +138,20 @@ const advancedSettingsValidation = joi.object({ systemEmailPort: joi.string().allow(""), systemEmailAddress: joi.string().allow(""), systemEmailPassword: joi.string().allow(""), - jwtTTL: joi.string().trim().messages({ - "string.empty": "JWT TTL is required.", - }), + jwtTTLNum: joi + .number() + .messages({ + "any.required": "JWT TTL is required.", + }), jwtTTLUnits: joi .string() .trim() .custom((value, helpers) => { - if (value == 0) { + if( !["days", "hours"].includes(value)) { return helpers.message("JWT TTL unit is required."); } return value; - }), + }), dbType: joi.string().trim().messages({ "string.empty": "DB type is required.", }), From 410f3b7ab1e8abf8bb01f723f6dfcf45b8383c8c Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Thu, 24 Oct 2024 13:33:05 -0400 Subject: [PATCH 26/92] - updated js doc --- guides/users-guide/server-settings.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/guides/users-guide/server-settings.md b/guides/users-guide/server-settings.md index 6276f3c42..ae1380dff 100644 --- a/guides/users-guide/server-settings.md +++ b/guides/users-guide/server-settings.md @@ -26,10 +26,12 @@ Here you can setup the following: **Email settings:** Set your host email settings here. These settings are used for sending system emails. -Server settings: Several server settings can be done here. Alternatively, you can add a pagespeed API key to bypass Google's limitations (albeit they are generous about it) +**Server settings:** Several server settings can be done here. Alternatively, you can add a pagespeed API key to bypass Google's limitations (albeit they are generous about it) + +Select a number for jwt TTL and a unit for it (days or hours), the combined result +would be in vercel/ms time format, e.g "99d", this will be sent to server to persist -\ From fd261d3eb12e87aa801c6f709dacc0a8be78bea0 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Thu, 24 Oct 2024 14:13:07 -0400 Subject: [PATCH 27/92] - update number validation --- Client/src/Validation/validation.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index dfdacd3b1..71495cfa1 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -138,20 +138,18 @@ const advancedSettingsValidation = joi.object({ systemEmailPort: joi.string().allow(""), systemEmailAddress: joi.string().allow(""), systemEmailPassword: joi.string().allow(""), - jwtTTLNum: joi - .number() - .messages({ - "any.required": "JWT TTL is required.", - }), + jwtTTLNum: joi.number().messages({ + "number.base": "JWT TTL is required.", + }), jwtTTLUnits: joi .string() .trim() .custom((value, helpers) => { - if( !["days", "hours"].includes(value)) { + if (!["days", "hours"].includes(value)) { return helpers.message("JWT TTL unit is required."); } return value; - }), + }), dbType: joi.string().trim().messages({ "string.empty": "DB type is required.", }), From 1526b50fd2044ca2df035c6f0a41eaab0e3c56c2 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Thu, 24 Oct 2024 14:40:04 -0400 Subject: [PATCH 28/92] - update unitItems with lookup --- Client/src/Pages/AdvancedSettings/index.jsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Client/src/Pages/AdvancedSettings/index.jsx b/Client/src/Pages/AdvancedSettings/index.jsx index 853817544..3f04653cb 100644 --- a/Client/src/Pages/AdvancedSettings/index.jsx +++ b/Client/src/Pages/AdvancedSettings/index.jsx @@ -81,15 +81,14 @@ const AdvancedSettings = ({ isAdmin }) => { warn: 4, }; - const unitItems = [ - { _id: "d", name: "days" }, - { _id: "h", name: "hours" } - ]; - const unitItemLookup = { days: "d", hours: "h", }; + const unitItems = Object.keys(unitItemLookup).map((key) => ({ + _id: unitItemLookup[key], + name: key, + })); const handleLogLevel = (e) => { const id = e.target.value; From 5d9b8bf0515fecc68d04ada8dc8ee3a24121a085 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 25 Oct 2024 09:58:11 +0800 Subject: [PATCH 29/92] remove some testing code accidentally committed --- Client/src/App.jsx | 15 --------------- Client/src/Features/Settings/settingsSlice.js | 1 - Client/src/Utils/NetworkService.js | 1 - 3 files changed, 17 deletions(-) diff --git a/Client/src/App.jsx b/Client/src/App.jsx index ed97b67f9..001758713 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -66,21 +66,6 @@ function App() { }; }, []); - useEffect(() => { - const thing = async () => { - const action = await dispatch( - updateAppSettings({ authToken, settings: { apiBaseUrl: "test" } }) - ); - - if (action.payload.success) { - console.log(action.payload.data); - } else { - console.log(action); - } - }; - thing(); - }, [dispatch, authToken]); - return ( diff --git a/Client/src/Features/Settings/settingsSlice.js b/Client/src/Features/Settings/settingsSlice.js index e5cd6e8b8..283a8502d 100644 --- a/Client/src/Features/Settings/settingsSlice.js +++ b/Client/src/Features/Settings/settingsSlice.js @@ -49,7 +49,6 @@ export const updateAppSettings = createAsyncThunk( systemEmailAddress: settings.systemEmailAddress, systemEmailPassword: settings.systemEmailPassword, }; - console.log(parsedSettings); const res = await networkService.updateAppSettings({ settings: parsedSettings, authToken, diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index 287cba4b4..71cbe9670 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -13,7 +13,6 @@ class NetworkService { this.setBaseUrl(baseURL); this.unsubscribe = store.subscribe(() => { const state = store.getState(); - console.log(state.settings.apiBaseUrl); if (BASE_URL !== undefined) { baseURL = BASE_URL; } else if (state?.settings?.apiBaseUrl ?? null) { From 70872c368d2739a56f2b08ca33ebfc0fe5088310 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 25 Oct 2024 10:48:37 +0800 Subject: [PATCH 30/92] Add status to hardware check model --- Server/db/models/HardwareCheck.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Server/db/models/HardwareCheck.js b/Server/db/models/HardwareCheck.js index 8ff615ec6..5e7f53329 100644 --- a/Server/db/models/HardwareCheck.js +++ b/Server/db/models/HardwareCheck.js @@ -37,6 +37,10 @@ const HardwareCheckSchema = mongoose.Schema( ref: "Monitor", immutable: true, }, + status: { + type: Boolean, + index: true, + }, cpu: { type: cpuSchema, default: () => ({}), From 50c6e32d6e1b6130f70e35fbbf01adf3b3b8bb03 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 25 Oct 2024 10:49:08 +0800 Subject: [PATCH 31/92] Fix variable assignment in --- Server/service/networkService.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Server/service/networkService.js b/Server/service/networkService.js index 2fa470df1..157a29006 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -104,8 +104,7 @@ class NetworkService { monitor: monitor, template: isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate", context: { monitorName: monitor.name, monitorUrl: monitor.url }, - subject: - (subject = `Monitor ${monitor.name} is ${isAlive === true ? "up" : "down"}`), + subject: `Monitor ${monitor.name} is ${isAlive === true ? "up" : "down"}`, }; this.handleNotification(config); } From ca2c269047836324e42c0934253557993cb5fd24 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 25 Oct 2024 11:55:24 +0800 Subject: [PATCH 32/92] 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 33/92] 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 34/92] 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 35/92] 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 36/92] 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 37/92] 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 38/92] 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 39/92] 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 40/92] 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 41/92] 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 77e9457614a3e2d3c94a56adaab79526d2105e57 Mon Sep 17 00:00:00 2001 From: aarya-16 Date: Fri, 25 Oct 2024 14:30:49 +0530 Subject: [PATCH 42/92] Added User theme preference detection -> Used window.matchMedia API to get the user's theme preference. --- Client/src/App.jsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Client/src/App.jsx b/Client/src/App.jsx index ed97b67f9..c8fb60d54 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -39,6 +39,14 @@ import { useDispatch } from "react-redux"; import { getAppSettings, updateAppSettings } from "./Features/Settings/settingsSlice"; import { logger } from "./Utils/Logger"; // Import the logger import { networkService } from "./main"; +import { setMode } from "./Features/UI/uiSlice"; + +const getPreferredTheme = () => { + if(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + return 'dark'; + } + return 'light'; +} function App() { const AdminCheckedRegister = withAdminCheck(Register); const MonitorsWithAdminProp = withAdminProp(Monitors); @@ -58,6 +66,11 @@ function App() { } }, [dispatch, authToken]); + useEffect(() => { + const theme = getPreferredTheme(); + dispatch(setMode(theme)); + },[dispatch]); + // Cleanup useEffect(() => { return () => { From 8c2151dcf325ceb0381593a8e7f5c8d7f84a7567 Mon Sep 17 00:00:00 2001 From: aarya-16 Date: Fri, 25 Oct 2024 14:46:57 +0530 Subject: [PATCH 43/92] Added event listener and older browser support --- Client/src/App.jsx | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Client/src/App.jsx b/Client/src/App.jsx index c8fb60d54..cccd4c6a7 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -42,11 +42,14 @@ import { networkService } from "./main"; import { setMode } from "./Features/UI/uiSlice"; const getPreferredTheme = () => { - if(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { - return 'dark'; + try { + return window?.matchMedia?.('(prefers-color-scheme: dark)')?.matches? 'dark': 'light'; + } + catch { + return 'light'; } - return 'light'; } + function App() { const AdminCheckedRegister = withAdminCheck(Register); const MonitorsWithAdminProp = withAdminProp(Monitors); @@ -69,6 +72,17 @@ function App() { useEffect(() => { const theme = getPreferredTheme(); dispatch(setMode(theme)); + + const mediaQuery = window?.matchMedia?.('(prefers-color-scheme: dark)'); + const handleThemeChange = (e) => { + dispatch(setMode(e.matches ? 'dark' : 'light')); + }; + + mediaQuery?.addEventListener?.('change', handleThemeChange); + + return () => { + mediaQuery?.removeEventListener?.('change', handleThemeChange); + }; },[dispatch]); // Cleanup From 2427bdb78804732071a288e4d69c54f43bae804f Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Fri, 25 Oct 2024 10:00:10 -0400 Subject: [PATCH 44/92] - Fix the wrong label --- Client/src/Pages/AdvancedSettings/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Pages/AdvancedSettings/index.jsx b/Client/src/Pages/AdvancedSettings/index.jsx index 3f04653cb..bfcc66a8b 100644 --- a/Client/src/Pages/AdvancedSettings/index.jsx +++ b/Client/src/Pages/AdvancedSettings/index.jsx @@ -202,7 +202,7 @@ const AdvancedSettings = ({ isAdmin }) => { Date: Fri, 25 Oct 2024 15:52:32 +0000 Subject: [PATCH 45/92] fix(deps): update material-ui monorepo to v7.22.0 --- Client/package-lock.json | 22 +++++++++++----------- Client/package.json | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Client/package-lock.json b/Client/package-lock.json index 29d019536..c2790af71 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -15,8 +15,8 @@ "@mui/lab": "^5.0.0-alpha.170", "@mui/material": "^5.15.16", "@mui/x-charts": "^7.5.1", - "@mui/x-data-grid": "7.21.0", - "@mui/x-date-pickers": "7.21.0", + "@mui/x-data-grid": "7.22.0", + "@mui/x-date-pickers": "7.22.0", "@reduxjs/toolkit": "2.3.0", "axios": "^1.7.4", "chart.js": "^4.4.3", @@ -1400,9 +1400,9 @@ } }, "node_modules/@mui/x-charts": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.21.0.tgz", - "integrity": "sha512-Qv7U1Koo7hxinn1ncbn+Yfcwd8h3bSJDVCpjyKKgO0247kGIAK4ecrBlFHwVLol4bNTY36Ir1prEaA0G1MmUrg==", + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.22.0.tgz", + "integrity": "sha512-B70ix8keyww9CpfdwbsHygQGsgEySCXuHhGrDRiVyFgK+Be4edBWNswbL3ngIp37CHBbWegaYkPp/Q9GDas0AA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", @@ -1458,9 +1458,9 @@ } }, "node_modules/@mui/x-data-grid": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.21.0.tgz", - "integrity": "sha512-0JAiwb2yRuVAd4idzfA64Bs1yn6KfC8VPBH7Njba0ySQB0+Ix+hvkzWQySD96hl7tK5heXbvbJ48pYLvhqS4Vw==", + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.22.0.tgz", + "integrity": "sha512-gXl7+hG0YRNU3YODlPvz6Q/9+EeUsPAWn/u2YMQmYTgwAxeY5QE3lY224VRnwM5v9SfTFheo1kzAKmXPdjb9tQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", @@ -1495,9 +1495,9 @@ } }, "node_modules/@mui/x-date-pickers": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.21.0.tgz", - "integrity": "sha512-WLpuTu3PvhYwd7IAJSuDWr1Zd8c5C8Cc7rpAYCaV5+tGBoEP0C2UKqClMR4F1wTiU2a7x3dzgQzkcgK72yyqDw==", + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.0.tgz", + "integrity": "sha512-hopYo3ORP7ddYKnyBsqAtO2txEe2Zf6cehdikS5b1cqMTGOSL+18b11jfGVod9oipjb9L2JcT/WWkjoifs9Iww==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", diff --git a/Client/package.json b/Client/package.json index 2aa5fb463..44d796143 100644 --- a/Client/package.json +++ b/Client/package.json @@ -17,8 +17,8 @@ "@mui/lab": "^5.0.0-alpha.170", "@mui/material": "^5.15.16", "@mui/x-charts": "^7.5.1", - "@mui/x-data-grid": "7.21.0", - "@mui/x-date-pickers": "7.21.0", + "@mui/x-data-grid": "7.22.0", + "@mui/x-date-pickers": "7.22.0", "@reduxjs/toolkit": "2.3.0", "axios": "^1.7.4", "chart.js": "^4.4.3", From a823bd9372167532602dca84cdd738277bcd0083 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Fri, 25 Oct 2024 14:27:11 -0400 Subject: [PATCH 46/92] - change joi to be optional number for redis port and email port --- Client/src/Validation/validation.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 71495cfa1..08b1632c3 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -135,7 +135,7 @@ const advancedSettingsValidation = joi.object({ }), logLevel: joi.string().valid("debug", "none", "error", "warn").allow(""), systemEmailHost: joi.string().allow(""), - systemEmailPort: joi.string().allow(""), + systemEmailPort: joi.number().allow(null, ""), systemEmailAddress: joi.string().allow(""), systemEmailPassword: joi.string().allow(""), jwtTTLNum: joi.number().messages({ @@ -156,15 +156,7 @@ const advancedSettingsValidation = joi.object({ redisHost: joi.string().trim().messages({ "string.empty": "Redis host is required.", }), - redisPort: joi - .string() - .allow("") - .custom((value, helpers) => { - if (value && Number.isNaN(parseInt(value))) { - return helpers.message("Redis port must be a number."); - } - return value; - }), + redisPort: joi.number().allow(null, ""), pagespeedApiKey: joi.string().allow(""), }); From 39e8ad4308ce37a74c3424b20da5f631b2f0dedc Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Fri, 25 Oct 2024 14:37:43 -0400 Subject: [PATCH 47/92] - Fix empty port number case --- Client/src/Pages/AdvancedSettings/index.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Client/src/Pages/AdvancedSettings/index.jsx b/Client/src/Pages/AdvancedSettings/index.jsx index bfcc66a8b..bc9affad6 100644 --- a/Client/src/Pages/AdvancedSettings/index.jsx +++ b/Client/src/Pages/AdvancedSettings/index.jsx @@ -204,7 +204,7 @@ const AdvancedSettings = ({ isAdmin }) => { id="systemEmailPort" label="System email port" name="systemEmailPort" - value={localSettings.systemEmailPort.toString()} + value={localSettings.systemEmailPort?.toString()} onChange={handleChange} onBlur={handleBlur} error={errors.systemEmailPort} @@ -285,7 +285,7 @@ const AdvancedSettings = ({ isAdmin }) => { id="redisPort" label="Redis port" name="redisPort" - value={localSettings.redisPort.toString()} + value={localSettings.redisPort?.toString()} onChange={handleChange} onBlur={handleBlur} error={errors.redisPort} From f3e33d1ebd9796c1a2ebb1710af4e909059a8fd7 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 25 Oct 2024 18:43:47 +0000 Subject: [PATCH 48/92] fix(deps): update dependency mongoose to v8.7.3 --- Server/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index 46faaa3e0..73f2316c8 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -4369,9 +4369,9 @@ } }, "node_modules/mongoose": { - "version": "8.7.2", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.2.tgz", - "integrity": "sha512-Ok4VzMds9p5G3ZSUhmvBm1GdxanbzhS29jpSn02SPj+IXEVFnIdfwAlHHXWkyNscZKlcn8GuMi68FH++jo0flg==", + "version": "8.7.3", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.3.tgz", + "integrity": "sha512-Xl6+dzU5ZpEcDoJ8/AyrIdAwTY099QwpolvV73PIytpK13XqwllLq/9XeVzzLEQgmyvwBVGVgjmMrKbuezxrIA==", "license": "MIT", "dependencies": { "bson": "^6.7.0", From f61cb275ffd2a3c2f2897be7ef620c1762e325f7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sat, 26 Oct 2024 09:36:15 +0800 Subject: [PATCH 49/92] email host -> System email host --- Client/src/Pages/AdvancedSettings/index.jsx | 61 +++++++++++---------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/Client/src/Pages/AdvancedSettings/index.jsx b/Client/src/Pages/AdvancedSettings/index.jsx index bc9affad6..a830159b8 100644 --- a/Client/src/Pages/AdvancedSettings/index.jsx +++ b/Client/src/Pages/AdvancedSettings/index.jsx @@ -58,7 +58,7 @@ const AdvancedSettings = ({ isAdmin }) => { const getSettings = async () => { const action = await dispatch(getAppSettings({ authToken })); if (action.payload.success) { - parseJWTTTL(action.payload.data) + parseJWTTTL(action.payload.data); setLocalSettings(action.payload.data); } else { createToast({ body: "Failed to get settings" }); @@ -100,7 +100,7 @@ const AdvancedSettings = ({ isAdmin }) => { const id = e.target.value; const newUnits = unitItems.find((item) => item._id === id).name; setLocalSettings({ ...localSettings, jwtTTLUnits: newUnits }); - }; + }; const handleBlur = (event) => { const { value, id } = event.target; @@ -116,7 +116,7 @@ const AdvancedSettings = ({ isAdmin }) => { }; const handleChange = (event) => { const { value, id } = event.target; - setLocalSettings({ ...localSettings, [id]: value }); + setLocalSettings({ ...localSettings, [id]: value }); }; const handleSave = async () => { @@ -125,12 +125,12 @@ const AdvancedSettings = ({ isAdmin }) => { if (hasValidationErrors(localSettings, advancedSettingsValidation, setErrors)) { return; } - const action = await dispatch( + const action = await dispatch( updateAppSettings({ settings: localSettings, authToken }) ); let body = ""; if (action.payload.success) { - parseJWTTTL(action.payload.data) + parseJWTTTL(action.payload.data); setLocalSettings(action.payload.data); body = "Settings saved successfully"; } else { @@ -192,7 +192,7 @@ const AdvancedSettings = ({ isAdmin }) => { { - - + Date: Sat, 26 Oct 2024 18:45:51 +0800 Subject: [PATCH 50/92] Revert moving notification check to validation, FE always sends an empty notification array --- Server/controllers/monitorController.js | 11 ++++++----- Server/validation/joi.js | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 9bdce12ef..06dcb9998 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -225,14 +225,15 @@ const createMonitor = async (req, res, next) => { const notifications = req.body.notifications; const monitor = await req.db.createMonitor(req, res); - monitor.notifications = - notifications && - (await Promise.all( + if (notifications && notifications.length > 0) { + monitor.notifications = await Promise.all( notifications.map(async (notification) => { notification.monitorId = monitor._id; - await req.db.createNotification(notification); + return await req.db.createNotification(notification); }) - )); + ); + } + await monitor.save(); // Add monitor to job queue req.jobQueue.addJob(monitor._id, monitor); diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 48edc6f9f..fece02926 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -227,7 +227,7 @@ const createMonitorBodyValidation = joi.object({ usage_memory: joi.number(), usage_disk: joi.number(), }), - notifications: joi.array().items(joi.object()).min(1), + notifications: joi.array().items(joi.object()), }); const editMonitorBodyValidation = joi.object({ From 0c3d8245e9ce4f162b8297bab51173f55275475b Mon Sep 17 00:00:00 2001 From: aarya-16 Date: Sun, 27 Oct 2024 20:33:33 +0530 Subject: [PATCH 51/92] Theme preference logic moved to UISlice --- Client/src/App.jsx | 27 --------------------------- Client/src/Features/UI/uiSlice.js | 2 +- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/Client/src/App.jsx b/Client/src/App.jsx index cccd4c6a7..ed97b67f9 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -39,17 +39,6 @@ import { useDispatch } from "react-redux"; import { getAppSettings, updateAppSettings } from "./Features/Settings/settingsSlice"; import { logger } from "./Utils/Logger"; // Import the logger import { networkService } from "./main"; -import { setMode } from "./Features/UI/uiSlice"; - -const getPreferredTheme = () => { - try { - return window?.matchMedia?.('(prefers-color-scheme: dark)')?.matches? 'dark': 'light'; - } - catch { - return 'light'; - } -} - function App() { const AdminCheckedRegister = withAdminCheck(Register); const MonitorsWithAdminProp = withAdminProp(Monitors); @@ -69,22 +58,6 @@ function App() { } }, [dispatch, authToken]); - useEffect(() => { - const theme = getPreferredTheme(); - dispatch(setMode(theme)); - - const mediaQuery = window?.matchMedia?.('(prefers-color-scheme: dark)'); - const handleThemeChange = (e) => { - dispatch(setMode(e.matches ? 'dark' : 'light')); - }; - - mediaQuery?.addEventListener?.('change', handleThemeChange); - - return () => { - mediaQuery?.removeEventListener?.('change', handleThemeChange); - }; - },[dispatch]); - // Cleanup useEffect(() => { return () => { diff --git a/Client/src/Features/UI/uiSlice.js b/Client/src/Features/UI/uiSlice.js index 0a938391a..d77e3d2c6 100644 --- a/Client/src/Features/UI/uiSlice.js +++ b/Client/src/Features/UI/uiSlice.js @@ -15,7 +15,7 @@ const initialState = { sidebar: { collapsed: false, }, - mode: "light", + mode: window?.matchMedia?.('(prefers-color-scheme: dark)')?.matches? "dark": "light", greeting: { index: 0, lastUpdate: null }, timezone: "America/Toronto", }; From 39802365803eb3cb7861f6a4343473c76eed0300 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 28 Oct 2024 14:17:10 +0800 Subject: [PATCH 52/92] 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 53/92] 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 54/92] 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 55/92] 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 56/92] 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 57/92] 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 58/92] 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 59/92] 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 60/92] 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 61/92] 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 62/92] 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 63/92] 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 64/92] 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 65/92] 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 66/92] 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 67/92] 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 68/92] 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 69/92] 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 3b1a90ab5224cf04508573cbc3ef1438d876df78 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 28 Oct 2024 11:02:05 +0000 Subject: [PATCH 70/92] fix(deps): update dependency nodemailer to v6.9.16 --- Server/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index 73f2316c8..128fc6c5b 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -4624,9 +4624,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz", - "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==", + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", + "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", "license": "MIT-0", "engines": { "node": ">=6.0.0" From 7a559977ca696d7b50a907f9445f57560e152a8c Mon Sep 17 00:00:00 2001 From: aarya-16 Date: Mon, 28 Oct 2024 17:03:52 +0530 Subject: [PATCH 71/92] Shifted mode ternary to a variable --- Client/src/Features/UI/uiSlice.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Client/src/Features/UI/uiSlice.js b/Client/src/Features/UI/uiSlice.js index d77e3d2c6..872691bbf 100644 --- a/Client/src/Features/UI/uiSlice.js +++ b/Client/src/Features/UI/uiSlice.js @@ -1,5 +1,7 @@ import { createSlice } from "@reduxjs/toolkit"; +const initialMode = window?.matchMedia?.('(prefers-color-scheme: dark)')?.matches ? "dark" : "light"; + // Initial state for UI settings. // Add more settings as needed (e.g., theme preferences, user settings) const initialState = { @@ -15,7 +17,7 @@ const initialState = { sidebar: { collapsed: false, }, - mode: window?.matchMedia?.('(prefers-color-scheme: dark)')?.matches? "dark": "light", + mode: initialMode, greeting: { index: 0, lastUpdate: null }, timezone: "America/Toronto", }; From 8c21b01264d8c14f4dea298dafb30436943bbed2 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 28 Oct 2024 17:15:11 +0000 Subject: [PATCH 72/92] 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 73/92] 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 74/92] 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, }); From b1abe8763310320b61860f2d382dbeaf1af5ebb0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 29 Oct 2024 13:46:51 +0800 Subject: [PATCH 75/92] Update HardwareCheck model to include all standard fields --- Server/db/models/HardwareCheck.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Server/db/models/HardwareCheck.js b/Server/db/models/HardwareCheck.js index 5e7f53329..c307b9804 100644 --- a/Server/db/models/HardwareCheck.js +++ b/Server/db/models/HardwareCheck.js @@ -36,11 +36,31 @@ const HardwareCheckSchema = mongoose.Schema( type: mongoose.Schema.Types.ObjectId, ref: "Monitor", immutable: true, + index: true, }, + status: { type: Boolean, index: true, }, + + responseTime: { + type: Number, + }, + + statusCode: { + type: Number, + index: true, + }, + + message: { + type: String, + }, + expiry: { + type: Date, + default: Date.now, + expires: 60 * 60 * 24 * 30, // 30 days + }, cpu: { type: cpuSchema, default: () => ({}), From 7fd63f8004575118ebee471e18f88527eb8c4ce2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 29 Oct 2024 13:47:30 +0800 Subject: [PATCH 76/92] Add option secret to Monitor for monitors that require authentication --- Server/db/models/Monitor.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Server/db/models/Monitor.js b/Server/db/models/Monitor.js index c4dcb649c..111c48d03 100644 --- a/Server/db/models/Monitor.js +++ b/Server/db/models/Monitor.js @@ -61,6 +61,9 @@ const MonitorSchema = mongoose.Schema( ref: "Notification", }, ], + secret: { + type: String, + }, }, { timestamps: true, From 0c321cf484455fec664b23da0682bbe03816e414 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 29 Oct 2024 13:51:00 +0800 Subject: [PATCH 77/92] Add optional authorization config to HTTP request. Delegate handleHardware to handleHttp as they are identical --- Server/service/jobQueue.js | 1 - Server/service/networkService.js | 20 ++++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index fcc9cc2b1..6d46052f1 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -163,7 +163,6 @@ class JobQueue { // Get the current status const networkResponse = await this.networkService.getStatus(job); // Handle status change - const { monitor, statusChanged, prevStatus } = await this.statusService.updateStatus(networkResponse); diff --git a/Server/service/networkService.js b/Server/service/networkService.js index efdd2e865..b4efab0f4 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -1,5 +1,4 @@ import { errorMessages, successMessages } from "../utils/messages.js"; - /** * Constructs a new NetworkService instance. * @@ -94,6 +93,7 @@ class NetworkService { * @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. + * @param {string} [job.data.secret] - Secret for authorization if provided. * @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". @@ -105,13 +105,18 @@ class NetworkService { */ async requestHttp(job) { const url = job.data.url; + const config = {}; + + job.data.secret !== undefined && + (config.headers = { Authorization: `Bearer ${job.data.secret}` }); + const { response, responseTime, error } = await this.timeRequest(() => - this.axios.get(url) + this.axios.get(url, config) ); const httpResponse = { monitorId: job.data._id, - type: "http", + type: job.data.type, responseTime, payload: response?.data, }; @@ -121,6 +126,7 @@ class NetworkService { httpResponse.code = code; httpResponse.status = false; httpResponse.message = this.http.STATUS_CODES[code] || "Network Error"; + return httpResponse; } httpResponse.status = true; @@ -155,7 +161,7 @@ class NetworkService { const pagespeedResponse = { monitorId: job.data._id, - type: "pagespeed", + type: job.data.type, responseTime, payload: response?.data, }; @@ -174,7 +180,9 @@ class NetworkService { return pagespeedResponse; } - async requestHandleHardware(job) {} + async requestHardware(job) { + return this.requestHttp(job); + } /** * Gets the status of a job based on its type and returns the appropriate response. @@ -194,7 +202,7 @@ class NetworkService { case this.TYPE_PAGESPEED: return await this.requestPagespeed(job); case this.TYPE_HARDWARE: - return await this.requestHandleHardware(job); + return await this.requestHardware(job); default: this.logger.error({ message: `Unsupported type: ${job.data.type}`, From 10a438a67729a3dd2cc1dfc4564e2ee19aaaf090 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 29 Oct 2024 13:51:31 +0800 Subject: [PATCH 78/92] Implement handling of hardwareChecks --- Server/service/statusService.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Server/service/statusService.js b/Server/service/statusService.js index c8089db98..694b0ed72 100644 --- a/Server/service/statusService.js +++ b/Server/service/statusService.js @@ -99,6 +99,13 @@ class StatusService { check.performance = (categories.performance?.score || 0) * 100; check.audits = { cls, si, fcp, lcp, tbt }; } + + if (type === "hardware") { + check.cpu = payload.cpu; + check.memory = payload.memory; + check.disk = payload.disk; + check.host = payload.host; + } return check; }; @@ -121,6 +128,7 @@ class StatusService { http: this.db.createCheck, ping: this.db.createCheck, pagespeed: this.db.createPageSpeedCheck, + hardware: this.db.createHardwareCheck, }; const operation = operationMap[networkResponse.type]; const check = this.buildCheck(networkResponse); @@ -130,7 +138,7 @@ class StatusService { message: error.message, service: this.SERVICE_NAME, method: "insertCheck", - details: `Error inserting check for monitor: ${networkResponse.monitorId}`, + details: `Error inserting check for monitor: ${networkResponse?.monitorId}`, stack: error.stack, }); } From a8060a72ab937e1748e185bcb65381485eaa5be0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 29 Oct 2024 13:51:50 +0800 Subject: [PATCH 79/92] Add validation for secret to Create and Edit Monitor validation --- Server/validation/joi.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 0b85a4557..cd69a959d 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -228,6 +228,7 @@ const createMonitorBodyValidation = joi.object({ usage_disk: joi.number(), }), notifications: joi.array().items(joi.object()), + secret: joi.string(), }); const editMonitorBodyValidation = joi.object({ @@ -235,6 +236,7 @@ const editMonitorBodyValidation = joi.object({ description: joi.string(), interval: joi.number(), notifications: joi.array().items(joi.object()), + secret: joi.string(), }); const pauseMonitorParamValidation = joi.object({ From e940a0d1519f3974e0f5b72a293e3f5551a34041 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 29 Oct 2024 13:51:57 +0800 Subject: [PATCH 80/92] update tests --- Server/tests/services/networkService.test.js | 71 +++++++++++++++++--- Server/tests/services/statusService.test.js | 20 ++++++ 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/Server/tests/services/networkService.test.js b/Server/tests/services/networkService.test.js index dbc2986ee..f27f0ef79 100644 --- a/Server/tests/services/networkService.test.js +++ b/Server/tests/services/networkService.test.js @@ -2,7 +2,6 @@ import sinon from "sinon"; import NetworkService from "../../service/networkService.js"; import { expect } from "chai"; import http from "http"; -import exp from "constants"; describe("Network Service", () => { let axios, ping, logger, networkService; @@ -74,7 +73,7 @@ describe("Network Service", () => { }); describe("requestHttp", () => { it("should return a response object if http successful", async () => { - const job = { data: { url: "http://test.com", _id: "123" } }; + const job = { data: { url: "http://test.com", _id: "123", type: "http" } }; const httpResult = await networkService.requestHttp(job); expect(httpResult.monitorId).to.equal("123"); expect(httpResult.type).to.equal("http"); @@ -87,7 +86,7 @@ describe("Network Service", () => { networkService.timeRequest = sinon .stub() .resolves({ response: null, responseTime: 1, error }); - const job = { data: { url: "http://test.com", _id: "123" } }; + const job = { data: { url: "http://test.com", _id: "123", type: "http" } }; const httpResult = await networkService.requestHttp(job); expect(httpResult.monitorId).to.equal("123"); expect(httpResult.type).to.equal("http"); @@ -101,7 +100,7 @@ describe("Network Service", () => { networkService.timeRequest = sinon .stub() .resolves({ response: null, responseTime: 1, error }); - const job = { data: { url: "http://test.com", _id: "123" } }; + const job = { data: { url: "http://test.com", _id: "123", type: "http" } }; const httpResult = await networkService.requestHttp(job); expect(httpResult.monitorId).to.equal("123"); expect(httpResult.type).to.equal("http"); @@ -113,7 +112,7 @@ describe("Network Service", () => { describe("requestPagespeed", () => { it("should return a response object if pagespeed successful", async () => { - const job = { data: { url: "http://test.com", _id: "123" } }; + const job = { data: { url: "http://test.com", _id: "123", type: "pagespeed" } }; const pagespeedResult = await networkService.requestPagespeed(job); expect(pagespeedResult.monitorId).to.equal("123"); expect(pagespeedResult.type).to.equal("pagespeed"); @@ -126,7 +125,7 @@ describe("Network Service", () => { networkService.timeRequest = sinon .stub() .resolves({ response: null, responseTime: 1, error }); - const job = { data: { url: "http://test.com", _id: "123" } }; + const job = { data: { url: "http://test.com", _id: "123", type: "pagespeed" } }; const pagespeedResult = await networkService.requestPagespeed(job); expect(pagespeedResult.monitorId).to.equal("123"); expect(pagespeedResult.type).to.equal("pagespeed"); @@ -140,7 +139,7 @@ describe("Network Service", () => { networkService.timeRequest = sinon .stub() .resolves({ response: null, responseTime: 1, error }); - const job = { data: { url: "http://test.com", _id: "123" } }; + const job = { data: { url: "http://test.com", _id: "123", type: "pagespeed" } }; const pagespeedResult = await networkService.requestPagespeed(job); expect(pagespeedResult.monitorId).to.equal("123"); expect(pagespeedResult.type).to.equal("pagespeed"); @@ -150,6 +149,60 @@ describe("Network Service", () => { }); }); + describe("requestHardware", () => { + it("should return a response object if hardware successful", async () => { + const job = { data: { url: "http://test.com", _id: "123", type: "hardware" } }; + const httpResult = await networkService.requestHardware(job); + expect(httpResult.monitorId).to.equal("123"); + expect(httpResult.type).to.equal("hardware"); + expect(httpResult.responseTime).to.be.a("number"); + expect(httpResult.status).to.be.true; + }); + it("should return a response object if hardware successful and job jas a secret", async () => { + const job = { + data: { + url: "http://test.com", + _id: "123", + type: "hardware", + secret: "my_secret", + }, + }; + const httpResult = await networkService.requestHardware(job); + expect(httpResult.monitorId).to.equal("123"); + expect(httpResult.type).to.equal("hardware"); + expect(httpResult.responseTime).to.be.a("number"); + expect(httpResult.status).to.be.true; + }); + it("should return a response object if hardware 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", type: "hardware" } }; + const httpResult = await networkService.requestHardware(job); + expect(httpResult.monitorId).to.equal("123"); + expect(httpResult.type).to.equal("hardware"); + 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 hardware 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", type: "hardware" } }; + const httpResult = await networkService.requestHardware(job); + expect(httpResult.monitorId).to.equal("123"); + expect(httpResult.type).to.equal("hardware"); + expect(httpResult.responseTime).to.be.a("number"); + expect(httpResult.status).to.be.false; + expect(httpResult.code).to.equal(networkService.NETWORK_ERROR); + }); + }); + describe("getStatus", () => { beforeEach(() => { networkService.requestPing = sinon.stub(); @@ -179,10 +232,10 @@ describe("Network Service", () => { expect(networkService.requestHttp.notCalled).to.be.true; expect(networkService.requestPagespeed.calledOnce).to.be.true; }); - it("should call requestHandleHardware if type is hardware", () => { + it("should call requestHardware if type is hardware", () => { networkService.getStatus({ data: { type: "hardware" } }); + expect(networkService.requestHardware.calledOnce).to.be.true; 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", () => { diff --git a/Server/tests/services/statusService.test.js b/Server/tests/services/statusService.test.js index df74ef715..19b0edf19 100644 --- a/Server/tests/services/statusService.test.js +++ b/Server/tests/services/statusService.test.js @@ -173,6 +173,26 @@ describe("StatusService", () => { tbt: 0, }); }); + it("should build a check for hardware type", () => { + const check = statusService.buildCheck({ + monitorId: "test", + type: "hardware", + status: true, + responseTime: 100, + code: 200, + message: "Test message", + payload: { cpu: "cpu", memory: "memory", disk: "disk", host: "host" }, + }); + expect(check.monitorId).to.equal("test"); + expect(check.status).to.be.true; + expect(check.statusCode).to.equal(200); + expect(check.responseTime).to.equal(100); + expect(check.message).to.equal("Test message"); + expect(check.cpu).to.equal("cpu"); + expect(check.memory).to.equal("memory"); + expect(check.disk).to.equal("disk"); + expect(check.host).to.equal("host"); + }); }); describe("insertCheck", () => { it("should log an error if one is thrown", async () => { From b417b72450bbc434600ce5eb4ed2ff46a90b9452 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 29 Oct 2024 14:47:54 +0800 Subject: [PATCH 81/92] Refactor requestPagespeed to simply call requestHttp with an updated job object --- Server/service/networkService.js | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/Server/service/networkService.js b/Server/service/networkService.js index b4efab0f4..fa665e1ea 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -126,7 +126,6 @@ class NetworkService { httpResponse.code = code; httpResponse.status = false; httpResponse.message = this.http.STATUS_CODES[code] || "Network Error"; - return httpResponse; } httpResponse.status = true; @@ -153,31 +152,10 @@ class NetworkService { */ 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: job.data.type, - 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; + const updatedJob = { ...job }; + const pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`; + updatedJob.data.url = pagespeedUrl; + return this.requestHttp(updatedJob); } async requestHardware(job) { From 29c83f5206ae691a7e2a4f091576dd637b0536c4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 29 Oct 2024 14:48:19 +0800 Subject: [PATCH 82/92] Safely access payload values --- Server/service/statusService.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Server/service/statusService.js b/Server/service/statusService.js index 694b0ed72..64705e122 100644 --- a/Server/service/statusService.js +++ b/Server/service/statusService.js @@ -101,10 +101,10 @@ class StatusService { } if (type === "hardware") { - check.cpu = payload.cpu; - check.memory = payload.memory; - check.disk = payload.disk; - check.host = payload.host; + check.cpu = payload?.cpu ?? {}; + check.memory = payload?.memory ?? {}; + check.disk = payload?.disk ?? {}; + check.host = payload?.host ?? {}; } return check; }; From 92fc6ac4637f0c6f8a71f415eec3deba51c517ab Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 29 Oct 2024 22:24:07 +0000 Subject: [PATCH 83/92] chore(deps): update dependency mocha to v10.8.1 --- Server/package-lock.json | 8 ++++---- Server/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index 128fc6c5b..fefc83f9d 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -35,7 +35,7 @@ "c8": "10.1.2", "chai": "5.1.2", "esm": "3.2.25", - "mocha": "10.7.3", + "mocha": "10.8.1", "nodemon": "3.1.7", "prettier": "^3.3.3", "sinon": "19.0.2" @@ -4129,9 +4129,9 @@ } }, "node_modules/mocha": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.3.tgz", - "integrity": "sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==", + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.1.tgz", + "integrity": "sha512-WxSpEWgF03HfgNKBuysfK40DUaOSVX5zxgLDoieMGO+zyE69iq2eQ1vBypvIJ5mOPKpuVAqWiTbt4Orj7L6wVw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/Server/package.json b/Server/package.json index d0aa1d0d1..512a1e01a 100644 --- a/Server/package.json +++ b/Server/package.json @@ -38,7 +38,7 @@ "c8": "10.1.2", "chai": "5.1.2", "esm": "3.2.25", - "mocha": "10.7.3", + "mocha": "10.8.1", "nodemon": "3.1.7", "prettier": "^3.3.3", "sinon": "19.0.2" From d4458e6cf17d777c6d682fab5743974ae3d11b5d Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 30 Oct 2024 01:06:12 +0000 Subject: [PATCH 84/92] fix(deps): update dependency recharts to v2.13.1 --- Client/package-lock.json | 8 ++++---- Client/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Client/package-lock.json b/Client/package-lock.json index cd1760e83..8d816f4dc 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -29,7 +29,7 @@ "react-router": "^6.23.0", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", - "recharts": "2.13.0", + "recharts": "2.13.1", "redux-persist": "6.0.0", "vite-plugin-svgr": "^4.2.0" }, @@ -5518,9 +5518,9 @@ } }, "node_modules/recharts": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.0.tgz", - "integrity": "sha512-sbfxjWQ+oLWSZEWmvbq/DFVdeRLqqA6d0CDjKx2PkxVVdoXo16jvENCE+u/x7HxOO+/fwx//nYRwb8p8X6s/lQ==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.1.tgz", + "integrity": "sha512-87LdsmgK/MHLmWQfTC6yDysno2cOigi/+2KRCwy0D8NDu1IOdtTGS8lMovA0VIvJ7kf3zdp1IiwznHZWSPJhYw==", "license": "MIT", "dependencies": { "clsx": "^2.0.0", diff --git a/Client/package.json b/Client/package.json index 7e649e1f7..8fe31f8da 100644 --- a/Client/package.json +++ b/Client/package.json @@ -32,7 +32,7 @@ "react-router": "^6.23.0", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", - "recharts": "2.13.0", + "recharts": "2.13.1", "redux-persist": "6.0.0", "vite-plugin-svgr": "^4.2.0" }, From b1d794771985f0e43a3763c8e8bacfead0e9723d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 30 Oct 2024 09:07:47 +0800 Subject: [PATCH 85/92] fix typo jas -> has --- Server/tests/services/networkService.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/tests/services/networkService.test.js b/Server/tests/services/networkService.test.js index f27f0ef79..4e48524e2 100644 --- a/Server/tests/services/networkService.test.js +++ b/Server/tests/services/networkService.test.js @@ -158,7 +158,7 @@ describe("Network Service", () => { expect(httpResult.responseTime).to.be.a("number"); expect(httpResult.status).to.be.true; }); - it("should return a response object if hardware successful and job jas a secret", async () => { + it("should return a response object if hardware successful and job has a secret", async () => { const job = { data: { url: "http://test.com", From a7c00e9383dc48cf18ec1e559e8e2852e09d8ec3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 30 Oct 2024 09:49:10 +0800 Subject: [PATCH 86/92] Refactor models to extend a base check model --- Server/db/models/Check.js | 127 ++++++++++++++--------------- Server/db/models/HardwareCheck.js | 34 +------- Server/db/models/PageSpeedCheck.js | 18 ++-- 3 files changed, 71 insertions(+), 108 deletions(-) diff --git a/Server/db/models/Check.js b/Server/db/models/Check.js index 320ddefe9..ad50c208a 100644 --- a/Server/db/models/Check.js +++ b/Server/db/models/Check.js @@ -1,6 +1,63 @@ import mongoose from "mongoose"; -import EmailService from "../../service/emailService.js"; -import Notification from "./Notification.js"; + +const BaseCheckSchema = mongoose.Schema({ + /** + * Reference to the associated Monitor document. + * + * @type {mongoose.Schema.Types.ObjectId} + */ + monitorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Monitor", + immutable: true, + index: true, + }, + /** + * Status of the check (true for up, false for down). + * + * @type {Boolean} + */ + status: { + type: Boolean, + index: true, + }, + /** + * Response time of the check in milliseconds. + * + * @type {Number} + */ + responseTime: { + type: Number, + }, + /** + * HTTP status code received during the check. + * + * @type {Number} + */ + statusCode: { + type: Number, + index: true, + }, + /** + * Message or description of the check result. + * + * @type {String} + */ + message: { + type: String, + }, + /** + * Expiry date of the check, auto-calculated to expire after 30 days. + * + * @type {Date} + */ + + expiry: { + type: Date, + default: Date.now, + expires: 60 * 60 * 24 * 30, // 30 days + }, +}); /** * Check Schema for MongoDB collection. @@ -8,69 +65,7 @@ import Notification from "./Notification.js"; * Represents a check associated with a monitor, storing information * about the status and response of a particular check event. */ -const CheckSchema = mongoose.Schema( - { - /** - * Reference to the associated Monitor document. - * - * @type {mongoose.Schema.Types.ObjectId} - */ - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - immutable: true, - index: true, - }, - /** - * Status of the check (true for up, false for down). - * - * @type {Boolean} - */ - status: { - type: Boolean, - index: true, - }, - /** - * Response time of the check in milliseconds. - * - * @type {Number} - */ - responseTime: { - type: Number, - }, - /** - * HTTP status code received during the check. - * - * @type {Number} - */ - statusCode: { - type: Number, - index: true, - }, - /** - * Message or description of the check result. - * - * @type {String} - */ - message: { - type: String, - }, - /** - * Expiry date of the check, auto-calculated to expire after 30 days. - * - * @type {Date} - */ - - expiry: { - type: Date, - default: Date.now, - expires: 60 * 60 * 24 * 30, // 30 days - }, - }, - { - timestamps: true, // Adds createdAt and updatedAt timestamps - } -); - +const CheckSchema = mongoose.Schema({ ...BaseCheckSchema.obj }, { timestamps: true }); CheckSchema.index({ createdAt: 1 }); export default mongoose.model("Check", CheckSchema); +export { BaseCheckSchema }; diff --git a/Server/db/models/HardwareCheck.js b/Server/db/models/HardwareCheck.js index c307b9804..75e4ac584 100644 --- a/Server/db/models/HardwareCheck.js +++ b/Server/db/models/HardwareCheck.js @@ -1,5 +1,5 @@ import mongoose from "mongoose"; - +import { BaseCheckSchema } from "./Check.js"; const cpuSchema = mongoose.Schema({ physical_core: { type: Number, default: 0 }, logical_core: { type: Number, default: 0 }, @@ -32,35 +32,7 @@ const hostSchema = mongoose.Schema({ const HardwareCheckSchema = mongoose.Schema( { - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - immutable: true, - index: true, - }, - - status: { - type: Boolean, - index: true, - }, - - responseTime: { - type: Number, - }, - - statusCode: { - type: Number, - index: true, - }, - - message: { - type: String, - }, - expiry: { - type: Date, - default: Date.now, - expires: 60 * 60 * 24 * 30, // 30 days - }, + ...BaseCheckSchema.obj, cpu: { type: cpuSchema, default: () => ({}), @@ -81,4 +53,6 @@ const HardwareCheckSchema = mongoose.Schema( { timestamps: true } ); +HardwareCheckSchema.index({ createdAt: 1 }); + export default mongoose.model("HardwareCheck", HardwareCheckSchema); diff --git a/Server/db/models/PageSpeedCheck.js b/Server/db/models/PageSpeedCheck.js index 344cc685d..4ebc60280 100644 --- a/Server/db/models/PageSpeedCheck.js +++ b/Server/db/models/PageSpeedCheck.js @@ -1,5 +1,7 @@ import mongoose from "mongoose"; +import { BaseCheckSchema } from "./Check.js"; import logger from "../../utils/logger.js"; +import { time } from "console"; const AuditSchema = mongoose.Schema({ id: { type: String, required: true }, title: { type: String, required: true }, @@ -46,15 +48,7 @@ const AuditsSchema = mongoose.Schema({ const PageSpeedCheck = mongoose.Schema( { - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - immutable: true, - }, - status: { - type: Boolean, - required: true, - }, + ...BaseCheckSchema.obj, accessibility: { type: Number, required: true, @@ -76,9 +70,7 @@ const PageSpeedCheck = mongoose.Schema( required: true, }, }, - { - timestamps: true, - } + { timestamps: true } ); /** @@ -112,4 +104,6 @@ PageSpeedCheck.pre("save", async function (next) { } }); +PageSpeedCheck.index({ createdAt: 1 }); + export default mongoose.model("PageSpeedCheck", PageSpeedCheck); From 7bf45cd803cf3b6c447096122f125710146f0fc3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 30 Oct 2024 10:11:17 +0800 Subject: [PATCH 87/92] Fix production base API url --- Docker/prod/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docker/prod/docker-compose.yaml b/Docker/prod/docker-compose.yaml index 69202c8d7..24e6d1165 100644 --- a/Docker/prod/docker-compose.yaml +++ b/Docker/prod/docker-compose.yaml @@ -2,7 +2,7 @@ services: client: image: uptime_client:latest environment: - UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1" + UPTIME_APP_API_BASE_URL: "https://uptime-demo.bluewavelabs.ca/api/v1" ports: - "80:80" - "443:443" From 100cea64afa0e3423288a594b55a84a7ca091a10 Mon Sep 17 00:00:00 2001 From: Alexander Holliday Date: Wed, 30 Oct 2024 10:13:03 +0800 Subject: [PATCH 88/92] Revert "Fix/devops/docker compose" --- Docker/prod/docker-compose.yaml | 2 +- Server/db/models/Check.js | 127 +++++++++++++++-------------- Server/db/models/HardwareCheck.js | 34 +++++++- Server/db/models/PageSpeedCheck.js | 18 ++-- 4 files changed, 109 insertions(+), 72 deletions(-) diff --git a/Docker/prod/docker-compose.yaml b/Docker/prod/docker-compose.yaml index 24e6d1165..69202c8d7 100644 --- a/Docker/prod/docker-compose.yaml +++ b/Docker/prod/docker-compose.yaml @@ -2,7 +2,7 @@ services: client: image: uptime_client:latest environment: - UPTIME_APP_API_BASE_URL: "https://uptime-demo.bluewavelabs.ca/api/v1" + UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1" ports: - "80:80" - "443:443" diff --git a/Server/db/models/Check.js b/Server/db/models/Check.js index ad50c208a..320ddefe9 100644 --- a/Server/db/models/Check.js +++ b/Server/db/models/Check.js @@ -1,63 +1,6 @@ import mongoose from "mongoose"; - -const BaseCheckSchema = mongoose.Schema({ - /** - * Reference to the associated Monitor document. - * - * @type {mongoose.Schema.Types.ObjectId} - */ - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - immutable: true, - index: true, - }, - /** - * Status of the check (true for up, false for down). - * - * @type {Boolean} - */ - status: { - type: Boolean, - index: true, - }, - /** - * Response time of the check in milliseconds. - * - * @type {Number} - */ - responseTime: { - type: Number, - }, - /** - * HTTP status code received during the check. - * - * @type {Number} - */ - statusCode: { - type: Number, - index: true, - }, - /** - * Message or description of the check result. - * - * @type {String} - */ - message: { - type: String, - }, - /** - * Expiry date of the check, auto-calculated to expire after 30 days. - * - * @type {Date} - */ - - expiry: { - type: Date, - default: Date.now, - expires: 60 * 60 * 24 * 30, // 30 days - }, -}); +import EmailService from "../../service/emailService.js"; +import Notification from "./Notification.js"; /** * Check Schema for MongoDB collection. @@ -65,7 +8,69 @@ const BaseCheckSchema = mongoose.Schema({ * Represents a check associated with a monitor, storing information * about the status and response of a particular check event. */ -const CheckSchema = mongoose.Schema({ ...BaseCheckSchema.obj }, { timestamps: true }); +const CheckSchema = mongoose.Schema( + { + /** + * Reference to the associated Monitor document. + * + * @type {mongoose.Schema.Types.ObjectId} + */ + monitorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Monitor", + immutable: true, + index: true, + }, + /** + * Status of the check (true for up, false for down). + * + * @type {Boolean} + */ + status: { + type: Boolean, + index: true, + }, + /** + * Response time of the check in milliseconds. + * + * @type {Number} + */ + responseTime: { + type: Number, + }, + /** + * HTTP status code received during the check. + * + * @type {Number} + */ + statusCode: { + type: Number, + index: true, + }, + /** + * Message or description of the check result. + * + * @type {String} + */ + message: { + type: String, + }, + /** + * Expiry date of the check, auto-calculated to expire after 30 days. + * + * @type {Date} + */ + + expiry: { + type: Date, + default: Date.now, + expires: 60 * 60 * 24 * 30, // 30 days + }, + }, + { + timestamps: true, // Adds createdAt and updatedAt timestamps + } +); + CheckSchema.index({ createdAt: 1 }); export default mongoose.model("Check", CheckSchema); -export { BaseCheckSchema }; diff --git a/Server/db/models/HardwareCheck.js b/Server/db/models/HardwareCheck.js index 75e4ac584..c307b9804 100644 --- a/Server/db/models/HardwareCheck.js +++ b/Server/db/models/HardwareCheck.js @@ -1,5 +1,5 @@ import mongoose from "mongoose"; -import { BaseCheckSchema } from "./Check.js"; + const cpuSchema = mongoose.Schema({ physical_core: { type: Number, default: 0 }, logical_core: { type: Number, default: 0 }, @@ -32,7 +32,35 @@ const hostSchema = mongoose.Schema({ const HardwareCheckSchema = mongoose.Schema( { - ...BaseCheckSchema.obj, + monitorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Monitor", + immutable: true, + index: true, + }, + + status: { + type: Boolean, + index: true, + }, + + responseTime: { + type: Number, + }, + + statusCode: { + type: Number, + index: true, + }, + + message: { + type: String, + }, + expiry: { + type: Date, + default: Date.now, + expires: 60 * 60 * 24 * 30, // 30 days + }, cpu: { type: cpuSchema, default: () => ({}), @@ -53,6 +81,4 @@ const HardwareCheckSchema = mongoose.Schema( { timestamps: true } ); -HardwareCheckSchema.index({ createdAt: 1 }); - export default mongoose.model("HardwareCheck", HardwareCheckSchema); diff --git a/Server/db/models/PageSpeedCheck.js b/Server/db/models/PageSpeedCheck.js index 4ebc60280..344cc685d 100644 --- a/Server/db/models/PageSpeedCheck.js +++ b/Server/db/models/PageSpeedCheck.js @@ -1,7 +1,5 @@ import mongoose from "mongoose"; -import { BaseCheckSchema } from "./Check.js"; import logger from "../../utils/logger.js"; -import { time } from "console"; const AuditSchema = mongoose.Schema({ id: { type: String, required: true }, title: { type: String, required: true }, @@ -48,7 +46,15 @@ const AuditsSchema = mongoose.Schema({ const PageSpeedCheck = mongoose.Schema( { - ...BaseCheckSchema.obj, + monitorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Monitor", + immutable: true, + }, + status: { + type: Boolean, + required: true, + }, accessibility: { type: Number, required: true, @@ -70,7 +76,9 @@ const PageSpeedCheck = mongoose.Schema( required: true, }, }, - { timestamps: true } + { + timestamps: true, + } ); /** @@ -104,6 +112,4 @@ PageSpeedCheck.pre("save", async function (next) { } }); -PageSpeedCheck.index({ createdAt: 1 }); - export default mongoose.model("PageSpeedCheck", PageSpeedCheck); From 400150486dd6ddf547f94ad03f491aa1556484eb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 30 Oct 2024 10:20:42 +0800 Subject: [PATCH 89/92] implement base check --- Server/db/models/Check.js | 127 ++++++++++++++--------------- Server/db/models/HardwareCheck.js | 34 +------- Server/db/models/PageSpeedCheck.js | 18 ++-- 3 files changed, 71 insertions(+), 108 deletions(-) diff --git a/Server/db/models/Check.js b/Server/db/models/Check.js index 320ddefe9..ad50c208a 100644 --- a/Server/db/models/Check.js +++ b/Server/db/models/Check.js @@ -1,6 +1,63 @@ import mongoose from "mongoose"; -import EmailService from "../../service/emailService.js"; -import Notification from "./Notification.js"; + +const BaseCheckSchema = mongoose.Schema({ + /** + * Reference to the associated Monitor document. + * + * @type {mongoose.Schema.Types.ObjectId} + */ + monitorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Monitor", + immutable: true, + index: true, + }, + /** + * Status of the check (true for up, false for down). + * + * @type {Boolean} + */ + status: { + type: Boolean, + index: true, + }, + /** + * Response time of the check in milliseconds. + * + * @type {Number} + */ + responseTime: { + type: Number, + }, + /** + * HTTP status code received during the check. + * + * @type {Number} + */ + statusCode: { + type: Number, + index: true, + }, + /** + * Message or description of the check result. + * + * @type {String} + */ + message: { + type: String, + }, + /** + * Expiry date of the check, auto-calculated to expire after 30 days. + * + * @type {Date} + */ + + expiry: { + type: Date, + default: Date.now, + expires: 60 * 60 * 24 * 30, // 30 days + }, +}); /** * Check Schema for MongoDB collection. @@ -8,69 +65,7 @@ import Notification from "./Notification.js"; * Represents a check associated with a monitor, storing information * about the status and response of a particular check event. */ -const CheckSchema = mongoose.Schema( - { - /** - * Reference to the associated Monitor document. - * - * @type {mongoose.Schema.Types.ObjectId} - */ - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - immutable: true, - index: true, - }, - /** - * Status of the check (true for up, false for down). - * - * @type {Boolean} - */ - status: { - type: Boolean, - index: true, - }, - /** - * Response time of the check in milliseconds. - * - * @type {Number} - */ - responseTime: { - type: Number, - }, - /** - * HTTP status code received during the check. - * - * @type {Number} - */ - statusCode: { - type: Number, - index: true, - }, - /** - * Message or description of the check result. - * - * @type {String} - */ - message: { - type: String, - }, - /** - * Expiry date of the check, auto-calculated to expire after 30 days. - * - * @type {Date} - */ - - expiry: { - type: Date, - default: Date.now, - expires: 60 * 60 * 24 * 30, // 30 days - }, - }, - { - timestamps: true, // Adds createdAt and updatedAt timestamps - } -); - +const CheckSchema = mongoose.Schema({ ...BaseCheckSchema.obj }, { timestamps: true }); CheckSchema.index({ createdAt: 1 }); export default mongoose.model("Check", CheckSchema); +export { BaseCheckSchema }; diff --git a/Server/db/models/HardwareCheck.js b/Server/db/models/HardwareCheck.js index c307b9804..75e4ac584 100644 --- a/Server/db/models/HardwareCheck.js +++ b/Server/db/models/HardwareCheck.js @@ -1,5 +1,5 @@ import mongoose from "mongoose"; - +import { BaseCheckSchema } from "./Check.js"; const cpuSchema = mongoose.Schema({ physical_core: { type: Number, default: 0 }, logical_core: { type: Number, default: 0 }, @@ -32,35 +32,7 @@ const hostSchema = mongoose.Schema({ const HardwareCheckSchema = mongoose.Schema( { - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - immutable: true, - index: true, - }, - - status: { - type: Boolean, - index: true, - }, - - responseTime: { - type: Number, - }, - - statusCode: { - type: Number, - index: true, - }, - - message: { - type: String, - }, - expiry: { - type: Date, - default: Date.now, - expires: 60 * 60 * 24 * 30, // 30 days - }, + ...BaseCheckSchema.obj, cpu: { type: cpuSchema, default: () => ({}), @@ -81,4 +53,6 @@ const HardwareCheckSchema = mongoose.Schema( { timestamps: true } ); +HardwareCheckSchema.index({ createdAt: 1 }); + export default mongoose.model("HardwareCheck", HardwareCheckSchema); diff --git a/Server/db/models/PageSpeedCheck.js b/Server/db/models/PageSpeedCheck.js index 344cc685d..4ebc60280 100644 --- a/Server/db/models/PageSpeedCheck.js +++ b/Server/db/models/PageSpeedCheck.js @@ -1,5 +1,7 @@ import mongoose from "mongoose"; +import { BaseCheckSchema } from "./Check.js"; import logger from "../../utils/logger.js"; +import { time } from "console"; const AuditSchema = mongoose.Schema({ id: { type: String, required: true }, title: { type: String, required: true }, @@ -46,15 +48,7 @@ const AuditsSchema = mongoose.Schema({ const PageSpeedCheck = mongoose.Schema( { - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - immutable: true, - }, - status: { - type: Boolean, - required: true, - }, + ...BaseCheckSchema.obj, accessibility: { type: Number, required: true, @@ -76,9 +70,7 @@ const PageSpeedCheck = mongoose.Schema( required: true, }, }, - { - timestamps: true, - } + { timestamps: true } ); /** @@ -112,4 +104,6 @@ PageSpeedCheck.pre("save", async function (next) { } }); +PageSpeedCheck.index({ createdAt: 1 }); + export default mongoose.model("PageSpeedCheck", PageSpeedCheck); From f6ee64431b60c0406918745f108b213792c029e5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 30 Oct 2024 10:23:30 +0800 Subject: [PATCH 90/92] Ammend productin base API url --- Docker/prod/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docker/prod/docker-compose.yaml b/Docker/prod/docker-compose.yaml index 69202c8d7..24e6d1165 100644 --- a/Docker/prod/docker-compose.yaml +++ b/Docker/prod/docker-compose.yaml @@ -2,7 +2,7 @@ services: client: image: uptime_client:latest environment: - UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1" + UPTIME_APP_API_BASE_URL: "https://uptime-demo.bluewavelabs.ca/api/v1" ports: - "80:80" - "443:443" From b730f076caf0240fa377be7334ca1ab5d1008d82 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 30 Oct 2024 16:05:07 +0000 Subject: [PATCH 91/92] fix(deps): update dependency recharts to v2.13.2 --- Client/package-lock.json | 8 ++++---- Client/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Client/package-lock.json b/Client/package-lock.json index 8d816f4dc..b9c92f3f7 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -29,7 +29,7 @@ "react-router": "^6.23.0", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", - "recharts": "2.13.1", + "recharts": "2.13.2", "redux-persist": "6.0.0", "vite-plugin-svgr": "^4.2.0" }, @@ -5518,9 +5518,9 @@ } }, "node_modules/recharts": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.1.tgz", - "integrity": "sha512-87LdsmgK/MHLmWQfTC6yDysno2cOigi/+2KRCwy0D8NDu1IOdtTGS8lMovA0VIvJ7kf3zdp1IiwznHZWSPJhYw==", + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.2.tgz", + "integrity": "sha512-UDLGFmnsBluDIPpQb9uty0ejb+jiVI71vkki8vVsR6ZCJdgjBfKQoQfft4re99CKlTy9qjQApxCLG6TrxJkeAg==", "license": "MIT", "dependencies": { "clsx": "^2.0.0", diff --git a/Client/package.json b/Client/package.json index 8fe31f8da..03c1a77c2 100644 --- a/Client/package.json +++ b/Client/package.json @@ -32,7 +32,7 @@ "react-router": "^6.23.0", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", - "recharts": "2.13.1", + "recharts": "2.13.2", "redux-persist": "6.0.0", "vite-plugin-svgr": "^4.2.0" }, From 2eadb9835040b7998c0fce758fda7a434804b656 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 30 Oct 2024 18:29:31 +0000 Subject: [PATCH 92/92] chore(deps): update dependency mocha to v10.8.2 --- Server/package-lock.json | 8 ++++---- Server/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index fefc83f9d..e7f12dd70 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -35,7 +35,7 @@ "c8": "10.1.2", "chai": "5.1.2", "esm": "3.2.25", - "mocha": "10.8.1", + "mocha": "10.8.2", "nodemon": "3.1.7", "prettier": "^3.3.3", "sinon": "19.0.2" @@ -4129,9 +4129,9 @@ } }, "node_modules/mocha": { - "version": "10.8.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.1.tgz", - "integrity": "sha512-WxSpEWgF03HfgNKBuysfK40DUaOSVX5zxgLDoieMGO+zyE69iq2eQ1vBypvIJ5mOPKpuVAqWiTbt4Orj7L6wVw==", + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/Server/package.json b/Server/package.json index 512a1e01a..5a8e98562 100644 --- a/Server/package.json +++ b/Server/package.json @@ -38,7 +38,7 @@ "c8": "10.1.2", "chai": "5.1.2", "esm": "3.2.25", - "mocha": "10.8.1", + "mocha": "10.8.2", "nodemon": "3.1.7", "prettier": "^3.3.3", "sinon": "19.0.2"