Merge branch 'develop' into 1067-fe-hardware-monitoring-create-hardware-monitor

This commit is contained in:
Shemy Gan committed 2024-11-12 09:12:41 -05:00
commit 2e51f025f9
23 files changed
+1097 -21

No files matched your search

+3 -3
View File
@@ -6275,9 +6275,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",
+1 -6
View File
@@ -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 };
+9
View File
@@ -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 }
);
+42
View File
@@ -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);
+7
View File
@@ -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,
};
+1 -1
View File
@@ -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 };
+2
View File
@@ -14,6 +14,7 @@ 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";
@@ -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) => {
+131
View File
@@ -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" }
}
}
}
}
}
+4 -4
View File
@@ -12,7 +12,7 @@
"@sendgrid/mail": "^8.1.3",
"axios": "^1.7.2",
"bcrypt": "^5.1.1",
"bullmq": "5.25.4",
"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.4",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.25.4.tgz",
"integrity": "sha512-f9M5qfFOg9hdoMWmux9x9rZm9ZUPTMFfdDMO2zRsi7IOzgvZ0UxB6oTk77PlC9YSDYoufAgBw82xU1nwvnsKSA==",
"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",
+1 -1
View File
@@ -15,7 +15,7 @@
"@sendgrid/mail": "^8.1.3",
"axios": "^1.7.2",
"bcrypt": "^5.1.1",
"bullmq": "5.25.4",
"bullmq": "5.25.6",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
+11
View File
@@ -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;
+9 -4
View File
@@ -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);
});
});
});
+108
View File
@@ -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);
}
});
});
});
+101 -1
View File
@@ -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);
}
});
});
});
+67
View File
@@ -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);
});
});
});
+8
View File
@@ -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 };
+28
View File
@@ -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,
};