From cf67c82f5ecf1714a33eb1390c19dbdc452dba7b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 22:06:46 +0000 Subject: [PATCH] convert job queue and job queue helper --- server/jest.config.ts | 11 ++- ...eueHelper.js => SuperSimpleQueueHelper.ts} | 60 ++++++----- server/test/superSimpleQueueHelper.test.ts | 99 +++++++++++++++++++ server/tsconfig.jest.json | 3 +- server/tsconfig.json | 2 +- 5 files changed, 149 insertions(+), 26 deletions(-) rename server/src/service/infrastructure/SuperSimpleQueue/{SuperSimpleQueueHelper.js => SuperSimpleQueueHelper.ts} (69%) create mode 100644 server/test/superSimpleQueueHelper.test.ts diff --git a/server/jest.config.ts b/server/jest.config.ts index 2bcfcbf21..4fb2c9831 100644 --- a/server/jest.config.ts +++ b/server/jest.config.ts @@ -1,5 +1,14 @@ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { pathsToModuleNameMapper } from "ts-jest"; import type { Config } from "jest"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const tsconfigPath = path.resolve(__dirname, "./tsconfig.json"); +const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf-8")); + const config: Config = { rootDir: ".", testEnvironment: "node", @@ -8,7 +17,7 @@ const config: Config = { "^.+\\.(t|j)sx?$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }], }, moduleNameMapper: { - "^@/(.*)$": "/src/$1", + ...pathsToModuleNameMapper(tsconfig.compilerOptions.paths || {}, { prefix: "/" }), }, testMatch: ["/test/**/*.test.ts"], setupFilesAfterEnv: [], diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts similarity index 69% rename from server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js rename to server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 3cd6b37a8..678986c62 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,18 +1,30 @@ const SERVICE_NAME = "JobQueueHelper"; +import type { Monitor } from "@/types/monitor.js"; +import { AppError } from "@/utils/AppError.js"; +import { error } from "winston"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; - /** - * @param {{ - * db: import("../database.js").Database, - * logger: import("../logger.js").Logger, - * networkService: import("../networkService.js").NetworkService, - * statusService: import("../statusService.js").StatusService, - * notificationService: import("../notificationService.js").NotificationService - * }} - */ - constructor({ db, logger, networkService, statusService, notificationService }) { + private db: any; + private logger: any; + private networkService: any; + private statusService: any; + private notificationService: any; + + constructor({ + db, + logger, + networkService, + statusService, + notificationService, + }: { + db: any; + logger: any; + networkService: any; + statusService: any; + notificationService: any; + }) { this.db = db; this.logger = logger; this.networkService = networkService; @@ -25,39 +37,41 @@ class SuperSimpleQueueHelper { } getMonitorJob = () => { - return async (monitor) => { + return async (monitor: Monitor) => { try { const monitorId = monitor.id; const teamId = monitor.teamId; if (!monitorId) { - throw new Error("No monitor id"); + throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getMonitorJob" }); } + // Step 1. Check for maintenacne window, if found, skip the check const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId); if (maintenanceWindowActive) { - this.logger.info({ + this.logger.debug({ message: `Monitor ${monitorId} is in maintenance window`, service: SERVICE_NAME, method: "getMonitorJob", }); return; } - const networkResponse = await this.networkService.requestStatus(monitor); - if (!networkResponse) { + // Step 2. Request monitor status + const status = await this.networkService.requestStatus(monitor); + if (!status) { throw new Error("No network response"); } - const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(networkResponse); + const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(status); this.notificationService .handleNotifications({ - ...networkResponse, + ...status, monitor: updatedMonitor, prevStatus, statusChanged, }) - .catch((error) => { + .catch((error: any) => { this.logger.error({ message: error.message, service: SERVICE_NAME, @@ -66,7 +80,7 @@ class SuperSimpleQueueHelper { stack: error.stack, }); }); - } catch (error) { + } catch (error: any) { this.logger.warn({ message: error.message, service: error.service || SERVICE_NAME, @@ -78,13 +92,13 @@ class SuperSimpleQueueHelper { }; }; - async isInMaintenanceWindow(monitorId, teamId) { + async isInMaintenanceWindow(monitorId: string, teamId: string) { const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({ - monitorId: monitorId.toString(), - teamId: teamId.toString(), + monitorId: monitorId, + teamId: teamId, }); // Check for active maintenance window: - const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => { + const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: any, window: any) => { if (window.active) { const start = new Date(window.start); const end = new Date(window.end); diff --git a/server/test/superSimpleQueueHelper.test.ts b/server/test/superSimpleQueueHelper.test.ts new file mode 100644 index 000000000..056173c82 --- /dev/null +++ b/server/test/superSimpleQueueHelper.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import SuperSimpleQueueHelper from "../src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts"; +import type { Monitor } from "../src/types/monitor.ts"; + +const createLogger = () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn() }); + +const createHelper = (overrides?: Partial[0]>) => { + const maintenanceWindowModule = { + getMaintenanceWindowsByMonitorId: jest.fn().mockResolvedValue([]), + }; + const helper = new SuperSimpleQueueHelper({ + db: { maintenanceWindowModule }, + logger: createLogger(), + networkService: { requestStatus: jest.fn() }, + statusService: { updateStatus: jest.fn() }, + notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) }, + ...overrides, + }); + return { helper, maintenanceWindowModule }; +}; + +describe("SuperSimpleQueueHelper", () => { + describe("getMonitorJob", () => { + it("skips execution when monitor is in maintenance window", async () => { + const { helper } = createHelper(); + const spy = jest.spyOn(helper, "isInMaintenanceWindow").mockResolvedValue(true); + const job = helper.getMonitorJob(); + await job({ id: "m1", teamId: "team", interval: 60000 } as Monitor); + expect(helper["networkService"].requestStatus).not.toHaveBeenCalled(); + expect(helper["logger"].info).toHaveBeenCalledWith( + expect.objectContaining({ message: expect.stringContaining("Monitor m1 is in maintenance window") }) + ); + spy.mockRestore(); + }); + + it("processes monitor status and notifications when active", async () => { + const networkResponse = { monitor: { id: "m1" }, status: true }; + const updatedMonitor = { id: "m1", status: true }; + const { helper } = createHelper({ + networkService: { requestStatus: jest.fn().mockResolvedValue(networkResponse) }, + statusService: { + updateStatus: jest.fn().mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }), + }, + notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) }, + }); + jest.spyOn(helper, "isInMaintenanceWindow").mockResolvedValue(false); + const job = helper.getMonitorJob(); + const monitor = { id: "m1", teamId: "team" } as Monitor; + await job(monitor); + expect(helper["networkService"].requestStatus).toHaveBeenCalledWith(monitor); + expect(helper["statusService"].updateStatus).toHaveBeenCalledWith(networkResponse); + expect(helper["notificationService"].handleNotifications).toHaveBeenCalledWith( + expect.objectContaining({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }) + ); + }); + + it("throws when monitor id is missing", async () => { + const { helper } = createHelper(); + const job = helper.getMonitorJob(); + await expect(job({} as Monitor)).rejects.toThrow("No monitor id"); + expect(helper["logger"].warn).toHaveBeenCalled(); + }); + }); + + describe("isInMaintenanceWindow", () => { + it("returns true when an active window spans now", async () => { + const now = new Date(); + const { helper, maintenanceWindowModule } = createHelper(); + maintenanceWindowModule.getMaintenanceWindowsByMonitorId.mockResolvedValue([ + { + active: true, + start: new Date(now.getTime() - 1000).toISOString(), + end: new Date(now.getTime() + 1000).toISOString(), + repeat: 0, + }, + ]); + await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(true); + }); + + it("returns true when repeat interval advances window into current time", async () => { + const now = Date.now(); + const { helper, maintenanceWindowModule } = createHelper(); + maintenanceWindowModule.getMaintenanceWindowsByMonitorId.mockResolvedValue([ + { + active: true, + start: new Date(now - 7200000).toISOString(), + end: new Date(now - 6600000).toISOString(), + repeat: 3600000, + }, + ]); + await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(true); + }); + + it("returns false when no active windows exist", async () => { + const { helper } = createHelper(); + await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(false); + }); + }); +}); diff --git a/server/tsconfig.jest.json b/server/tsconfig.jest.json index 928b0977c..80c321cf6 100644 --- a/server/tsconfig.jest.json +++ b/server/tsconfig.jest.json @@ -3,7 +3,8 @@ "compilerOptions": { "rootDir": ".", "types": ["jest"], - "noEmit": true + "noEmit": true, + "resolveJsonModule": true }, "include": ["src", "test"] } diff --git a/server/tsconfig.json b/server/tsconfig.json index cbb874656..bdb3a7b0e 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -4,7 +4,7 @@ "allowJs": true, "checkJs": false, "paths": { - "@/*": ["./src/*"] // allows "@/db" -> "./src/db" + "@/*": ["./src/*"] }, "outDir": "./dist", "strict": true,