add tests
This commit is contained in:
1 parent
b4da045770
commit
c9d33c4292
3 files changed
+215
-14
No files matched your search
@@ -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: {} });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user