Merge pull request #1138 from bluewave-labs/feat/be/status-page-model
feat/be/status page implementation
This commit is contained in:
12 files changed
+516
-6
No files matched your search
+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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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