From 3b738100ec3e6a2c5e9e96bcd71da8396cdd58da Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 14 Nov 2025 15:22:58 -0800 Subject: [PATCH] remove files --- .../infrastructure/NetworkService.test.ts | 613 ------------------ .../infrastructure/StatusService.test.ts | 321 --------- server/tests/utils/ApiError.test.ts | 17 - 3 files changed, 951 deletions(-) delete mode 100644 server/tests/services/infrastructure/NetworkService.test.ts delete mode 100644 server/tests/services/infrastructure/StatusService.test.ts delete mode 100644 server/tests/utils/ApiError.test.ts diff --git a/server/tests/services/infrastructure/NetworkService.test.ts b/server/tests/services/infrastructure/NetworkService.test.ts deleted file mode 100644 index 9b23ab574..000000000 --- a/server/tests/services/infrastructure/NetworkService.test.ts +++ /dev/null @@ -1,613 +0,0 @@ -import type { IMonitor } from "@/db/models/monitors/Monitor.js"; -import NetworkService from "@/services/infrastructure/NetworkService.js"; -import type { StatusResponse } from "@/services/infrastructure/NetworkService.js"; -import type { Response } from "got"; -import type { Got } from "got"; -import { Types } from "mongoose"; -import ApiError from "@/utils/ApiError.js"; -import ping from "ping"; - -jest.mock("@/config/index.js", () => ({ - config: { PAGESPEED_API_KEY: "test-key" }, -})); -const configModule = jest.requireMock("@/config/index.js"); - -jest.mock("got", () => { - const extend = jest.fn(); - - class MockHTTPError extends Error { - public response?: any; - public timings?: any; - constructor(message?: string, options: any = {}) { - super(message); - this.response = options.response; - this.timings = options.timings; - } - } - - return { - __esModule: true, - default: { extend }, - HTTPError: MockHTTPError, - }; -}); - -jest.mock("cacheable-lookup", () => ({ - __esModule: true, - default: class CacheableLookup {}, -})); - -jest.mock("ping", () => ({ - promise: { probe: jest.fn() }, -})); - -const getGotModule = () => jest.requireMock("got"); - -const buildMonitor = (overrides: Partial = {}): IMonitor => - ({ - _id: new Types.ObjectId(), - teamId: new Types.ObjectId(), - type: "http" as any, - url: "https://example.com", - secret: "secret", - interval: 60000, - isActive: true, - status: "up", - n: 1, - latestChecks: [], - save: jest.fn(), - createdBy: new Types.ObjectId(), - updatedBy: new Types.ObjectId(), - createdAt: new Date(), - updatedAt: new Date(), - ...overrides, - } as unknown as IMonitor); - -const createService = (clientMock?: jest.Mock) => { - const gotModule = getGotModule(); - const extendMock = gotModule.default.extend as jest.Mock; - const requestMock = clientMock ?? jest.fn(); - extendMock.mockReturnValue(requestMock); - const service = new NetworkService(gotModule.default as unknown as Got); - return { service, requestMock, extendMock, gotModule }; -}; - -const invokeBuildStatusResponse = ( - service: NetworkService, - monitor: IMonitor, - response: Response | null, - error: any | null -) => - ( - service as unknown as { - buildStatusResponse: ( - monitor: IMonitor, - response: Response | null, - error: any | null - ) => StatusResponse; - } - ).buildStatusResponse(monitor, response, error); - -beforeEach(() => { - const gotModule = getGotModule(); - (gotModule.default.extend as jest.Mock).mockReset(); - (ping.promise.probe as jest.Mock).mockReset(); - configModule.config.PAGESPEED_API_KEY = "test-key"; -}); - -beforeEach(() => { - const gotModule = getGotModule(); - (gotModule.default.extend as jest.Mock).mockReset(); - (ping.promise.probe as jest.Mock).mockReset(); - configModule.config.PAGESPEED_API_KEY = "test-key"; -}); - -describe("NetworkService.buildStatusResponse", () => { - it("maps successful response details", () => { - const { service } = createService(); - const timings = { - phases: { total: 321 }, - } as Response["timings"]; - const response = { - statusCode: 201, - statusMessage: "Accepted", - ok: true, - timings, - } as Response; - const monitor = buildMonitor(); - - const result = invokeBuildStatusResponse(service, monitor, response, null); - - expect(result.monitorId).toBe(monitor._id.toString()); - expect(result.teamId).toBe(monitor.teamId.toString()); - expect(result.type).toBe(monitor.type); - expect(result.code).toBe(201); - expect(result.status).toBe("up"); - expect(result.message).toBe("Accepted"); - expect(result.responseTime).toBe(321); - expect(result.timings).toBe(timings); - }); - - it("returns default error payload for generic failures", () => { - const { service } = createService(); - const monitor = buildMonitor(); - const error = new Error("timeout"); - - const result = invokeBuildStatusResponse(service, monitor, null, error); - - expect(result.monitorId).toBe(monitor._id.toString()); - expect(result.status).toBe("down"); - expect(result.code).toBe(5000); - expect(result.message).toBe("timeout"); - expect(result.responseTime).toBe(0); - expect(result.timings).toEqual({ phases: {} }); - }); - - it("returns default error payload for generic failures without message", () => { - const { service } = createService(); - const monitor = buildMonitor(); - const error = new Error("timeout"); - error.message = ""; - - const result = invokeBuildStatusResponse(service, monitor, null, error); - - expect(result.monitorId).toBe(monitor._id.toString()); - expect(result.status).toBe("down"); - expect(result.code).toBe(5000); - expect(result.message).toBe("Network error"); - expect(result.responseTime).toBe(0); - expect(result.timings).toEqual({ phases: {} }); - }); - - it("honors HTTPError metadata", () => { - const { service, gotModule } = createService(); - const { HTTPError } = gotModule; - const monitor = buildMonitor(); - const timings = { phases: { total: 987 } }; - const httpError = new HTTPError("Bad Gateway", { - response: { statusCode: 502 }, - timings, - }); - - const result = invokeBuildStatusResponse(service, monitor, null, httpError); - - expect(result.status).toBe("down"); - expect(result.code).toBe(502); - expect(result.message).toBe("Bad Gateway"); - expect(result.responseTime).toBe(987); - expect(result.timings).toBe(timings); - }); -}); - -describe("NetworkService.requestHttp", () => { - it("requests monitor URL and maps successful response", async () => { - const timings = { phases: { total: 123 } } as Response["timings"]; - const response = { - statusCode: 204, - statusMessage: "No Content", - ok: true, - timings, - } as Response; - const requestMock = jest.fn().mockResolvedValue(response); - const { service, extendMock } = createService(requestMock); - const monitor = buildMonitor({ url: "https://api.example.com" }); - - const result = await service.requestHttp(monitor); - - expect(extendMock).toHaveBeenCalledTimes(1); - expect(requestMock).toHaveBeenCalledWith("https://api.example.com"); - expect(result.status).toBe("up"); - expect(result.code).toBe(204); - expect(result.message).toBe("No Content"); - expect(result.responseTime).toBe(123); - expect(result.timings).toBe(timings); - }); - - it("throws when monitor is missing a URL", async () => { - const requestMock = jest.fn(); - const { service, extendMock } = createService(requestMock); - const monitor = buildMonitor({ url: undefined as unknown as string }); - - await expect(service.requestHttp(monitor)).rejects.toThrow( - "No URL provided" - ); - expect(extendMock).toHaveBeenCalledTimes(1); - expect(requestMock).not.toHaveBeenCalled(); - }); - - it("returns fallback response when request throws", async () => { - const error = new Error("connection reset"); - const requestMock = jest.fn().mockRejectedValue(error); - const { service, extendMock } = createService(requestMock); - const monitor = buildMonitor({ url: "https://api.example.com" }); - - const result = await service.requestHttp(monitor); - - expect(extendMock).toHaveBeenCalledTimes(1); - expect(requestMock).toHaveBeenCalledWith("https://api.example.com"); - expect(result.status).toBe("down"); - expect(result.code).toBe(5000); - expect(result.message).toBe("connection reset"); - expect(result.responseTime).toBe(0); - expect(result.timings).toEqual({ phases: {} }); - }); -}); - -// requestInfrastructure block covers success path and every guard/exception branch so all lines execute. -describe("NetworkService.requestInfrastructure", () => { - it("requests infrastructure endpoint with auth header and attaches payload", async () => { - const body = { data: { cpu: 10 }, capture: { version: "1" } }; - const timings = { phases: { total: 456 } } as Response["timings"]; - const response = { - statusCode: 200, - statusMessage: "OK", - ok: true, - timings, - body, - } as Response; - const requestMock = jest.fn().mockResolvedValue(response); - const { service, extendMock } = createService(requestMock); - const monitor = buildMonitor({ - type: "infrastructure" as any, - url: "https://infra.example.com", - secret: "topsecret", - }); - - const result = await service.requestInfrastructure(monitor); - - expect(extendMock).toHaveBeenCalledTimes(1); - expect(requestMock).toHaveBeenCalledWith("https://infra.example.com", { - headers: { Authorization: "Bearer topsecret" }, - responseType: "json", - }); - expect(result.status).toBe("up"); - expect(result.code).toBe(200); - expect(result.message).toBe("OK"); - expect(result.responseTime).toBe(456); - expect(result.payload).toBe(body); - }); - - it("throws when monitor is missing a URL", async () => { - const requestMock = jest.fn(); - const { service, extendMock } = createService(requestMock); - const monitor = buildMonitor({ - type: "infrastructure" as any, - url: undefined as any, - }); - - await expect(service.requestInfrastructure(monitor)).rejects.toThrow( - "No URL provided" - ); - expect(extendMock).toHaveBeenCalledTimes(1); - expect(requestMock).not.toHaveBeenCalled(); - }); - - it("throws when monitor is missing a secret", async () => { - const requestMock = jest.fn(); - const { service, extendMock } = createService(requestMock); - const monitor = buildMonitor({ - type: "infrastructure" as any, - secret: undefined as any, - }); - - await expect(service.requestInfrastructure(monitor)).rejects.toThrow( - "No secret provided for infrastructure monitor" - ); - expect(extendMock).toHaveBeenCalledTimes(1); - expect(requestMock).not.toHaveBeenCalled(); - }); - - it("returns fallback response when infrastructure payload missing", async () => { - const response = { - statusCode: 200, - statusMessage: "OK", - ok: true, - timings: { phases: { total: 111 } } as Response["timings"], - body: undefined, - } as Response; - const requestMock = jest.fn().mockResolvedValue(response); - const { service, extendMock } = createService(requestMock); - const monitor = buildMonitor({ - type: "infrastructure" as any, - url: "https://infra.example.com", - secret: "secret", - }); - - const result = await service.requestInfrastructure(monitor); - expect(extendMock).toHaveBeenCalledTimes(1); - expect(requestMock).toHaveBeenCalledWith("https://infra.example.com", { - headers: { Authorization: "Bearer secret" }, - responseType: "json", - }); - expect(result.status).toBe("down"); - expect(result.code).toBe(5000); - expect(result.message).toBe( - "No payload received from infrastructure monitor" - ); - }); - - it("returns fallback response when infrastructure request rejects", async () => { - const error = new Error("timeout"); - const requestMock = jest.fn().mockRejectedValue(error); - const { service, extendMock } = createService(requestMock); - const monitor = buildMonitor({ - type: "infrastructure" as any, - url: "https://infra.example.com", - secret: "secret", - }); - - const result = await service.requestInfrastructure(monitor); - - expect(extendMock).toHaveBeenCalledTimes(1); - expect(requestMock).toHaveBeenCalledWith("https://infra.example.com", { - headers: { Authorization: "Bearer secret" }, - responseType: "json", - }); - expect(result.status).toBe("down"); - expect(result.message).toBe("timeout"); - expect(result.code).toBe(5000); - }); -}); -describe("NetworkService.requestPagespeed", () => { - const pagespeedUrl = - "https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=https://site.example.com&category=seo&category=accessibility&category=best-practices&category=performance&key=test-key"; - - it("requests pagespeed URL and attaches payload", async () => { - const primaryResponse = { - statusCode: 200, - statusMessage: "OK", - ok: true, - timings: { phases: { total: 222 } }, - } as Response; - const payload = { lighthouseResult: { score: 0.9 } }; - const secondaryResponse = { body: payload } as Response; - const requestMock = jest - .fn() - .mockResolvedValueOnce(primaryResponse) - .mockResolvedValueOnce(secondaryResponse); - const { service, extendMock } = createService(requestMock); - const monitor = buildMonitor({ - type: "pagespeed" as any, - url: "https://site.example.com", - }); - - const result = await service.requestPagespeed(monitor); - - expect(extendMock).toHaveBeenCalledTimes(1); - expect(requestMock).toHaveBeenNthCalledWith(1, "https://site.example.com"); - expect(requestMock).toHaveBeenNthCalledWith(2, pagespeedUrl, { - responseType: "json", - }); - expect(result.status).toBe("up"); - expect(result.payload).toBe(payload); - }); - - it("throws when pagespeed API key missing", async () => { - configModule.config.PAGESPEED_API_KEY = ""; - const requestMock = jest.fn(); - const { service, extendMock } = createService(requestMock); - const monitor = buildMonitor({ type: "pagespeed" as any }); - - await expect(service.requestPagespeed(monitor)).rejects.toThrow( - "No API key provided for pagespeed monitor" - ); - expect(extendMock).toHaveBeenCalledTimes(1); - expect(requestMock).not.toHaveBeenCalled(); - }); - - it("throws when monitor lacks URL", async () => { - const requestMock = jest.fn(); - const { service, extendMock } = createService(requestMock); - const monitor = buildMonitor({ - type: "pagespeed" as any, - url: undefined as any, - }); - - await expect(service.requestPagespeed(monitor)).rejects.toThrow( - "No URL provided" - ); - expect(extendMock).toHaveBeenCalledTimes(1); - expect(requestMock).not.toHaveBeenCalled(); - }); - - it("returns fallback response when initial request fails", async () => { - const error = new Error("timeout"); - const payload = { lighthouseResult: { score: 0.8 } }; - const requestMock = jest - .fn() - .mockRejectedValueOnce(error) - .mockResolvedValueOnce({ body: payload }); - const { service, extendMock } = createService(requestMock); - const monitor = buildMonitor({ - type: "pagespeed" as any, - url: "https://s", - }); - - const result = await service.requestPagespeed(monitor); - - expect(extendMock).toHaveBeenCalledTimes(1); - expect(requestMock).toHaveBeenNthCalledWith(1, "https://s"); - expect(result.status).toBe("down"); - expect(result.message).toBe("timeout"); - expect(result.payload).toBe(payload); - }); - - it("throws ApiError when pagespeed payload missing", async () => { - const primaryResponse = { - statusCode: 200, - statusMessage: "OK", - ok: true, - timings: { phases: { total: 111 } }, - } as Response; - const requestMock = jest - .fn() - .mockResolvedValueOnce(primaryResponse) - .mockResolvedValueOnce({ body: undefined }); - const { service, extendMock } = createService(requestMock); - const monitor = buildMonitor({ - type: "pagespeed" as any, - url: "https://site.example.com", - }); - - await expect(service.requestPagespeed(monitor)).rejects.toThrow( - "No payload received from pagespeed monitor" - ); - expect(extendMock).toHaveBeenCalledTimes(1); - expect(requestMock).toHaveBeenNthCalledWith(2, pagespeedUrl, { - responseType: "json", - }); - }); -}); - -describe("NetworkService.requestPing", () => { - it("maps successful ping with string time", async () => { - (ping.promise.probe as jest.Mock).mockResolvedValue({ - alive: true, - time: "12.5", - }); - const { service } = createService(); - const monitor = buildMonitor({ type: "ping" as any, url: "8.8.8.8" }); - - const result = await service.requestPing(monitor); - - expect(ping.promise.probe).toHaveBeenCalledWith("8.8.8.8"); - expect(result.status).toBe("up"); - expect(result.responseTime).toBeCloseTo(12.5); - expect(result.timings?.phases).toEqual({}); - }); - - it("maps failure ping with invalid time to zero", async () => { - (ping.promise.probe as jest.Mock).mockResolvedValue({ - alive: false, - time: "NaN", - }); - const { service } = createService(); - const monitor = buildMonitor({ type: "ping" as any, url: "1.1.1.1" }); - - const result = await service.requestPing(monitor); - - expect(result.status).toBe("down"); - expect(result.responseTime).toBe(0); - expect(result.message).toBe("Ping successful"); - }); - - it("handles numeric ping times", async () => { - (ping.promise.probe as jest.Mock).mockResolvedValue({ - alive: true, - time: 18, - }); - const { service } = createService(); - const monitor = buildMonitor({ type: "ping" as any, url: "9.9.9.9" }); - - const result = await service.requestPing(monitor); - - expect(result.responseTime).toBe(18); - expect(result.status).toBe("up"); - }); -}); - -describe("NetworkService.requestStatus", () => { - it("routes http monitors through requestHttp", async () => { - const { service } = createService(); - const httpSpy = jest.spyOn(service, "requestHttp").mockResolvedValueOnce({ - monitorId: "1", - teamId: "t", - type: "http" as any, - status: "up", - message: "OK", - responseTime: 1, - timings: { phases: {} } as any, - }); - const monitor = buildMonitor({ type: "http" as any }); - - const result = await service.requestStatus(monitor); - - expect(httpSpy).toHaveBeenCalledTimes(1); - expect(result.status).toBe("up"); - }); - - it("routes https monitors through requestHttp", async () => { - const { service } = createService(); - const httpSpy = jest.spyOn(service, "requestHttp").mockResolvedValueOnce({ - monitorId: "2", - teamId: "t", - type: "https" as any, - status: "up", - message: "OK", - responseTime: 2, - timings: { phases: {} } as any, - }); - const monitor = buildMonitor({ type: "https" as any }); - - await service.requestStatus(monitor); - - expect(httpSpy).toHaveBeenCalledTimes(1); - }); - - it("routes infrastructure monitors through requestInfrastructure", async () => { - const { service } = createService(); - const infraSpy = jest - .spyOn(service, "requestInfrastructure") - .mockResolvedValueOnce({ - monitorId: "3", - teamId: "t", - type: "infrastructure" as any, - status: "up", - message: "OK", - responseTime: 3, - timings: { phases: {} } as any, - }); - const monitor = buildMonitor({ type: "infrastructure" as any }); - - await service.requestStatus(monitor); - - expect(infraSpy).toHaveBeenCalledTimes(1); - }); - - it("routes pagespeed monitors through requestPagespeed", async () => { - const { service } = createService(); - const pageSpy = jest - .spyOn(service, "requestPagespeed") - .mockResolvedValueOnce({ - monitorId: "4", - teamId: "t", - type: "pagespeed" as any, - status: "up", - message: "OK", - responseTime: 4, - timings: { phases: {} } as any, - }); - const monitor = buildMonitor({ type: "pagespeed" as any }); - - await service.requestStatus(monitor); - - expect(pageSpy).toHaveBeenCalledTimes(1); - }); - - it("routes ping monitors through requestPing", async () => { - const { service } = createService(); - const pingSpy = jest.spyOn(service, "requestPing").mockResolvedValueOnce({ - monitorId: "5", - teamId: "t", - type: "ping" as any, - status: "down", - message: "Timeout", - responseTime: 0, - timings: { phases: {} } as any, - }); - const monitor = buildMonitor({ type: "ping" as any }); - - await service.requestStatus(monitor); - - expect(pingSpy).toHaveBeenCalledTimes(1); - }); - - it("throws for unsupported monitor types", async () => { - const { service } = createService(); - const monitor = buildMonitor({ type: "websocket" as any }); - - await expect(service.requestStatus(monitor)).rejects.toThrow( - "Not implemented" - ); - }); -}); diff --git a/server/tests/services/infrastructure/StatusService.test.ts b/server/tests/services/infrastructure/StatusService.test.ts deleted file mode 100644 index 4589ba973..000000000 --- a/server/tests/services/infrastructure/StatusService.test.ts +++ /dev/null @@ -1,321 +0,0 @@ -import StatusService from "@/services/infrastructure/StatusService.js"; -import { MonitorStats } from "@/db/models/index.js"; -import ApiError from "@/utils/ApiError.js"; -import { Types } from "mongoose"; -import type { IMonitorStats } from "@/db/models/index.js"; -import type { StatusResponse } from "@/services/infrastructure/NetworkService.js"; -import type { IMonitor } from "@/db/models/monitors/Monitor.js"; -import type { MonitorStatus } from "@/db/models/monitors/Monitor.js"; - -const buildStats = ( - overrides: Partial }> = {} -): IMonitorStats => { - const saveMock = - overrides.save || - jest.fn().mockImplementation(async function (this: unknown) { - return this; - }); - return { - monitorId: {} as any, - avgResponseTime: 0, - maxResponseTime: 0, - totalChecks: 1, - totalUpChecks: 0, - totalDownChecks: 0, - uptimePercentage: 0, - lastCheckTimestamp: 0, - lastResponseTime: 0, - timeOfLastFailure: 0, - currentStreak: 0, - currentStreakStatus: "up", - currentStreakStartedAt: 0, - createdAt: new Date(), - updatedAt: new Date(), - save: saveMock, - ...overrides, - } as unknown as IMonitorStats; -}; - -const buildStatusResponse = ( - overrides: Partial = {} -): StatusResponse => ({ - monitorId: "monitor-id", - teamId: "team-id", - type: "http" as any, - status: "up", - message: "OK", - responseTime: 0, - ...overrides, -}); - -const buildMonitor = ( - overrides: Partial & { status?: MonitorStatus } -) => { - const monitor: any = { - _id: new Types.ObjectId(), - teamId: "team-id", - status: "up" as MonitorStatus, - latestChecks: [], - n: 1, - save: jest.fn(), - ...overrides, - }; - if (!monitor.latestChecks) { - monitor.latestChecks = []; - } - monitor.save.mockImplementation(async () => monitor); - return { - monitor: monitor as IMonitor, - saveMock: monitor.save as jest.Mock, - }; -}; - -describe("StatusService.calculateAvgResponseTime", () => { - const service = new StatusService(); - - it("returns latest response time when average is unset", () => { - const stats = buildStats({ avgResponseTime: 0, totalChecks: 1 }); - const statusResponse = buildStatusResponse({ responseTime: 250 }); - - const result = service.calculateAvgResponseTime(stats, statusResponse); - - expect(result).toBe(250); - }); - - it("computes rolling average when checks exist", () => { - const stats = buildStats({ avgResponseTime: 120, totalChecks: 5 }); - const statusResponse = buildStatusResponse({ responseTime: 300 }); - - const result = service.calculateAvgResponseTime(stats, statusResponse); - - expect(result).toBeCloseTo(156); - }); -}); - -describe("StatusService.updateMonitorStatus", () => { - const service = new StatusService(); - - it("initializing monitor adopts first status", async () => { - const { monitor, saveMock } = buildMonitor({ - status: "initializing" as MonitorStatus, - latestChecks: [], - }); - const statusResponse = buildStatusResponse({ status: "up", responseTime: 120 }); - - const [savedMonitor, statusChanged] = await service.updateMonitorStatus( - monitor, - statusResponse - ); - - expect(saveMock).toHaveBeenCalledTimes(1); - expect(monitor.status).toBe("up"); - expect(monitor.latestChecks).toHaveLength(1); - expect(monitor.latestChecks?.[0]?.status).toBe("up"); - expect(monitor.lastCheckedAt).toBeInstanceOf(Date); - expect(savedMonitor).toBe(monitor); - expect(statusChanged).toBe(false); - }); - - it("returns early when not enough checks accumulated", async () => { - const { monitor, saveMock } = buildMonitor({ - status: "up" as MonitorStatus, - n: 3, - latestChecks: [], - }); - const statusResponse = buildStatusResponse({ status: "down", responseTime: 90 }); - - const [, statusChanged] = await service.updateMonitorStatus( - monitor, - statusResponse - ); - - expect(saveMock).toHaveBeenCalledTimes(1); - expect(monitor.status).toBe("up"); - expect(monitor.latestChecks).toHaveLength(1); - expect(statusChanged).toBe(false); - }); - - it("updates status when last n checks differ", async () => { - const { monitor, saveMock } = buildMonitor({ - status: "up" as MonitorStatus, - n: 2, - latestChecks: [ - { - status: "down" as MonitorStatus, - responseTime: 80, - checkedAt: new Date("2024-01-01"), - }, - ], - }); - const statusResponse = buildStatusResponse({ status: "down", responseTime: 70 }); - - const [, statusChanged] = await service.updateMonitorStatus( - monitor, - statusResponse - ); - - expect(saveMock).toHaveBeenCalledTimes(1); - expect(monitor.status).toBe("down"); - expect(statusChanged).toBe(true); - expect(monitor.latestChecks.slice(-2).every((c) => c.status === "down")).toBe( - true - ); - }); - - it("keeps only latest 25 checks", async () => { - const existingChecks = Array.from({ length: 25 }, (_, index) => ({ - status: "up" as MonitorStatus, - responseTime: index, - checkedAt: new Date(`2024-01-${index + 1}`), - })); - const { monitor, saveMock } = buildMonitor({ - status: "up" as MonitorStatus, - n: 1, - latestChecks: [...existingChecks], - }); - const statusResponse = buildStatusResponse({ status: "up", responseTime: 200 }); - - await service.updateMonitorStatus(monitor, statusResponse); - - expect(saveMock).toHaveBeenCalledTimes(1); - expect(monitor.latestChecks).toHaveLength(25); - expect(monitor.latestChecks?.[0]?.responseTime).toBe(1); - expect( - monitor.latestChecks?.some((check) => check.responseTime === 0) - ).toBe(false); - const lastCheck = - monitor.latestChecks?.[monitor.latestChecks.length - 1] ?? null; - expect(lastCheck?.responseTime).toBe(200); - }); - - it("initializes latestChecks when missing", async () => { - const { monitor, saveMock } = buildMonitor({}); - delete (monitor as any).latestChecks; - const statusResponse = buildStatusResponse({ status: "up", responseTime: 75 }); - - const [, statusChanged] = await service.updateMonitorStatus( - monitor, - statusResponse - ); - - expect(saveMock).toHaveBeenCalledTimes(1); - expect(Array.isArray(monitor.latestChecks)).toBe(true); - expect(monitor.latestChecks).toHaveLength(1); - expect(statusChanged).toBe(false); - }); -}); - -describe("StatusService.updateMonitorStats", () => { - const service = new StatusService(); - let findOneSpy: jest.SpiedFunction; - - beforeEach(() => { - findOneSpy = jest.spyOn(MonitorStats, "findOne"); - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.useRealTimers(); - jest.restoreAllMocks(); - }); - - it("throws ApiError when monitor stats are missing", async () => { - findOneSpy.mockResolvedValue(null); - const { monitor } = buildMonitor({}); - const statusResponse = buildStatusResponse({ status: "up" }); - - await expect( - service.updateMonitorStats(monitor, statusResponse, false) - ).rejects.toBeInstanceOf(ApiError); - await expect( - service.updateMonitorStats(monitor, statusResponse, false) - ).rejects.toMatchObject({ message: "MonitorStats not found", status: 500 }); - expect(findOneSpy).toHaveBeenCalledWith({ monitorId: monitor._id }); - }); - - it("increments counters and preserves streak when status unchanged", async () => { - const now = new Date("2024-02-01T12:00:00Z"); - jest.setSystemTime(now); - const stats = buildStats({ - totalChecks: 4, - totalUpChecks: 3, - totalDownChecks: 1, - avgResponseTime: 100, - maxResponseTime: 250, - lastResponseTime: 90, - currentStreak: 2, - currentStreakStatus: "up", - currentStreakStartedAt: 111, - lastCheckTimestamp: 1, - timeOfLastFailure: 555, - }); - const saveMock = stats.save as jest.Mock; - findOneSpy.mockResolvedValue(stats); - const { monitor } = buildMonitor({}); - const statusResponse = buildStatusResponse({ status: "up", responseTime: 150 }); - - const result = await service.updateMonitorStats( - monitor, - statusResponse, - false - ); - - expect(findOneSpy).toHaveBeenCalledWith({ monitorId: monitor._id }); - expect(saveMock).toHaveBeenCalledTimes(1); - expect(result).toBe(stats); - expect(stats.totalChecks).toBe(5); - expect(stats.totalUpChecks).toBe(4); - expect(stats.totalDownChecks).toBe(1); - expect(stats.currentStreak).toBe(3); - expect(stats.currentStreakStatus).toBe("up"); - expect(stats.currentStreakStartedAt).toBe(111); - expect(stats.lastCheckTimestamp).toBe(now.getTime()); - expect(stats.timeOfLastFailure).toBe(555); - expect(stats.avgResponseTime).toBeCloseTo(110); - expect(stats.uptimePercentage).toBeCloseTo(4 / 5); - expect(stats.lastResponseTime).toBe(150); - expect(stats.maxResponseTime).toBe(250); - }); - - it("resets streak and records failure on status change to down", async () => { - const now = new Date("2024-02-01T12:05:00Z"); - jest.setSystemTime(now); - const stats = buildStats({ - totalChecks: 10, - totalUpChecks: 7, - totalDownChecks: 3, - avgResponseTime: 200, - maxResponseTime: 300, - currentStreak: 5, - currentStreakStatus: "up", - currentStreakStartedAt: 999, - timeOfLastFailure: 444, - }); - const saveMock = stats.save as jest.Mock; - findOneSpy.mockResolvedValue(stats); - const { monitor } = buildMonitor({}); - const statusResponse = buildStatusResponse({ status: "down", responseTime: 400 }); - - const result = await service.updateMonitorStats( - monitor, - statusResponse, - true - ); - - expect(findOneSpy).toHaveBeenCalledWith({ monitorId: monitor._id }); - expect(saveMock).toHaveBeenCalledTimes(1); - expect(result).toBe(stats); - expect(stats.totalChecks).toBe(11); - expect(stats.totalUpChecks).toBe(7); - expect(stats.totalDownChecks).toBe(4); - expect(stats.currentStreak).toBe(1); - expect(stats.currentStreakStatus).toBe("down"); - expect(stats.currentStreakStartedAt).toBe(now.getTime()); - expect(stats.lastCheckTimestamp).toBe(now.getTime()); - expect(stats.timeOfLastFailure).toBe(now.getTime()); - expect(stats.avgResponseTime).toBeCloseTo((200 * 10 + 400) / 11); - expect(stats.uptimePercentage).toBeCloseTo(7 / 11); - expect(stats.lastResponseTime).toBe(400); - expect(stats.maxResponseTime).toBe(400); - }); -}); diff --git a/server/tests/utils/ApiError.test.ts b/server/tests/utils/ApiError.test.ts deleted file mode 100644 index 2bc2ca728..000000000 --- a/server/tests/utils/ApiError.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import ApiError from "@/utils/ApiError.js"; - -describe("ApiError", () => { - it("captures message and status", () => { - const error = new ApiError("boom", 418); - expect(error).toBeInstanceOf(Error); - expect(error.message).toBe("boom"); - expect(error.status).toBe(418); - expect(error.name).toBe("ApiError"); - }); - - it("defaults status to 500", () => { - const error = new ApiError("failure"); - expect(error.status).toBe(500); - }); -}); -