remove files
This commit is contained in:
1 parent
c7046be642
commit
3b738100ec
3 files changed
-951
No files matched your search
@@ -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> = {}): 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 = <T>(
|
||||
service: NetworkService,
|
||||
monitor: IMonitor,
|
||||
response: Response<T> | null,
|
||||
error: any | null
|
||||
) =>
|
||||
(
|
||||
service as unknown as {
|
||||
buildStatusResponse: (
|
||||
monitor: IMonitor,
|
||||
response: Response<T> | null,
|
||||
error: any | null
|
||||
) => StatusResponse<T>;
|
||||
}
|
||||
).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<typeof body>;
|
||||
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<any>;
|
||||
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<typeof payload>;
|
||||
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"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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 & { save: jest.Mock<any, any> }> = {}
|
||||
): 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> = {}
|
||||
): StatusResponse => ({
|
||||
monitorId: "monitor-id",
|
||||
teamId: "team-id",
|
||||
type: "http" as any,
|
||||
status: "up",
|
||||
message: "OK",
|
||||
responseTime: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const buildMonitor = (
|
||||
overrides: Partial<IMonitor> & { 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<typeof MonitorStats.findOne>;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user