From b5d93456592a15a8bac358abcdce26dd1c9d7c0d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 8 Nov 2024 10:48:48 +0800 Subject: [PATCH 01/23] Add missing test cases for pagespeed and hardware monitor types --- Server/tests/db/monitorModule.test.js | 102 +++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/Server/tests/db/monitorModule.test.js b/Server/tests/db/monitorModule.test.js index 558dcb4d2..189c8a703 100644 --- a/Server/tests/db/monitorModule.test.js +++ b/Server/tests/db/monitorModule.test.js @@ -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 From dc73e00127a204c40ac849f8c9e81db7c79955c8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 8 Nov 2024 11:28:01 +0800 Subject: [PATCH 02/23] Correct getInviteTokenAndDelete error method name --- Server/db/mongo/modules/inviteModule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/db/mongo/modules/inviteModule.js b/Server/db/mongo/modules/inviteModule.js index 63636be86..0cbf0ab04 100644 --- a/Server/db/mongo/modules/inviteModule.js +++ b/Server/db/mongo/modules/inviteModule.js @@ -78,7 +78,7 @@ const getInviteTokenAndDelete = async (token) => { return invite; } catch (error) { error.service = SERVICE_NAME; - error.method = "getInviteToken"; + error.method = "getInviteTokenAndDelete"; throw error; } }; From c701caff93f05249b131c9f9a4241ca57f265a9c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 8 Nov 2024 11:28:17 +0800 Subject: [PATCH 03/23] Add tests for invite module --- Server/tests/db/inviteModule.test.js | 108 +++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 Server/tests/db/inviteModule.test.js diff --git a/Server/tests/db/inviteModule.test.js b/Server/tests/db/inviteModule.test.js new file mode 100644 index 000000000..997352704 --- /dev/null +++ b/Server/tests/db/inviteModule.test.js @@ -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"); + } + }); + }); +}); From 349646639c4d72a4658009cf5ca0a764deaa6bed Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 8 Nov 2024 12:08:08 +0800 Subject: [PATCH 04/23] add missing await to --- Server/db/mongo/modules/maintenanceWindowModule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/db/mongo/modules/maintenanceWindowModule.js b/Server/db/mongo/modules/maintenanceWindowModule.js index e2488dc3b..072f83c3d 100644 --- a/Server/db/mongo/modules/maintenanceWindowModule.js +++ b/Server/db/mongo/modules/maintenanceWindowModule.js @@ -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 } From 247c034dd784badae2d65d4742b9e56c8cc3bffa Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 8 Nov 2024 12:10:13 +0800 Subject: [PATCH 05/23] Add tests for maintenance module --- .../tests/db/maintenanceWindowModule.test.js | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 Server/tests/db/maintenanceWindowModule.test.js diff --git a/Server/tests/db/maintenanceWindowModule.test.js b/Server/tests/db/maintenanceWindowModule.test.js new file mode 100644 index 000000000..094d9a3e8 --- /dev/null +++ b/Server/tests/db/maintenanceWindowModule.test.js @@ -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); + } + }); + }); +}); From 4c3eb826b285976865d25b2296ecfb7ac8d131e3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 8 Nov 2024 12:28:25 +0800 Subject: [PATCH 06/23] Add tests --- Server/tests/db/notificationModule.test.js | 77 ++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 Server/tests/db/notificationModule.test.js diff --git a/Server/tests/db/notificationModule.test.js b/Server/tests/db/notificationModule.test.js new file mode 100644 index 000000000..ebe213731 --- /dev/null +++ b/Server/tests/db/notificationModule.test.js @@ -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); + } + }); + }); +}); From d1061be0280561c2182790e8eae5b92e02668303 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 11 Nov 2024 08:52:34 +0800 Subject: [PATCH 07/23] Updated hardware check schema to reflect new agent structure --- Server/db/models/HardwareCheck.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Server/db/models/HardwareCheck.js b/Server/db/models/HardwareCheck.js index 75e4ac584..527f226b6 100644 --- a/Server/db/models/HardwareCheck.js +++ b/Server/db/models/HardwareCheck.js @@ -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 } ); From 3b8454b1544e393e78850a7bc0b684bbc6e4f062 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 11 Nov 2024 08:52:51 +0800 Subject: [PATCH 08/23] Update status service to handle new schema --- Server/service/statusService.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Server/service/statusService.js b/Server/service/statusService.js index 64705e122..22254d050 100644 --- a/Server/service/statusService.js +++ b/Server/service/statusService.js @@ -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) { From cc0ecf2b12053ee41744cf7d3a3c1289b7dda574 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 11 Nov 2024 12:16:07 +0800 Subject: [PATCH 09/23] Created model for StatusPage --- Server/db/models/StatusPage.js | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 Server/db/models/StatusPage.js diff --git a/Server/db/models/StatusPage.js b/Server/db/models/StatusPage.js new file mode 100644 index 000000000..05ae7ffa6 --- /dev/null +++ b/Server/db/models/StatusPage.js @@ -0,0 +1,41 @@ +import mongoose from "mongoose"; + +const StatusPageSchema = mongoose.Schema( + { + companyName: { + type: String, + required: true, + default: "", + }, + url: { + type: String, + 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); From 505de31fe02058237f826b2bfa138786418e5faa Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 11 Nov 2024 12:16:34 +0800 Subject: [PATCH 10/23] Add db methods for statusPage --- Server/db/mongo/modules/statusPageModule.js | 47 +++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 Server/db/mongo/modules/statusPageModule.js diff --git a/Server/db/mongo/modules/statusPageModule.js b/Server/db/mongo/modules/statusPageModule.js new file mode 100644 index 000000000..229eff74e --- /dev/null +++ b/Server/db/mongo/modules/statusPageModule.js @@ -0,0 +1,47 @@ +import StatusPage from "../../models/StatusPage.js"; +import { errorMessages } from "../../../utils/messages.js"; + +const SERVICE_NAME = "statusPageModule"; + +const createStatusPage = async (statusPageData) => { + try { + const isUnique = await urlIsUnique(statusPageData.url); + if (!isUnique) { + const error = new Error(errorMessages.STATUS_PAGE_URL_NOT_UNIQUE); + error.status = 400; + throw error; + } + const statusPage = new StatusPage({ ...statusPageData }); + await statusPage.save(); + return statusPage; + } catch (error) { + 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; + } +}; + +const urlIsUnique = async (url) => { + const statusPage = await StatusPage.find({ url }); + if (statusPage.length > 0) return false; + return true; +}; + +export { createStatusPage, getStatusPageByUrl }; From 285fbf2a421a84f259e7919eedee3fa6947059d1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 11 Nov 2024 12:16:44 +0800 Subject: [PATCH 11/23] Add DB methods to MongoDB --- Server/db/mongo/MongoDB.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js index 581a9e588..bab17c5ef 100644 --- a/Server/db/mongo/MongoDB.js +++ b/Server/db/mongo/MongoDB.js @@ -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, }; From f3a29c3be76f2bc6ea6ac4ce4b03103f86803437 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 11 Nov 2024 12:17:06 +0800 Subject: [PATCH 12/23] Add controller for status page --- Server/controllers/statusPageController.js | 51 ++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 Server/controllers/statusPageController.js diff --git a/Server/controllers/statusPageController.js b/Server/controllers/statusPageController.js new file mode 100644 index 000000000..facac7c4a --- /dev/null +++ b/Server/controllers/statusPageController.js @@ -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 }; From 8164ca2bfe933b2232e3dc00501643f4e3cfff5c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 11 Nov 2024 12:17:19 +0800 Subject: [PATCH 13/23] Add validations for status page --- Server/validation/joi.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 06804975e..07521c7e0 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -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, }; From b6781a4945b76c6ffe69de687f9bc2bc9ffbaecb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 11 Nov 2024 12:17:37 +0800 Subject: [PATCH 14/23] Add success/error messages for status pages --- Server/utils/messages.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Server/utils/messages.js b/Server/utils/messages.js index dd7f0f76a..c75c617d2 100644 --- a/Server/utils/messages.js +++ b/Server/utils/messages.js @@ -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 }; From 6b04f9a978b9cc477f2296cb81a3f80b3ff1f63b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 11 Nov 2024 12:17:55 +0800 Subject: [PATCH 15/23] Create router for status page --- Server/routes/statusPageRoute.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Server/routes/statusPageRoute.js diff --git a/Server/routes/statusPageRoute.js b/Server/routes/statusPageRoute.js new file mode 100644 index 000000000..6300723f4 --- /dev/null +++ b/Server/routes/statusPageRoute.js @@ -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; From a27cadc94f13faae43b109184629e229140f3518 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 11 Nov 2024 12:18:05 +0800 Subject: [PATCH 16/23] Add routes for status page --- Server/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Server/index.js b/Server/index.js index e12d83a18..11b5b2151 100644 --- a/Server/index.js +++ b/Server/index.js @@ -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) => { From 54fca685c0fea4352278e30d9fb57f217b351d75 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 11 Nov 2024 14:25:34 +0800 Subject: [PATCH 17/23] Add try/catch to --- Server/db/mongo/modules/statusPageModule.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Server/db/mongo/modules/statusPageModule.js b/Server/db/mongo/modules/statusPageModule.js index 229eff74e..2f30bcbeb 100644 --- a/Server/db/mongo/modules/statusPageModule.js +++ b/Server/db/mongo/modules/statusPageModule.js @@ -39,9 +39,15 @@ const getStatusPageByUrl = async (url) => { }; const urlIsUnique = async (url) => { - const statusPage = await StatusPage.find({ url }); - if (statusPage.length > 0) return false; - return true; + try { + const statusPage = await StatusPage.find({ url }); + if (statusPage.length > 0) return false; + return true; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "urlIsUnique"; + throw error; + } }; -export { createStatusPage, getStatusPageByUrl }; +export { createStatusPage, getStatusPageByUrl, urlIsUnique }; From 75ca6f32391c75e6a15ed8cb73d46859aa05bb9b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 11 Nov 2024 14:25:44 +0800 Subject: [PATCH 18/23] Add tests --- .../controllers/statusPageController.test.js | 128 ++++++++++++++++++ Server/tests/db/statusPageModule.test.js | 82 +++++++++++ 2 files changed, 210 insertions(+) create mode 100644 Server/tests/controllers/statusPageController.test.js create mode 100644 Server/tests/db/statusPageModule.test.js diff --git a/Server/tests/controllers/statusPageController.test.js b/Server/tests/controllers/statusPageController.test.js new file mode 100644 index 000000000..0031e73df --- /dev/null +++ b/Server/tests/controllers/statusPageController.test.js @@ -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); + }); + }); +}); diff --git a/Server/tests/db/statusPageModule.test.js b/Server/tests/db/statusPageModule.test.js new file mode 100644 index 000000000..bf35e2e3e --- /dev/null +++ b/Server/tests/db/statusPageModule.test.js @@ -0,0 +1,82 @@ +import sinon from "sinon"; +import { + createStatusPage, + getStatusPageByUrl, + urlIsUnique, +} 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); + statusPageFindStub.resolves([{}]); + 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 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); + }); + }); + describe("urlIsUnique", () => { + it("should throw an error if an error occurs", async () => { + const err = new Error("test"); + statusPageFindStub.rejects(err); + try { + await urlIsUnique("test"); + } catch (error) { + expect(error).to.deep.equal(err); + } + }); + + it("should return true if a url is unique", async () => { + statusPageFindStub.resolves([]); + const isUnique = await urlIsUnique("test"); + expect(isUnique).to.be.true; + }); + + it("should return false if a url is not unique", async () => { + statusPageFindStub.resolves([{ url: "test" }]); + const isUnique = await urlIsUnique("test"); + expect(isUnique).to.be.false; + }); + }); +}); From 58176981058640da43aa8950e8f4ef24b9dc3886 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 11 Nov 2024 14:38:59 +0800 Subject: [PATCH 19/23] Add unique contraint to URL --- Server/db/models/StatusPage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Server/db/models/StatusPage.js b/Server/db/models/StatusPage.js index 05ae7ffa6..1169d4661 100644 --- a/Server/db/models/StatusPage.js +++ b/Server/db/models/StatusPage.js @@ -9,6 +9,7 @@ const StatusPageSchema = mongoose.Schema( }, url: { type: String, + unique: true, required: true, default: "", }, From a4a01b0b8305c57887d75ec1fc7d9cf9d1217859 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 11 Nov 2024 14:39:30 +0800 Subject: [PATCH 20/23] Remove urlIsUnqiue method, update tests --- Server/.nycrc | 7 +---- Server/db/mongo/modules/statusPageModule.js | 25 ++++----------- Server/tests/db/statusPageModule.test.js | 35 ++++++--------------- 3 files changed, 17 insertions(+), 50 deletions(-) diff --git a/Server/.nycrc b/Server/.nycrc index 159e02a0d..96f20d795 100644 --- a/Server/.nycrc +++ b/Server/.nycrc @@ -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, diff --git a/Server/db/mongo/modules/statusPageModule.js b/Server/db/mongo/modules/statusPageModule.js index 2f30bcbeb..194287448 100644 --- a/Server/db/mongo/modules/statusPageModule.js +++ b/Server/db/mongo/modules/statusPageModule.js @@ -5,16 +5,15 @@ const SERVICE_NAME = "statusPageModule"; const createStatusPage = async (statusPageData) => { try { - const isUnique = await urlIsUnique(statusPageData.url); - if (!isUnique) { - const error = new Error(errorMessages.STATUS_PAGE_URL_NOT_UNIQUE); - error.status = 400; - throw error; - } 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; @@ -38,16 +37,4 @@ const getStatusPageByUrl = async (url) => { } }; -const urlIsUnique = async (url) => { - try { - const statusPage = await StatusPage.find({ url }); - if (statusPage.length > 0) return false; - return true; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "urlIsUnique"; - throw error; - } -}; - -export { createStatusPage, getStatusPageByUrl, urlIsUnique }; +export { createStatusPage, getStatusPageByUrl }; diff --git a/Server/tests/db/statusPageModule.test.js b/Server/tests/db/statusPageModule.test.js index bf35e2e3e..8450ac5fc 100644 --- a/Server/tests/db/statusPageModule.test.js +++ b/Server/tests/db/statusPageModule.test.js @@ -2,7 +2,6 @@ import sinon from "sinon"; import { createStatusPage, getStatusPageByUrl, - urlIsUnique, } from "../../db/mongo/modules/statusPageModule.js"; import StatusPage from "../../db/models/StatusPage.js"; import { errorMessages } from "../../utils/messages.js"; @@ -20,7 +19,6 @@ describe("statusPageModule", () => { describe("createStatusPage", () => { it("should throw an error if a non-unique url is provided", async () => { statusPageFindOneStub.resolves(true); - statusPageFindStub.resolves([{}]); try { await createStatusPage({ url: "test" }); } catch (error) { @@ -28,6 +26,16 @@ describe("statusPageModule", () => { 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([]); @@ -56,27 +64,4 @@ describe("statusPageModule", () => { expect(statusPage).to.deep.equal(mockStatusPage); }); }); - describe("urlIsUnique", () => { - it("should throw an error if an error occurs", async () => { - const err = new Error("test"); - statusPageFindStub.rejects(err); - try { - await urlIsUnique("test"); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - - it("should return true if a url is unique", async () => { - statusPageFindStub.resolves([]); - const isUnique = await urlIsUnique("test"); - expect(isUnique).to.be.true; - }); - - it("should return false if a url is not unique", async () => { - statusPageFindStub.resolves([{ url: "test" }]); - const isUnique = await urlIsUnique("test"); - expect(isUnique).to.be.false; - }); - }); }); From 1476e9da389ce11f5ffaa3efadfa20abc7c0c098 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 11 Nov 2024 14:48:44 +0800 Subject: [PATCH 21/23] Update swagger docs --- Server/openapi.json | 131 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/Server/openapi.json b/Server/openapi.json index 5d2d512e6..7ba2fc222 100644 --- a/Server/openapi.json +++ b/Server/openapi.json @@ -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" } + } + } } } } From 9d7c6bb6d21a5d26be25f46cc86ec9136b499931 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 11 Nov 2024 10:34:12 +0000 Subject: [PATCH 22/23] chore(deps): update dependency vite to v5.4.11 --- Client/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Client/package-lock.json b/Client/package-lock.json index 83ffa6cfa..cbe559889 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -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", From 38690a813ebc1358d8e8f145c496b7c325f5a5fb Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 11 Nov 2024 16:17:44 +0000 Subject: [PATCH 23/23] fix(deps): update dependency bullmq to v5.25.6 --- Server/package-lock.json | 8 ++++---- Server/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index 53278a0e6..d03515453 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -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", diff --git a/Server/package.json b/Server/package.json index 8e822c024..16e0e9d1c 100644 --- a/Server/package.json +++ b/Server/package.json @@ -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",