Merge branch 'develop' of https://github.com/bluewave-labs/bluewave-uptime into refactor/theme
This commit is contained in:
commit
9fc5256774
27 files changed
+1172
-75
No files matched your search
Generated
+15
-15
@@ -16,8 +16,8 @@
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "6.1",
|
||||
"@mui/x-charts": "^7.5.1",
|
||||
"@mui/x-data-grid": "7.22.1",
|
||||
"@mui/x-date-pickers": "7.22.1",
|
||||
"@mui/x-data-grid": "7.22.2",
|
||||
"@mui/x-date-pickers": "7.22.2",
|
||||
"@reduxjs/toolkit": "2.3.0",
|
||||
"axios": "^1.7.4",
|
||||
"chart.js": "^4.4.3",
|
||||
@@ -1536,9 +1536,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-charts": {
|
||||
"version": "7.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.22.1.tgz",
|
||||
"integrity": "sha512-zgr8CN4yLen5puqaX7Haj5+AoVG7E13HHsIiDoEAuQvuFDF0gKTxTTdLSKXqhd1qJUIIzJaztZtrr3YCVrENqw==",
|
||||
"version": "7.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.22.2.tgz",
|
||||
"integrity": "sha512-0Y2du4Ed7gOT53l8vVJ4vKT+Jz4Dh/iHnLy8TtL3+XhbPH9Ndu9Q30WwyyzOn84yt37hSUru/njQ1BWaSvVPHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.7",
|
||||
@@ -1594,9 +1594,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-data-grid": {
|
||||
"version": "7.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.22.1.tgz",
|
||||
"integrity": "sha512-YHF96MEv7ACG/VuiycZjEAPH7cZLNuV2+bi/MyR1t/e6E6LTolYFykvjSFq+Imz1mYbW4+9mEvrHZsIKL5KKIQ==",
|
||||
"version": "7.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.22.2.tgz",
|
||||
"integrity": "sha512-yfy2s5A6tbajQZiEdsba49T4FYb9F0WPrzbbG30dl1+sIiX4ZRX7ma44UIDGPZrsZv8xkkE+p8qeJxZ7OaMteA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.7",
|
||||
@@ -1631,9 +1631,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-date-pickers": {
|
||||
"version": "7.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.1.tgz",
|
||||
"integrity": "sha512-VBgicE+7PvJrdHSL6HyieHT6a/0dENH8RaMIM2VwUFrGoZzvik50WNwY5U+Hip1BwZLIEvlqtNRQIIj6kgBR6Q==",
|
||||
"version": "7.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.2.tgz",
|
||||
"integrity": "sha512-1KHSlIlnSoY3oHm820By8X344pIdGYqPvCCvfVHrEeeIQ/pHdxDD8tjZFWkFl4Jgm9oVFK90fMcqNZAzc+WaCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.7",
|
||||
@@ -1661,7 +1661,7 @@
|
||||
"dayjs": "^1.10.7",
|
||||
"luxon": "^3.0.2",
|
||||
"moment": "^2.29.4",
|
||||
"moment-hijri": "^2.1.2",
|
||||
"moment-hijri": "^2.1.2 || ^3.0.0",
|
||||
"moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
|
||||
"react": "^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0"
|
||||
@@ -6417,9 +6417,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.10",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz",
|
||||
"integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==",
|
||||
"version": "5.4.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
|
||||
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "6.1",
|
||||
"@mui/x-charts": "^7.5.1",
|
||||
"@mui/x-data-grid": "7.22.1",
|
||||
"@mui/x-date-pickers": "7.22.1",
|
||||
"@mui/x-data-grid": "7.22.2",
|
||||
"@mui/x-date-pickers": "7.22.2",
|
||||
"@reduxjs/toolkit": "2.3.0",
|
||||
"axios": "^1.7.4",
|
||||
"chart.js": "^4.4.3",
|
||||
|
||||
@@ -46,6 +46,8 @@ We've just launched our [Discussions](https://github.com/bluewave-labs/bluewave-
|
||||
|
||||
We are actively developing **infrastructure monitoring** features for Uptime Manager, which will include comprehensive monitoring of memory, disk usage, and CPU performance. Our goal is to build a lightweight agent that runs on Linux servers, continuously collecting and transmitting health metrics to Uptime Manager, where the data will be visualized for real-time insights.
|
||||
|
||||
Additionally, we will introduce **Docker monitoring** to track the performance and health of containerized environments.
|
||||
|
||||
**Roadmap (long term):**
|
||||
|
||||
- DNS monitoring
|
||||
|
||||
+1
-6
@@ -1,11 +1,6 @@
|
||||
{
|
||||
"all": true,
|
||||
"include": [
|
||||
"controllers/*.js",
|
||||
"utils/*.js",
|
||||
"service/*.js",
|
||||
"db/mongo/modules/monitorModule.js"
|
||||
],
|
||||
"include": ["controllers/*.js", "utils/*.js", "service/*.js", "db/mongo/modules/*.js"],
|
||||
"exclude": ["**/*.test.js"],
|
||||
"reporter": ["html", "text", "lcov"],
|
||||
"sourceMap": false,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { handleError, handleValidationError } from "./controllerUtils.js";
|
||||
import {
|
||||
createStatusPageBodyValidation,
|
||||
getStatusPageParamValidation,
|
||||
} from "../validation/joi.js";
|
||||
import { successMessages } from "../utils/messages.js";
|
||||
|
||||
const SERVICE_NAME = "statusPageController";
|
||||
|
||||
const createStatusPage = async (req, res, next) => {
|
||||
try {
|
||||
await createStatusPageBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const statusPage = await req.db.createStatusPage(req.body);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.STATUS_PAGE_CREATE,
|
||||
data: statusPage,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "createStatusPage"));
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusPageByUrl = async (req, res, next) => {
|
||||
try {
|
||||
await getStatusPageParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { url } = req.params;
|
||||
const statusPage = await req.db.getStatusPageByUrl(url);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.STATUS_PAGE_BY_URL,
|
||||
data: statusPage,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getStatusPage"));
|
||||
}
|
||||
};
|
||||
|
||||
export { createStatusPage, getStatusPageByUrl };
|
||||
@@ -30,6 +30,11 @@ const hostSchema = mongoose.Schema({
|
||||
kernel_version: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const errorSchema = mongoose.Schema({
|
||||
metric: { type: [String], default: [] },
|
||||
err: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const HardwareCheckSchema = mongoose.Schema(
|
||||
{
|
||||
...BaseCheckSchema.obj,
|
||||
@@ -49,6 +54,10 @@ const HardwareCheckSchema = mongoose.Schema(
|
||||
type: hostSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: [errorSchema],
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const StatusPageSchema = mongoose.Schema(
|
||||
{
|
||||
companyName: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
unique: true,
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
timezone: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "America/Toronto",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "#4169E1",
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "light",
|
||||
},
|
||||
monitors: [
|
||||
{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
export default mongoose.model("StatusPage", StatusPageSchema);
|
||||
@@ -169,6 +169,11 @@ import {
|
||||
//****************************************
|
||||
import { getAppSettings, updateAppSettings } from "./modules/settingsModule.js";
|
||||
|
||||
//****************************************
|
||||
// Status Page
|
||||
//****************************************
|
||||
import { createStatusPage, getStatusPageByUrl } from "./modules/statusPageModule.js";
|
||||
|
||||
export default {
|
||||
connect,
|
||||
disconnect,
|
||||
@@ -223,4 +228,6 @@ export default {
|
||||
deleteNotificationsByMonitorId,
|
||||
getAppSettings,
|
||||
updateAppSettings,
|
||||
createStatusPage,
|
||||
getStatusPageByUrl,
|
||||
};
|
||||
@@ -78,7 +78,7 @@ const getInviteTokenAndDelete = async (token) => {
|
||||
return invite;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getInviteToken";
|
||||
error.method = "getInviteTokenAndDelete";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -203,7 +203,7 @@ const deleteMaintenanceWindowByUserId = async (userId) => {
|
||||
|
||||
const editMaintenanceWindowById = async (maintenanceWindowId, maintenanceWindowData) => {
|
||||
try {
|
||||
const editedMaintenanceWindow = MaintenanceWindow.findByIdAndUpdate(
|
||||
const editedMaintenanceWindow = await MaintenanceWindow.findByIdAndUpdate(
|
||||
maintenanceWindowId,
|
||||
maintenanceWindowData,
|
||||
{ new: true }
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import StatusPage from "../../models/StatusPage.js";
|
||||
import { errorMessages } from "../../../utils/messages.js";
|
||||
|
||||
const SERVICE_NAME = "statusPageModule";
|
||||
|
||||
const createStatusPage = async (statusPageData) => {
|
||||
try {
|
||||
const statusPage = new StatusPage({ ...statusPageData });
|
||||
await statusPage.save();
|
||||
return statusPage;
|
||||
} catch (error) {
|
||||
if (error?.code === 11000) {
|
||||
// Handle duplicate URL errors
|
||||
error.status = 400;
|
||||
error.message = errorMessages.STATUS_PAGE_URL_NOT_UNIQUE;
|
||||
}
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "createStatusPage";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusPageByUrl = async (url) => {
|
||||
try {
|
||||
const statusPage = await StatusPage.findOne({ url });
|
||||
if (statusPage === null || statusPage === undefined) {
|
||||
const error = new Error(errorMessages.STATUS_PAGE_NOT_FOUND);
|
||||
error.status = 404;
|
||||
|
||||
throw error;
|
||||
}
|
||||
return statusPage;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getStatusPageByUrl";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export { createStatusPage, getStatusPageByUrl };
|
||||
+26
-15
@@ -8,13 +8,13 @@ import cors from "cors";
|
||||
import logger from "./utils/logger.js";
|
||||
import { verifyJWT } from "./middleware/verifyJWT.js";
|
||||
import { handleErrors } from "./middleware/handleErrors.js";
|
||||
import { errorMessages } from "./utils/messages.js";
|
||||
import authRouter from "./routes/authRoute.js";
|
||||
import inviteRouter from "./routes/inviteRoute.js";
|
||||
import monitorRouter from "./routes/monitorRoute.js";
|
||||
import checkRouter from "./routes/checkRoute.js";
|
||||
import maintenanceWindowRouter from "./routes/maintenanceWindowRoute.js";
|
||||
import settingsRouter from "./routes/settingsRoute.js";
|
||||
import statusPageRouter from "./routes/statusPageRoute.js";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import queueRouter from "./routes/queueRoute.js";
|
||||
@@ -45,8 +45,9 @@ import NotificationService from "./service/notificationService.js";
|
||||
|
||||
import db from "./db/mongo/MongoDB.js";
|
||||
const SERVICE_NAME = "Server";
|
||||
const SHUTDOWN_TIMEOUT = 0;
|
||||
|
||||
let cleaningUp = false;
|
||||
let isShuttingDown = false;
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@@ -88,6 +89,7 @@ const startApp = async () => {
|
||||
app.use("/api/v1/checks", verifyJWT, checkRouter);
|
||||
app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRouter);
|
||||
app.use("/api/v1/queue", verifyJWT, queueRouter);
|
||||
app.use("/api/v1/status-page", statusPageRouter);
|
||||
|
||||
//health check
|
||||
app.use("/api/v1/healthy", (req, res) => {
|
||||
@@ -113,11 +115,11 @@ const startApp = async () => {
|
||||
|
||||
// Create services
|
||||
await db.connect();
|
||||
app.listen(PORT, () => {
|
||||
const server = app.listen(PORT, () => {
|
||||
logger.info({ message: `server started on port:${PORT}` });
|
||||
});
|
||||
const settingsService = new SettingsService(AppSettings);
|
||||
|
||||
const settingsService = new SettingsService(AppSettings);
|
||||
await settingsService.loadSettings();
|
||||
const emailService = new EmailService(
|
||||
settingsService,
|
||||
@@ -142,30 +144,39 @@ const startApp = async () => {
|
||||
Worker
|
||||
);
|
||||
|
||||
const cleanup = async () => {
|
||||
if (cleaningUp) {
|
||||
logger.warn({ message: "Already cleaning up" });
|
||||
const shutdown = async () => {
|
||||
if (isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
cleaningUp = true;
|
||||
isShuttingDown = true;
|
||||
logger.info({ message: "Attempting graceful shutdown" });
|
||||
setTimeout(() => {
|
||||
logger.error({
|
||||
message: "Could not shut down in time, forcing shutdown",
|
||||
service: SERVICE_NAME,
|
||||
method: "shutdown",
|
||||
});
|
||||
process.exit(1);
|
||||
}, SHUTDOWN_TIMEOUT);
|
||||
try {
|
||||
logger.info({ message: "shutting down gracefully" });
|
||||
server.close();
|
||||
await jobQueue.obliterate();
|
||||
await db.disconnect();
|
||||
logger.info({ message: "shut down gracefully" });
|
||||
logger.info({ message: "Graceful shutdown complete" });
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "cleanup",
|
||||
method: "shutdown",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
process.exit(0);
|
||||
};
|
||||
process.on("SIGUSR2", cleanup);
|
||||
process.on("SIGINT", cleanup);
|
||||
process.on("SIGTERM", cleanup);
|
||||
|
||||
process.on("SIGUSR2", shutdown);
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
};
|
||||
|
||||
startApp().catch((error) => {
|
||||
|
||||
@@ -84,6 +84,10 @@
|
||||
{
|
||||
"name": "queue",
|
||||
"description": "Queue"
|
||||
},
|
||||
{
|
||||
"name": "status-page",
|
||||
"description": "Status Page"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
@@ -2239,6 +2243,118 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/status-page/{url}": {
|
||||
"get": {
|
||||
"tags": ["status-page"],
|
||||
"description": "Get a status page by URL",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "url",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "string" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/SuccessResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": ["status-page"],
|
||||
"description": "Create a status page",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateStatusPageBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SuccessResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Duplicate URL",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@@ -2425,6 +2541,21 @@
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateStatusPageBody": {
|
||||
"type": "object",
|
||||
"required": ["companyName", "url", "timezone", "color", "theme", "monitors"],
|
||||
"properties": {
|
||||
"companyName": { "type": "string" },
|
||||
"url": { "type": "string" },
|
||||
"timezone": { "type": "string" },
|
||||
"color": { "type": "string" },
|
||||
"theme": { "type": "string" },
|
||||
"monitors": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+22
-20
@@ -12,7 +12,7 @@
|
||||
"@sendgrid/mail": "^8.1.3",
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "5.25.0",
|
||||
"bullmq": "5.25.6",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
@@ -1164,9 +1164,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bullmq": {
|
||||
"version": "5.25.0",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.25.0.tgz",
|
||||
"integrity": "sha512-QBbtabDUgdztalbYbrCc5NFwrRUKOZyiAkVFPhdrBGFsdraPq5SrXx6WP7U1stKj3hYYp1IuW+n3wuksYGITvw==",
|
||||
"version": "5.25.6",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.25.6.tgz",
|
||||
"integrity": "sha512-jxpa/DB02V20CqBAgyqpQazT630CJm0r4fky8EchH3mcJAomRtKXLS6tRA0J8tb29BDGlr/LXhlUuZwdBJBSdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cron-parser": "^4.6.0",
|
||||
@@ -3467,9 +3467,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/logform": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz",
|
||||
"integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
|
||||
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
@@ -4369,9 +4370,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.0.tgz",
|
||||
"integrity": "sha512-KluvgwnQB1GPOYZZXUHJRjS1TW6xxwTlf/YgjWExuuNanIe3W7VcR7dDXQVCIRk8L7NYge8EnoTcu2grWtN+XQ==",
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.1.tgz",
|
||||
"integrity": "sha512-l7DgeY1szT98+EKU8GYnga5WnyatAu+kOQ2VlVX1Mxif6A0Umt0YkSiksCiyGxzx8SPhGe9a53ND1GD4yVDrPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bson": "^6.7.0",
|
||||
@@ -6673,34 +6674,35 @@
|
||||
}
|
||||
},
|
||||
"node_modules/winston": {
|
||||
"version": "3.16.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.16.0.tgz",
|
||||
"integrity": "sha512-xz7+cyGN5M+4CmmD4Npq1/4T+UZaz7HaeTlAruFUTjk79CNMq+P6H30vlE4z0qfqJ01VHYQwd7OZo03nYm/+lg==",
|
||||
"version": "3.17.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
|
||||
"integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.2",
|
||||
"async": "^3.2.3",
|
||||
"is-stream": "^2.0.0",
|
||||
"logform": "^2.6.0",
|
||||
"logform": "^2.7.0",
|
||||
"one-time": "^1.0.0",
|
||||
"readable-stream": "^3.4.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"stack-trace": "0.0.x",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston-transport": "^4.7.0"
|
||||
"winston-transport": "^4.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz",
|
||||
"integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==",
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
|
||||
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"logform": "^2.3.2",
|
||||
"readable-stream": "^3.6.0",
|
||||
"logform": "^2.7.0",
|
||||
"readable-stream": "^3.6.2",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
"@sendgrid/mail": "^8.1.3",
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "5.25.0",
|
||||
"bullmq": "5.25.6",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import express from "express";
|
||||
import { verifyJWT } from "../middleware/verifyJWT.js";
|
||||
import {
|
||||
createStatusPage,
|
||||
getStatusPageByUrl,
|
||||
} from "../controllers/statusPageController.js";
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/:url", getStatusPageByUrl);
|
||||
router.post("/:url", verifyJWT, createStatusPage);
|
||||
export default router;
|
||||
@@ -36,10 +36,10 @@ class JobQueue {
|
||||
host: redisHost,
|
||||
port: redisPort,
|
||||
};
|
||||
this.connection = connection;
|
||||
this.queue = new Queue(QUEUE_NAME, {
|
||||
connection,
|
||||
});
|
||||
this.connection = connection;
|
||||
this.workers = [];
|
||||
this.db = null;
|
||||
this.networkService = null;
|
||||
@@ -406,9 +406,6 @@ class JobQueue {
|
||||
delayed: await this.queue.getDelayedCount(),
|
||||
repeatableJobs: (await this.queue.getRepeatableJobs()).length,
|
||||
};
|
||||
this.logger.info({
|
||||
message: metrics,
|
||||
});
|
||||
return metrics;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
@@ -424,17 +421,20 @@ class JobQueue {
|
||||
* @async
|
||||
* @returns {Promise<boolean>} - Returns true if obliteration is successful
|
||||
*/
|
||||
|
||||
async obliterate() {
|
||||
try {
|
||||
let metrics = await this.getMetrics();
|
||||
this.logger.info({ message: metrics });
|
||||
this.logger.info({ message: "Attempting to obliterate job queue..." });
|
||||
await this.queue.pause();
|
||||
const jobs = await this.getJobs();
|
||||
|
||||
// Remove all repeatable jobs
|
||||
for (const job of jobs) {
|
||||
await this.queue.removeRepeatableByKey(job.key);
|
||||
await this.queue.remove(job.id);
|
||||
}
|
||||
|
||||
// Close workers
|
||||
await Promise.all(
|
||||
this.workers.map(async (worker) => {
|
||||
await worker.close();
|
||||
@@ -442,9 +442,13 @@ class JobQueue {
|
||||
);
|
||||
|
||||
await this.queue.obliterate();
|
||||
metrics = await this.getMetrics();
|
||||
this.logger.info({ message: metrics });
|
||||
this.logger.info({ message: successMessages.JOB_QUEUE_OBLITERATE });
|
||||
const metrics = await this.getMetrics();
|
||||
this.logger.info({
|
||||
message: successMessages.JOB_QUEUE_OBLITERATE,
|
||||
service: SERVICE_NAME,
|
||||
method: "obliterate",
|
||||
details: metrics,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
|
||||
@@ -83,6 +83,7 @@ class StatusService {
|
||||
responseTime,
|
||||
message,
|
||||
};
|
||||
|
||||
if (type === "pagespeed") {
|
||||
const categories = payload.lighthouseResult?.categories;
|
||||
const audits = payload.lighthouseResult?.audits;
|
||||
@@ -101,10 +102,13 @@ class StatusService {
|
||||
}
|
||||
|
||||
if (type === "hardware") {
|
||||
check.cpu = payload?.cpu ?? {};
|
||||
check.memory = payload?.memory ?? {};
|
||||
check.disk = payload?.disk ?? {};
|
||||
check.host = payload?.host ?? {};
|
||||
const { cpu, memory, disk, host } = payload?.data ?? {};
|
||||
const { errors } = payload;
|
||||
check.cpu = cpu ?? {};
|
||||
check.memory = memory ?? {};
|
||||
check.disk = disk ?? {};
|
||||
check.host = host ?? {};
|
||||
check.errors = errors ?? [];
|
||||
}
|
||||
return check;
|
||||
};
|
||||
@@ -131,6 +135,7 @@ class StatusService {
|
||||
hardware: this.db.createHardwareCheck,
|
||||
};
|
||||
const operation = operationMap[networkResponse.type];
|
||||
|
||||
const check = this.buildCheck(networkResponse);
|
||||
await operation(check);
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import sinon from "sinon";
|
||||
import {
|
||||
createStatusPage,
|
||||
getStatusPageByUrl,
|
||||
} from "../../controllers/statusPageController.js";
|
||||
|
||||
describe("statusPageController", () => {
|
||||
let req, res, next;
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
params: {},
|
||||
body: {},
|
||||
db: {
|
||||
createStatusPage: sinon.stub(),
|
||||
getStatusPageByUrl: sinon.stub(),
|
||||
},
|
||||
};
|
||||
res = {
|
||||
status: sinon.stub().returnsThis(),
|
||||
json: sinon.stub(),
|
||||
};
|
||||
next = sinon.stub();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("createStatusPage", () => {
|
||||
beforeEach(() => {
|
||||
req.body = {
|
||||
companyName: "Test Company",
|
||||
url: "123456",
|
||||
timezone: "America/Toronto",
|
||||
color: "#000000",
|
||||
theme: "light",
|
||||
monitors: ["67309ca673788e808884c8ac", "67309ca673788e808884c8ac"],
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should handle a validation error", async () => {
|
||||
req.body = {
|
||||
// Invalid data that will trigger validation error
|
||||
companyName: "",
|
||||
url: "",
|
||||
timezone: "",
|
||||
color: "invalid",
|
||||
theme: "invalid",
|
||||
monitors: ["invalid-id"],
|
||||
};
|
||||
try {
|
||||
await createStatusPage(req, res, next);
|
||||
} catch (error) {
|
||||
expect(error).to.be.an.instanceOf(Error);
|
||||
expect(error.message).to.equal("Validation error");
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle a db error", async () => {
|
||||
const err = new Error("DB error");
|
||||
req.db.createStatusPage.throws(err);
|
||||
|
||||
try {
|
||||
await createStatusPage(req, res, next);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
|
||||
it("should insert a properly formatted status page", async () => {
|
||||
const result = await createStatusPage(req, res, next);
|
||||
expect(res.status.firstCall.args[0]).to.equal(200);
|
||||
expect(res.json.firstCall.args[0].success).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStatusPageByUrl", () => {
|
||||
beforeEach(() => {
|
||||
req.params = {
|
||||
url: "123456",
|
||||
};
|
||||
});
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should handle a validation error", async () => {
|
||||
req.params = {
|
||||
url: "",
|
||||
};
|
||||
|
||||
try {
|
||||
await getStatusPageByUrl(req, res, next);
|
||||
} catch (error) {
|
||||
expect(error).to.be.an.instanceOf(Error);
|
||||
expect(error.message).to.equal("Validation error");
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle a DB error", async () => {
|
||||
const err = new Error("DB error");
|
||||
req.db.getStatusPageByUrl.throws(err);
|
||||
|
||||
try {
|
||||
await getStatusPageByUrl(req, res, next);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return a status page", async () => {
|
||||
const statusPage = {
|
||||
_id: "123456",
|
||||
companyName: "Test Company",
|
||||
url: "123456",
|
||||
};
|
||||
req.db.getStatusPageByUrl.resolves(statusPage);
|
||||
const result = await getStatusPageByUrl(req, res, next);
|
||||
expect(res.status.firstCall.args[0]).to.equal(200);
|
||||
expect(res.json.firstCall.args[0].success).to.be.true;
|
||||
expect(res.json.firstCall.args[0].data).to.deep.equal(statusPage);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import sinon from "sinon";
|
||||
import InviteToken from "../../db/models/InviteToken.js";
|
||||
import {
|
||||
requestInviteToken,
|
||||
getInviteToken,
|
||||
getInviteTokenAndDelete,
|
||||
} from "../../db/mongo/modules/inviteModule.js";
|
||||
import { errorMessages } from "../../utils/messages.js";
|
||||
|
||||
describe("Invite Module", () => {
|
||||
const mockUserData = {
|
||||
email: "test@test.com",
|
||||
teamId: "123",
|
||||
role: ["admin"],
|
||||
token: "123",
|
||||
};
|
||||
const mockInviteToken = { _id: 123, time: 123 };
|
||||
let inviteTokenDeleteManyStub,
|
||||
inviteTokenSaveStub,
|
||||
inviteTokenFindOneStub,
|
||||
inviteTokenFindOneAndDeleteStub;
|
||||
beforeEach(() => {
|
||||
inviteTokenDeleteManyStub = sinon.stub(InviteToken, "deleteMany");
|
||||
inviteTokenSaveStub = sinon.stub(InviteToken.prototype, "save");
|
||||
inviteTokenFindOneStub = sinon.stub(InviteToken, "findOne");
|
||||
inviteTokenFindOneAndDeleteStub = sinon.stub(InviteToken, "findOneAndDelete");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
describe("requestInviteToken", () => {
|
||||
it("should return a new invite token", async () => {
|
||||
inviteTokenDeleteManyStub.resolves();
|
||||
inviteTokenSaveStub.resolves();
|
||||
const inviteToken = await requestInviteToken(mockUserData);
|
||||
expect(inviteToken.email).to.equal(mockUserData.email);
|
||||
expect(inviteToken.role).to.deep.equal(mockUserData.role);
|
||||
expect(inviteToken.token).to.exist;
|
||||
});
|
||||
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
inviteTokenDeleteManyStub.rejects(err);
|
||||
try {
|
||||
await requestInviteToken(mockUserData);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getInviteToken", () => {
|
||||
it("should return an invite token", async () => {
|
||||
inviteTokenFindOneStub.resolves(mockInviteToken);
|
||||
const inviteToken = await getInviteToken(mockUserData.token);
|
||||
expect(inviteToken).to.deep.equal(mockInviteToken);
|
||||
});
|
||||
|
||||
it("should handle a token not found", async () => {
|
||||
inviteTokenFindOneStub.resolves(null);
|
||||
try {
|
||||
await getInviteToken(mockUserData.token);
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal(errorMessages.AUTH_INVITE_NOT_FOUND);
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle DB errors", async () => {
|
||||
const err = new Error("test error");
|
||||
inviteTokenFindOneStub.rejects(err);
|
||||
try {
|
||||
await getInviteToken(mockUserData.token);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
expect(error.method).to.equal("getInviteToken");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getInviteTokenAndDelete", () => {
|
||||
it("should return a deleted invite", async () => {
|
||||
inviteTokenFindOneAndDeleteStub.resolves(mockInviteToken);
|
||||
const deletedInvite = await getInviteTokenAndDelete(mockUserData.token);
|
||||
expect(deletedInvite).to.deep.equal(mockInviteToken);
|
||||
});
|
||||
|
||||
it("should handle a token not found", async () => {
|
||||
inviteTokenFindOneAndDeleteStub.resolves(null);
|
||||
try {
|
||||
await getInviteTokenAndDelete(mockUserData.token);
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal(errorMessages.AUTH_INVITE_NOT_FOUND);
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle DB errors", async () => {
|
||||
const err = new Error("test error");
|
||||
inviteTokenFindOneAndDeleteStub.rejects(err);
|
||||
try {
|
||||
await getInviteTokenAndDelete(mockUserData.token);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
expect(error.method).to.equal("getInviteTokenAndDelete");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,267 @@
|
||||
import sinon from "sinon";
|
||||
import MaintenanceWindow from "../../db/models/MaintenanceWindow.js";
|
||||
import {
|
||||
createMaintenanceWindow,
|
||||
getMaintenanceWindowById,
|
||||
getMaintenanceWindowsByTeamId,
|
||||
getMaintenanceWindowsByMonitorId,
|
||||
deleteMaintenanceWindowById,
|
||||
deleteMaintenanceWindowByMonitorId,
|
||||
deleteMaintenanceWindowByUserId,
|
||||
editMaintenanceWindowById,
|
||||
} from "../../db/mongo/modules/maintenanceWindowModule.js";
|
||||
|
||||
describe("MaintenanceWindow Module", () => {
|
||||
const mockMaintenanceWindow = {
|
||||
monitorId: "123",
|
||||
active: true,
|
||||
oneTime: true,
|
||||
start: 1,
|
||||
end: 20000,
|
||||
};
|
||||
|
||||
let mockMaintenanceWindows = [mockMaintenanceWindow];
|
||||
let maintenanceWindowSaveStub,
|
||||
maintenanceWindowFindByIdStub,
|
||||
maintenanceWindowCountDocumentsStub,
|
||||
maintenanceWindowFindStub,
|
||||
maintenanceWindowFindByIdAndDeleteStub,
|
||||
maintenanceWindowDeleteManyStub,
|
||||
maintenanceWindowFindByIdAndUpdateStub;
|
||||
|
||||
beforeEach(() => {
|
||||
maintenanceWindowSaveStub = sinon.stub(MaintenanceWindow.prototype, "save");
|
||||
maintenanceWindowFindByIdStub = sinon.stub(MaintenanceWindow, "findById");
|
||||
maintenanceWindowCountDocumentsStub = sinon.stub(MaintenanceWindow, "countDocuments");
|
||||
maintenanceWindowFindStub = sinon.stub(MaintenanceWindow, "find").returns({
|
||||
skip: sinon.stub().returns({
|
||||
limit: sinon.stub().returns({
|
||||
sort: sinon.stub().returns(mockMaintenanceWindows),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
maintenanceWindowFindByIdAndDeleteStub = sinon.stub(
|
||||
MaintenanceWindow,
|
||||
"findByIdAndDelete"
|
||||
);
|
||||
maintenanceWindowDeleteManyStub = sinon.stub(MaintenanceWindow, "deleteMany");
|
||||
maintenanceWindowFindByIdAndUpdateStub = sinon.stub(
|
||||
MaintenanceWindow,
|
||||
"findByIdAndUpdate"
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("createMaintenanceWindow", () => {
|
||||
it("should save a new maintenance window", async () => {
|
||||
maintenanceWindowSaveStub.resolves(mockMaintenanceWindow);
|
||||
const result = await createMaintenanceWindow(mockMaintenanceWindow);
|
||||
expect(result).to.deep.equal(mockMaintenanceWindow);
|
||||
});
|
||||
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
maintenanceWindowSaveStub.rejects(err);
|
||||
try {
|
||||
await createMaintenanceWindow(mockMaintenanceWindow);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("getMaintenanceWindowById", () => {
|
||||
it("should return a maintenance window", async () => {
|
||||
maintenanceWindowFindByIdStub.resolves(mockMaintenanceWindow);
|
||||
const result = await getMaintenanceWindowById(mockMaintenanceWindow.id);
|
||||
expect(result).to.deep.equal(mockMaintenanceWindow);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
maintenanceWindowFindByIdStub.rejects(err);
|
||||
try {
|
||||
await getMaintenanceWindowById(mockMaintenanceWindow.id);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMaintenanceWindowsByTeamId", () => {
|
||||
let query;
|
||||
beforeEach(() => {
|
||||
query = {
|
||||
active: true,
|
||||
page: 1,
|
||||
rowsPerPage: 10,
|
||||
field: "name",
|
||||
order: "asc",
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should return a list of maintenance windows and count", async () => {
|
||||
maintenanceWindowCountDocumentsStub.resolves(1);
|
||||
const result = await getMaintenanceWindowsByTeamId(
|
||||
mockMaintenanceWindow.teamId,
|
||||
query
|
||||
);
|
||||
expect(result).to.deep.equal({
|
||||
maintenanceWindows: mockMaintenanceWindows,
|
||||
maintenanceWindowCount: 1,
|
||||
});
|
||||
});
|
||||
it("should return a list of maintenance windows and count with empty query", async () => {
|
||||
query = undefined;
|
||||
maintenanceWindowCountDocumentsStub.resolves(1);
|
||||
const result = await getMaintenanceWindowsByTeamId(
|
||||
mockMaintenanceWindow.teamId,
|
||||
query
|
||||
);
|
||||
expect(result).to.deep.equal({
|
||||
maintenanceWindows: mockMaintenanceWindows,
|
||||
maintenanceWindowCount: 1,
|
||||
});
|
||||
});
|
||||
it("should return a list of maintenance windows and count with no pagination provided", async () => {
|
||||
query.page = undefined;
|
||||
query.rowsPerPage = undefined;
|
||||
maintenanceWindowCountDocumentsStub.resolves(1);
|
||||
const result = await getMaintenanceWindowsByTeamId(
|
||||
mockMaintenanceWindow.teamId,
|
||||
query
|
||||
);
|
||||
expect(result).to.deep.equal({
|
||||
maintenanceWindows: mockMaintenanceWindows,
|
||||
maintenanceWindowCount: 1,
|
||||
});
|
||||
});
|
||||
it("should return a list of maintenance windows and count with field and desc order", async () => {
|
||||
query.order = "desc";
|
||||
maintenanceWindowCountDocumentsStub.resolves(1);
|
||||
const result = await getMaintenanceWindowsByTeamId(
|
||||
mockMaintenanceWindow.teamId,
|
||||
query
|
||||
);
|
||||
expect(result).to.deep.equal({
|
||||
maintenanceWindows: mockMaintenanceWindows,
|
||||
maintenanceWindowCount: 1,
|
||||
});
|
||||
});
|
||||
it("should return a list of maintenance windows and count no field", async () => {
|
||||
query.field = undefined;
|
||||
maintenanceWindowCountDocumentsStub.resolves(1);
|
||||
const result = await getMaintenanceWindowsByTeamId(
|
||||
mockMaintenanceWindow.teamId,
|
||||
query
|
||||
);
|
||||
expect(result).to.deep.equal({
|
||||
maintenanceWindows: mockMaintenanceWindows,
|
||||
maintenanceWindowCount: 1,
|
||||
});
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
maintenanceWindowCountDocumentsStub.rejects(err);
|
||||
try {
|
||||
await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("getMaintenanceWindowsByMonitorId", () => {
|
||||
it("should return a list of maintenance windows", async () => {
|
||||
maintenanceWindowFindStub.resolves(mockMaintenanceWindows);
|
||||
const result = await getMaintenanceWindowsByMonitorId(
|
||||
mockMaintenanceWindow.monitorId
|
||||
);
|
||||
expect(result).to.deep.equal(mockMaintenanceWindows);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
maintenanceWindowFindStub.rejects(err);
|
||||
try {
|
||||
await getMaintenanceWindowsByMonitorId(mockMaintenanceWindow.monitorId);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("deleteMaintenanceWindowById", () => {
|
||||
it("should delete a maintenance window", async () => {
|
||||
maintenanceWindowFindByIdAndDeleteStub.resolves(mockMaintenanceWindow);
|
||||
const result = await deleteMaintenanceWindowById(mockMaintenanceWindow.id);
|
||||
expect(result).to.deep.equal(mockMaintenanceWindow);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
maintenanceWindowFindByIdAndDeleteStub.rejects(err);
|
||||
try {
|
||||
await deleteMaintenanceWindowById(mockMaintenanceWindow.id);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteMaintenanceWindowByMonitorId", () => {
|
||||
it("should return the number of documents deleted", async () => {
|
||||
maintenanceWindowDeleteManyStub.resolves({ deletedCount: 1 });
|
||||
const result = await deleteMaintenanceWindowByMonitorId(
|
||||
mockMaintenanceWindow.monitorId
|
||||
);
|
||||
expect(result).to.deep.equal({ deletedCount: 1 });
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
maintenanceWindowDeleteManyStub.rejects(err);
|
||||
try {
|
||||
await deleteMaintenanceWindowByMonitorId(mockMaintenanceWindow.monitorId);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteMaintenanceWindowByUserId", () => {
|
||||
it("should return the number of documents deleted", async () => {
|
||||
maintenanceWindowDeleteManyStub.resolves({ deletedCount: 1 });
|
||||
const result = await deleteMaintenanceWindowByUserId(mockMaintenanceWindow.userId);
|
||||
expect(result).to.deep.equal({ deletedCount: 1 });
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
maintenanceWindowDeleteManyStub.rejects(err);
|
||||
try {
|
||||
await deleteMaintenanceWindowByUserId(mockMaintenanceWindow.userId);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("editMaintenanceWindowById", () => {
|
||||
it("should return the updated maintenance window", async () => {
|
||||
maintenanceWindowFindByIdAndUpdateStub.resolves(mockMaintenanceWindow);
|
||||
const result = await editMaintenanceWindowById(
|
||||
mockMaintenanceWindow.id,
|
||||
mockMaintenanceWindow
|
||||
);
|
||||
expect(result).to.deep.equal(mockMaintenanceWindow);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
maintenanceWindowFindByIdAndUpdateStub.rejects(err);
|
||||
try {
|
||||
await editMaintenanceWindowById(mockMaintenanceWindow.id, mockMaintenanceWindow);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
import sinon from "sinon";
|
||||
import Monitor from "../../db/models/Monitor.js";
|
||||
import Check from "../../db/models/Check.js";
|
||||
import PageSpeedCheck from "../../db/models/PageSpeedCheck.js";
|
||||
import HardwareCheck from "../../db/models/HardwareCheck.js";
|
||||
import Notification from "../../db/models/Notification.js";
|
||||
|
||||
import { errorMessages } from "../../utils/messages.js";
|
||||
@@ -37,7 +39,9 @@ describe("monitorModule", () => {
|
||||
monitorDeleteManyStub,
|
||||
monitorCountStub,
|
||||
monitorInsertManyStub,
|
||||
checkFindStub;
|
||||
checkFindStub,
|
||||
pageSpeedCheckFindStub,
|
||||
hardwareCheckFindStub;
|
||||
beforeEach(() => {
|
||||
monitorFindStub = sinon.stub(Monitor, "find");
|
||||
monitorFindByIdStub = sinon.stub(Monitor, "findById");
|
||||
@@ -50,6 +54,12 @@ describe("monitorModule", () => {
|
||||
checkFindStub = sinon.stub(Check, "find").returns({
|
||||
sort: sinon.stub(),
|
||||
});
|
||||
pageSpeedCheckFindStub = sinon.stub(PageSpeedCheck, "find").returns({
|
||||
sort: sinon.stub(),
|
||||
});
|
||||
hardwareCheckFindStub = sinon.stub(HardwareCheck, "find").returns({
|
||||
sort: sinon.stub(),
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
@@ -137,6 +147,96 @@ describe("monitorModule", () => {
|
||||
expect(monitor["30"]).to.equal(75);
|
||||
expect(monitor["90"]).to.equal(75);
|
||||
});
|
||||
it("should return monitors with stats for pagespeed type", async () => {
|
||||
// Mock data
|
||||
const mockMonitors = [
|
||||
{
|
||||
_id: "monitor1",
|
||||
type: "pagespeed",
|
||||
toObject: () => ({
|
||||
_id: "monitor1",
|
||||
type: "pagespeed",
|
||||
name: "Test Monitor",
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const mockChecks = [
|
||||
{ status: true },
|
||||
{ status: true },
|
||||
{ status: false },
|
||||
{ status: true },
|
||||
];
|
||||
|
||||
monitorFindStub.resolves(mockMonitors);
|
||||
pageSpeedCheckFindStub.resolves(mockChecks);
|
||||
|
||||
const result = await getAllMonitorsWithUptimeStats();
|
||||
|
||||
expect(result).to.be.an("array");
|
||||
expect(result).to.have.lengthOf(1);
|
||||
|
||||
const monitor = result[0];
|
||||
expect(monitor).to.have.property("_id", "monitor1");
|
||||
expect(monitor).to.have.property("name", "Test Monitor");
|
||||
|
||||
// Check uptime percentages exist for all time periods
|
||||
expect(monitor).to.have.property("1");
|
||||
expect(monitor).to.have.property("7");
|
||||
expect(monitor).to.have.property("30");
|
||||
expect(monitor).to.have.property("90");
|
||||
|
||||
// Verify uptime percentage calculation (3 successful out of 4 = 75%)
|
||||
expect(monitor["1"]).to.equal(75);
|
||||
expect(monitor["7"]).to.equal(75);
|
||||
expect(monitor["30"]).to.equal(75);
|
||||
expect(monitor["90"]).to.equal(75);
|
||||
});
|
||||
it("should return monitors with stats for hardware type", async () => {
|
||||
// Mock data
|
||||
const mockMonitors = [
|
||||
{
|
||||
_id: "monitor1",
|
||||
type: "hardware",
|
||||
toObject: () => ({
|
||||
_id: "monitor1",
|
||||
type: "hardware",
|
||||
name: "Test Monitor",
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const mockChecks = [
|
||||
{ status: true },
|
||||
{ status: true },
|
||||
{ status: false },
|
||||
{ status: true },
|
||||
];
|
||||
|
||||
monitorFindStub.resolves(mockMonitors);
|
||||
hardwareCheckFindStub.resolves(mockChecks);
|
||||
|
||||
const result = await getAllMonitorsWithUptimeStats();
|
||||
|
||||
expect(result).to.be.an("array");
|
||||
expect(result).to.have.lengthOf(1);
|
||||
|
||||
const monitor = result[0];
|
||||
expect(monitor).to.have.property("_id", "monitor1");
|
||||
expect(monitor).to.have.property("name", "Test Monitor");
|
||||
|
||||
// Check uptime percentages exist for all time periods
|
||||
expect(monitor).to.have.property("1");
|
||||
expect(monitor).to.have.property("7");
|
||||
expect(monitor).to.have.property("30");
|
||||
expect(monitor).to.have.property("90");
|
||||
|
||||
// Verify uptime percentage calculation (3 successful out of 4 = 75%)
|
||||
expect(monitor["1"]).to.equal(75);
|
||||
expect(monitor["7"]).to.equal(75);
|
||||
expect(monitor["30"]).to.equal(75);
|
||||
expect(monitor["90"]).to.equal(75);
|
||||
});
|
||||
|
||||
it("should handle errors appropriately", async () => {
|
||||
// Setup stub to throw error
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import sinon from "sinon";
|
||||
import Notification from "../../db/models/Notification.js";
|
||||
import {
|
||||
createNotification,
|
||||
getNotificationsByMonitorId,
|
||||
deleteNotificationsByMonitorId,
|
||||
} from "../../db/mongo/modules/notificationModule.js";
|
||||
|
||||
describe("notificationModule", () => {
|
||||
const mockNotification = {
|
||||
monitorId: "123",
|
||||
};
|
||||
const mockNotifications = [mockNotification];
|
||||
let notificationSaveStub, notificationFindStub, notificationDeleteManyStub;
|
||||
|
||||
beforeEach(() => {
|
||||
notificationSaveStub = sinon.stub(Notification.prototype, "save").resolves();
|
||||
notificationFindStub = sinon.stub(Notification, "find").resolves();
|
||||
notificationDeleteManyStub = sinon.stub(Notification, "deleteMany").resolves();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("createNotification", () => {
|
||||
it("should create a new notification", async () => {
|
||||
const notificationData = { _id: "123", name: "test" };
|
||||
notificationSaveStub.resolves(notificationData);
|
||||
const res = await createNotification(notificationData);
|
||||
expect(res).to.deep.equal(notificationData);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
notificationSaveStub.rejects(err);
|
||||
try {
|
||||
await createNotification(mockNotification);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getNotificationsByMonitorId", () => {
|
||||
it("should return notifications by monitor ID", async () => {
|
||||
notificationFindStub.resolves(mockNotifications);
|
||||
const res = await getNotificationsByMonitorId(mockNotification.monitorId);
|
||||
expect(res).to.deep.equal(mockNotifications);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
notificationFindStub.rejects(err);
|
||||
try {
|
||||
await getNotificationsByMonitorId(mockNotification.monitorId);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteNotificationsByMonitorId", () => {
|
||||
it("should delete notifications by monitor ID", async () => {
|
||||
notificationDeleteManyStub.resolves({ deletedCount: mockNotifications.length });
|
||||
const res = await deleteNotificationsByMonitorId(mockNotification.monitorId);
|
||||
expect(res).to.deep.equal(mockNotifications.length);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
notificationDeleteManyStub.rejects(err);
|
||||
try {
|
||||
await deleteNotificationsByMonitorId(mockNotification.monitorId);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import sinon from "sinon";
|
||||
import {
|
||||
createStatusPage,
|
||||
getStatusPageByUrl,
|
||||
} from "../../db/mongo/modules/statusPageModule.js";
|
||||
import StatusPage from "../../db/models/StatusPage.js";
|
||||
import { errorMessages } from "../../utils/messages.js";
|
||||
|
||||
describe("statusPageModule", () => {
|
||||
let statusPageFindOneStub, statusPageSaveStub, statusPageFindStub;
|
||||
beforeEach(() => {
|
||||
statusPageSaveStub = sinon.stub(StatusPage.prototype, "save");
|
||||
statusPageFindOneStub = sinon.stub(StatusPage, "findOne");
|
||||
statusPageFindStub = sinon.stub(StatusPage, "find");
|
||||
});
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
describe("createStatusPage", () => {
|
||||
it("should throw an error if a non-unique url is provided", async () => {
|
||||
statusPageFindOneStub.resolves(true);
|
||||
try {
|
||||
await createStatusPage({ url: "test" });
|
||||
} catch (error) {
|
||||
expect(error.status).to.equal(400);
|
||||
expect(error.message).to.equal(errorMessages.STATUS_PAGE_URL_NOT_UNIQUE);
|
||||
}
|
||||
});
|
||||
it("should handle duplicate URL errors", async () => {
|
||||
const err = new Error("test");
|
||||
err.code = 11000;
|
||||
statusPageSaveStub.rejects(err);
|
||||
try {
|
||||
await createStatusPage({ url: "test" });
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
it("should return a status page if a unique url is provided", async () => {
|
||||
statusPageFindOneStub.resolves(null);
|
||||
statusPageFindStub.resolves([]);
|
||||
const mockStatusPage = { url: "test" };
|
||||
const statusPage = await createStatusPage(mockStatusPage);
|
||||
expect(statusPage).to.exist;
|
||||
expect(statusPage.url).to.equal(mockStatusPage.url);
|
||||
});
|
||||
});
|
||||
describe("getStatusPageByUrl", () => {
|
||||
it("should throw an error if a status page is not found", async () => {
|
||||
statusPageFindOneStub.resolves(null);
|
||||
try {
|
||||
await getStatusPageByUrl("test");
|
||||
} catch (error) {
|
||||
expect(error.status).to.equal(404);
|
||||
expect(error.message).to.equal(errorMessages.STATUS_PAGE_NOT_FOUND);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return a status page if a status page is found", async () => {
|
||||
const mockStatusPage = { url: "test" };
|
||||
statusPageFindOneStub.resolves(mockStatusPage);
|
||||
const statusPage = await getStatusPageByUrl(mockStatusPage.url);
|
||||
expect(statusPage).to.exist;
|
||||
expect(statusPage).to.deep.equal(mockStatusPage);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,10 @@ class Logger {
|
||||
if (message instanceof Object) {
|
||||
message = JSON.stringify(message, null, 2);
|
||||
}
|
||||
|
||||
if (details instanceof Object) {
|
||||
details = JSON.stringify(details, null, 2);
|
||||
}
|
||||
let msg = `${timestamp} ${level}:`;
|
||||
service && (msg += ` [${service}]`);
|
||||
method && (msg += `(${method})`);
|
||||
|
||||
@@ -51,6 +51,10 @@ const errorMessages = {
|
||||
|
||||
// PING Operations
|
||||
PING_CANNOT_RESOLVE: "No response",
|
||||
|
||||
// Status Page Errors
|
||||
STATUS_PAGE_NOT_FOUND: "Status page not found",
|
||||
STATUS_PAGE_URL_NOT_UNIQUE: "Status page url must be unique",
|
||||
};
|
||||
|
||||
const successMessages = {
|
||||
@@ -115,6 +119,10 @@ const successMessages = {
|
||||
// App Settings
|
||||
GET_APP_SETTINGS: "Got app settings successfully",
|
||||
UPDATE_APP_SETTINGS: "Updated app settings successfully",
|
||||
|
||||
// Status Page
|
||||
STATUS_PAGE_BY_URL: "Got status page by url successfully",
|
||||
STATUS_PAGE_CREATE: "Status page created successfully",
|
||||
};
|
||||
|
||||
export { errorMessages, successMessages };
|
||||
@@ -410,6 +410,32 @@ const updateAppSettingsBodyValidation = joi.object({
|
||||
systemEmailPassword: joi.string().allow(""),
|
||||
});
|
||||
|
||||
//****************************************
|
||||
// Status Page Validation
|
||||
//****************************************
|
||||
|
||||
const getStatusPageParamValidation = joi.object({
|
||||
url: joi.string().required(),
|
||||
});
|
||||
|
||||
const createStatusPageBodyValidation = joi.object({
|
||||
companyName: joi.string().required(),
|
||||
url: joi.string().required(),
|
||||
timezone: joi.string().required(),
|
||||
color: joi.string().required(),
|
||||
theme: joi.string().required(),
|
||||
monitors: joi
|
||||
.array()
|
||||
.items(joi.string().pattern(/^[0-9a-fA-F]{24}$/))
|
||||
.required()
|
||||
.messages({
|
||||
"string.pattern.base": "Must be a valid monitor ID",
|
||||
"array.base": "Monitors must be an array",
|
||||
"array.empty": "At least one monitor is required",
|
||||
"any.required": "Monitors are required",
|
||||
}),
|
||||
});
|
||||
|
||||
export {
|
||||
roleValidatior,
|
||||
loginValidation,
|
||||
@@ -465,4 +491,6 @@ export {
|
||||
editMaintenanceWindowByIdParamValidation,
|
||||
editMaintenanceByIdWindowBodyValidation,
|
||||
updateAppSettingsBodyValidation,
|
||||
createStatusPageBodyValidation,
|
||||
getStatusPageParamValidation,
|
||||
};
|
||||
Reference in new issue
Block a user