add tests

This commit is contained in:
Alex Holliday committed 2025-11-14 19:10:34 +00:00
1 parent b4da045770
commit c9d33c4292
3 files changed
+215 -14

No files matched your search

+3 -1
View File
@@ -1,3 +1,5 @@
import got from "got";
import {
CheckService,
InviteService,
@@ -38,7 +40,7 @@ export const initServices = async () => {
const emailService = new EmailService(userService);
const slackService = new SlackService();
const webhookService = new WebhookService();
const networkService = new NetworkService();
const networkService = new NetworkService(got);
const statusService = new StatusService();
const notificationService = new NotificationService(userService);
const incidentService = new IncidentService();
@@ -1,5 +1,4 @@
import { Got, HTTPError } from "got";
import got from "got";
import ping from "ping";
import { IMonitor } from "@/db/models/index.js";
import { GotTimings } from "@/db/models/checks/Check.js";
@@ -48,15 +47,15 @@ export interface StatusResponse<TPayload = unknown> {
class NetworkService implements INetworkService {
public SERVICE_NAME = SERVICE_NAME;
private got: Got;
private client: Got;
private NETWORK_ERROR: number;
constructor() {
constructor(got: Got) {
this.SERVICE_NAME = SERVICE_NAME;
this.NETWORK_ERROR = 5000;
const cacheable = new CacheableLookup();
this.got = got.extend({
this.client = got.extend({
dnsCache: cacheable,
timeout: {
request: 30000,
@@ -112,7 +111,7 @@ class NetworkService implements INetworkService {
}
try {
const response: Response = await this.got(url);
const response: Response = await this.client(url);
return this.buildStatusResponse(monitor, response, null);
} catch (error) {
return this.buildStatusResponse(monitor, null, error);
@@ -134,10 +133,13 @@ class NetworkService implements INetworkService {
let statusResponse: StatusResponse<ICapturePayload>;
try {
const response: Response<ICapturePayload> | null = await this.got(url, {
headers: { Authorization: `Bearer ${secret}` },
responseType: "json",
});
const response: Response<ICapturePayload> | null = await this.client(
url,
{
headers: { Authorization: `Bearer ${secret}` },
responseType: "json",
}
);
statusResponse = this.buildStatusResponse(monitor, response, null);
if (!response?.body) {
@@ -167,7 +169,7 @@ class NetworkService implements INetworkService {
let statusResponse: StatusResponse<ILighthousePayload>;
try {
const response: Response = await this.got(url);
const response: Response = await this.client(url);
statusResponse = this.buildStatusResponse(
monitor,
response,
@@ -178,9 +180,12 @@ class NetworkService implements INetworkService {
}
const pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance&key=${apiKey}`;
const pagespeedResponse = await this.got<ILighthousePayload>(pagespeedUrl, {
responseType: "json",
});
const pagespeedResponse = await this.client<ILighthousePayload>(
pagespeedUrl,
{
responseType: "json",
}
);
const payload = pagespeedResponse.body;
if (payload) {
statusResponse.payload = payload;
@@ -0,0 +1,194 @@
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";
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 {},
}));
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();
});
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("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: {} });
});
});