add provider tests

This commit is contained in:
Alex Holliday committed 2026-04-09 10:36:59 -07:00
1 parent dca09daf35
commit 63efef65c7
15 files changed
+2434 -4

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@
"type": "module",
"scripts": {
"test": "NODE_OPTIONS=--experimental-vm-modules jest --coverage",
"test:services": "NODE_OPTIONS=--experimental-vm-modules jest --coverage --collectCoverageFrom='src/service/**/*.ts' test/unit/services",
"test:services": "NODE_OPTIONS=--experimental-vm-modules jest --coverage --collectCoverageFrom='src/service/**/*.ts' test/unit/services test/unit/providers",
"dev": "nodemon --exec tsx src/index.js",
"start": "node --watch ./dist/index.js",
"build": "tsc && tsc-alias && cp -r src/templates dist/templates",
@@ -90,7 +90,7 @@ export class DockerProvider implements IStatusProvider<DockerStatusPayload> {
if (partialIdMatch && !exactIdMatch) matchTypes.push("partial ID");
if (matchTypes.length > 1) {
const message = `Ambiguous container match for "${containerInput}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`;
const message = `Ambiguous container match for "${containerInput}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : "exact name"} match.`;
this.logger.warn({
message,
@@ -142,7 +142,7 @@ export class DockerProvider implements IStatusProvider<DockerStatusPayload> {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: response?.State?.Status === "running",
status: response.State?.Status === "running",
code: 200,
message: "Docker container status fetched successfully",
responseTime,
@@ -60,7 +60,7 @@ export class GrpcProvider implements IStatusProvider<GrpcStatusPayload> {
throw new AppError({ message: "Monitor port is required", service: SERVICE_NAME, method: "handle" });
}
const host = url?.replace(/^https?:\/\//, "").split(/[/?#:]/)[0];
const host = url.replace(/^https?:\/\//, "").split(/[/?#:]/)[0];
const target = `${host}:${port}`;
const currentFilePath = fileURLToPath(import.meta.url);
@@ -0,0 +1,54 @@
import { describe, expect, it } from "@jest/globals";
import type { IStatusProvider } from "../../src/service/infrastructure/network/IStatusProvider.ts";
import type { Monitor } from "../../src/types/index.ts";
/**
* Shared contract tests for all IStatusProvider implementations.
* Tests the interface guarantees that every provider must satisfy.
*/
export const testStatusProviderContract = (
name: string,
opts: {
create: () => IStatusProvider<unknown>;
supportedType: string;
unsupportedType: string;
makeMonitor: () => Monitor;
}
) => {
describe(`IStatusProvider contract: ${name}`, () => {
it("has a readonly type property matching the supported type", () => {
const provider = opts.create();
expect(provider.type).toBe(opts.supportedType);
});
it("supports() returns true for its own type", () => {
const provider = opts.create();
expect(provider.supports(opts.supportedType as any)).toBe(true);
});
it("supports() returns false for a different type", () => {
const provider = opts.create();
expect(provider.supports(opts.unsupportedType as any)).toBe(false);
});
it("handle() returns an object with required MonitorStatusResponse fields", async () => {
const provider = opts.create();
try {
const result = await provider.handle(opts.makeMonitor());
expect(result).toEqual(
expect.objectContaining({
monitorId: expect.anything(),
teamId: expect.anything(),
type: expect.anything(),
status: expect.any(Boolean),
code: expect.any(Number),
message: expect.any(String),
})
);
} catch (err: any) {
// Some providers throw AppError on failure — that's also valid behavior
expect(err).toBeDefined();
}
});
});
};
@@ -0,0 +1,228 @@
import { describe, expect, it, jest } from "@jest/globals";
import { AdvancedMatcher } from "../../../src/service/infrastructure/network/AdvancedMatcher.ts";
import type { Monitor } from "../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
const createMockJmespath = () => ({
search: jest.fn((data: any, expr: string) => {
// Simple dot-path evaluation for tests
return expr.split(".").reduce((acc: any, key: string) => acc?.[key], data);
}),
});
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
({
useAdvancedMatching: false,
jsonPath: undefined,
matchMethod: undefined,
expectedValue: undefined,
...overrides,
}) as Monitor;
// ── Tests ────────────────────────────────────────────────────────────────────
describe("AdvancedMatcher", () => {
describe("validate", () => {
it("returns ok when advanced matching is disabled", () => {
const matcher = new AdvancedMatcher(createMockJmespath() as any);
const result = matcher.validate({ data: "test" }, makeMonitor({ useAdvancedMatching: false }));
expect(result).toEqual({ ok: true, message: "Success" });
});
// ── jsonPath extraction ──────────────────────────────────────────
describe("jsonPath extraction", () => {
it("extracts value using jsonPath", () => {
const matcher = new AdvancedMatcher(createMockJmespath() as any);
const result = matcher.validate({ status: "healthy" }, makeMonitor({ useAdvancedMatching: true, jsonPath: "status" }));
expect(result).toEqual({ ok: true, message: "Success", extracted: "healthy" });
});
it("returns error when jsonPath evaluation throws", () => {
const jmespath = {
search: jest.fn().mockImplementation(() => {
throw new Error("bad expression");
}),
};
const matcher = new AdvancedMatcher(jmespath as any);
const result = matcher.validate({ data: "test" }, makeMonitor({ useAdvancedMatching: true, jsonPath: "invalid[" }));
expect(result).toEqual({ ok: false, message: "Error evaluating JSON path" });
});
});
// ── expectedValue matching ───────────────────────────────────────
describe("expectedValue with matchMethod", () => {
it("matches with 'equal' method", () => {
const matcher = new AdvancedMatcher(createMockJmespath() as any);
const result = matcher.validate(
{ status: "ok" },
makeMonitor({ useAdvancedMatching: true, jsonPath: "status", matchMethod: "equal", expectedValue: "ok" })
);
expect(result.ok).toBe(true);
});
it("fails with 'equal' method on mismatch", () => {
const matcher = new AdvancedMatcher(createMockJmespath() as any);
const result = matcher.validate(
{ status: "error" },
makeMonitor({ useAdvancedMatching: true, jsonPath: "status", matchMethod: "equal", expectedValue: "ok" })
);
expect(result.ok).toBe(false);
expect(result.message).toBe("Expected value did not match");
});
it("matches with 'include' method", () => {
const matcher = new AdvancedMatcher(createMockJmespath() as any);
const result = matcher.validate(
{ msg: "server is healthy" },
makeMonitor({ useAdvancedMatching: true, jsonPath: "msg", matchMethod: "include", expectedValue: "healthy" })
);
expect(result.ok).toBe(true);
});
it("fails with 'include' method when substring not found", () => {
const matcher = new AdvancedMatcher(createMockJmespath() as any);
const result = matcher.validate(
{ msg: "server is down" },
makeMonitor({ useAdvancedMatching: true, jsonPath: "msg", matchMethod: "include", expectedValue: "healthy" })
);
expect(result.ok).toBe(false);
});
it("matches with 'regex' method", () => {
const matcher = new AdvancedMatcher(createMockJmespath() as any);
const result = matcher.validate(
{ code: "200" },
makeMonitor({ useAdvancedMatching: true, jsonPath: "code", matchMethod: "regex", expectedValue: "^2\\d{2}$" })
);
expect(result.ok).toBe(true);
});
it("fails with 'regex' method on mismatch", () => {
const matcher = new AdvancedMatcher(createMockJmespath() as any);
const result = matcher.validate(
{ code: "500" },
makeMonitor({ useAdvancedMatching: true, jsonPath: "code", matchMethod: "regex", expectedValue: "^2\\d{2}$" })
);
expect(result.ok).toBe(false);
});
it("defaults to 'equal' comparison when matchMethod is undefined", () => {
const matcher = new AdvancedMatcher(createMockJmespath() as any);
const result = matcher.validate({ val: "exact" }, makeMonitor({ useAdvancedMatching: true, jsonPath: "val", expectedValue: "exact" }));
expect(result.ok).toBe(true);
});
it("defaults to 'equal' comparison for unknown matchMethod", () => {
const matcher = new AdvancedMatcher(createMockJmespath() as any);
const result = matcher.validate(
{ val: "exact" },
makeMonitor({ useAdvancedMatching: true, jsonPath: "val", matchMethod: "unknown" as any, expectedValue: "exact" })
);
expect(result.ok).toBe(true);
});
it("compares entire payload when jsonPath is not set", () => {
const matcher = new AdvancedMatcher(createMockJmespath() as any);
const result = matcher.validate(
"hello",
makeMonitor({ useAdvancedMatching: true, jsonPath: undefined, matchMethod: "equal", expectedValue: "hello" })
);
expect(result.ok).toBe(true);
expect(result.extracted).toBe("hello");
});
it("includes extracted value in result", () => {
const matcher = new AdvancedMatcher(createMockJmespath() as any);
const result = matcher.validate(
{ nested: { value: "42" } },
makeMonitor({ useAdvancedMatching: true, jsonPath: "nested.value", matchMethod: "equal", expectedValue: "42" })
);
expect(result.extracted).toBe("42");
});
});
// ── falsy value check (no expectedValue) ────────────────────────
describe("falsy value check (no expectedValue)", () => {
it("returns ok for truthy extracted value", () => {
const matcher = new AdvancedMatcher(createMockJmespath() as any);
const result = matcher.validate({ status: "up" }, makeMonitor({ useAdvancedMatching: true, jsonPath: "status" }));
expect(result.ok).toBe(true);
expect(result.extracted).toBe("up");
});
it("returns not ok for false", () => {
const jmespath = { search: jest.fn().mockReturnValue(false) };
const matcher = new AdvancedMatcher(jmespath as any);
const result = matcher.validate({}, makeMonitor({ useAdvancedMatching: true, jsonPath: "missing" }));
expect(result.ok).toBe(false);
expect(result.message).toBe("Extracted value is falsy");
});
it("returns not ok for string 'false'", () => {
const jmespath = { search: jest.fn().mockReturnValue("false") };
const matcher = new AdvancedMatcher(jmespath as any);
const result = matcher.validate({}, makeMonitor({ useAdvancedMatching: true, jsonPath: "field" }));
expect(result.ok).toBe(false);
});
it("returns not ok for undefined", () => {
const jmespath = { search: jest.fn().mockReturnValue(undefined) };
const matcher = new AdvancedMatcher(jmespath as any);
const result = matcher.validate({}, makeMonitor({ useAdvancedMatching: true, jsonPath: "field" }));
expect(result.ok).toBe(false);
});
it("returns not ok for null", () => {
const jmespath = { search: jest.fn().mockReturnValue(null) };
const matcher = new AdvancedMatcher(jmespath as any);
const result = matcher.validate({}, makeMonitor({ useAdvancedMatching: true, jsonPath: "field" }));
expect(result.ok).toBe(false);
});
it("returns ok for zero (not in falsy list)", () => {
const jmespath = { search: jest.fn().mockReturnValue(0) };
const matcher = new AdvancedMatcher(jmespath as any);
const result = matcher.validate({}, makeMonitor({ useAdvancedMatching: true, jsonPath: "field" }));
expect(result.ok).toBe(true);
});
it("returns ok for empty string (not in falsy list)", () => {
const jmespath = { search: jest.fn().mockReturnValue("") };
const matcher = new AdvancedMatcher(jmespath as any);
const result = matcher.validate({}, makeMonitor({ useAdvancedMatching: true, jsonPath: "field" }));
expect(result.ok).toBe(true);
});
});
});
});
@@ -0,0 +1,328 @@
import { describe, expect, it, jest } from "@jest/globals";
import { DockerProvider } from "../../../src/service/infrastructure/network/DockerProvider.ts";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import { createMockLogger } from "../../helpers/createMockLogger.ts";
import { NETWORK_ERROR } from "../../../src/service/infrastructure/network/utils.ts";
import type { Monitor } from "../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
({
id: "mon-1",
teamId: "team-1",
type: "docker",
url: "my-container",
...overrides,
}) as Monitor;
const makeContainer = (overrides?: Record<string, any>) => ({
Id: "abc123def456abc123def456abc123def456abc123def456abc123def456abcd",
Names: ["/my-container"],
State: "running",
...overrides,
});
const createMockDocker = (containers: any[] = [makeContainer()], inspectResult?: any) => {
const inspect = jest.fn().mockResolvedValue(inspectResult ?? { State: { Status: "running" } });
const getContainer = jest.fn().mockReturnValue({ inspect });
const instance = {
listContainers: jest.fn().mockResolvedValue(containers),
getContainer,
};
const DockerLib = jest.fn().mockReturnValue(instance) as any;
return { DockerLib, instance, inspect, getContainer };
};
const createProvider = (opts?: { containers?: any[]; inspectResult?: any }) => {
const logger = createMockLogger();
const { DockerLib, instance, inspect, getContainer } = createMockDocker(opts?.containers, opts?.inspectResult);
const provider = new DockerProvider(logger as any, DockerLib);
return { provider, logger, docker: instance, inspect, getContainer };
};
// ── Contract ─────────────────────────────────────────────────────────────────
testStatusProviderContract("DockerProvider", {
create: () => createProvider().provider,
supportedType: "docker",
unsupportedType: "http",
makeMonitor: () => makeMonitor(),
});
// ── Tests ────────────────────────────────────────────────────────────────────
describe("DockerProvider", () => {
// ── Container matching ───────────────────────────────────────────────
describe("container matching", () => {
it("matches by exact container name", async () => {
const { provider } = createProvider();
const result = await provider.handle(makeMonitor({ url: "my-container" }));
expect(result.status).toBe(true);
expect(result.code).toBe(200);
});
it("matches by exact full ID (64 chars)", async () => {
const fullId = "abc123def456abc123def456abc123def456abc123def456abc123def456abcd";
const { provider } = createProvider();
const result = await provider.handle(makeMonitor({ url: fullId }));
expect(result.status).toBe(true);
});
it("matches by partial ID prefix", async () => {
const { provider } = createProvider();
const result = await provider.handle(makeMonitor({ url: "abc123" }));
expect(result.status).toBe(true);
});
it("strips leading slashes from input", async () => {
const { provider } = createProvider();
const result = await provider.handle(makeMonitor({ url: "///my-container" }));
expect(result.status).toBe(true);
});
it("matches case-insensitively", async () => {
const { provider } = createProvider();
const result = await provider.handle(makeMonitor({ url: "MY-CONTAINER" }));
expect(result.status).toBe(true);
});
it("returns 404 when no container matches", async () => {
const { provider, logger } = createProvider();
const result = await provider.handle(makeMonitor({ url: "nonexistent" }));
expect(result.status).toBe(false);
expect(result.code).toBe(404);
expect(result.message).toBe("Docker container not found");
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("No container found"),
})
);
});
});
// ── Ambiguity detection ──────────────────────────────────────────────
describe("ambiguity detection", () => {
it("returns error when input matches both exact name and partial ID", async () => {
// Container whose name matches the input AND whose ID starts with the input
const container = makeContainer({
Id: "my-containerabc123def456abc123def456abc123def456abc123def456abcd",
Names: ["/my-container"],
});
const { provider, logger } = createProvider({ containers: [container] });
const result = await provider.handle(makeMonitor({ url: "my-container" }));
expect(result.status).toBe(false);
expect(result.code).toBe(NETWORK_ERROR);
expect(result.message).toContain("Ambiguous");
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("Ambiguous"),
})
);
});
it("reports 'exact ID' in ambiguity message when exact ID and name both match", async () => {
const fullId = "abc123def456abc123def456abc123def456abc123def456abc123def456abcd";
const container = makeContainer({
Id: fullId,
Names: ["/" + fullId],
});
const { provider } = createProvider({ containers: [container] });
const result = await provider.handle(makeMonitor({ url: fullId }));
expect(result.message).toContain("Using exact ID");
});
it("reports 'exact name' in ambiguity message when name and partial ID match", async () => {
const container = makeContainer({
Id: "my-containerabc123def456abc123def456abc123def456abc123def456abcd",
Names: ["/my-container"],
});
const { provider } = createProvider({ containers: [container] });
const result = await provider.handle(makeMonitor({ url: "my-container" }));
expect(result.message).toContain("Using exact name");
});
});
// ── Container inspection ─────────────────────────────────────────────
describe("container inspection", () => {
it("returns running status for running container", async () => {
const { provider } = createProvider({ inspectResult: { State: { Status: "running" } } });
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(true);
expect(result.message).toBe("Docker container status fetched successfully");
});
it("returns not-running status for stopped container", async () => {
const { provider } = createProvider({ inspectResult: { State: { Status: "exited" } } });
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(false);
});
it("returns false status when inspect response has no State", async () => {
const { provider } = createProvider({ inspectResult: {} });
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(false);
});
it("returns false status when inspect response has State but no Status", async () => {
const { provider } = createProvider({ inspectResult: { State: {} } });
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(false);
});
it("handles inspect error with Docker-specific error", async () => {
const { provider } = createProvider();
// Make inspect throw a Docker error
const dockerErr = Object.assign(new Error("container not found"), {
statusCode: 404,
reason: "no such container",
json: { message: "Container not found" },
});
provider["docker"] = {
listContainers: jest.fn().mockResolvedValue([makeContainer()]),
getContainer: jest.fn().mockReturnValue({
inspect: jest.fn().mockRejectedValue(dockerErr),
}),
} as any;
// Need to re-assign; simpler to use the createProvider mock
const { provider: p2 } = createProvider();
// Override getContainer to return failing inspect
p2["docker"].getContainer = jest.fn().mockReturnValue({
inspect: jest.fn().mockRejectedValue(dockerErr),
});
const result = await p2.handle(makeMonitor());
expect(result.status).toBe(false);
expect(result.code).toBe(404);
expect(result.message).toBe("Container not found");
});
it("handles inspect error with Docker error using reason fallback", async () => {
const { provider } = createProvider();
const dockerErr = Object.assign(new Error("error"), {
statusCode: 500,
reason: "internal error",
json: {},
});
provider["docker"].getContainer = jest.fn().mockReturnValue({
inspect: jest.fn().mockRejectedValue(dockerErr),
});
const result = await provider.handle(makeMonitor());
expect(result.message).toBe("internal error");
});
it("handles inspect error with Docker error using Error message fallback", async () => {
const { provider } = createProvider();
const dockerErr = Object.assign(new Error("base error"), {
statusCode: 503,
reason: undefined,
json: undefined,
});
provider["docker"].getContainer = jest.fn().mockReturnValue({
inspect: jest.fn().mockRejectedValue(dockerErr),
});
const result = await provider.handle(makeMonitor());
expect(result.message).toBe("base error");
});
it("handles inspect error with NETWORK_ERROR when no statusCode", async () => {
const { provider } = createProvider();
const dockerErr = Object.assign(new Error("unknown"), {
statusCode: undefined,
});
provider["docker"].getContainer = jest.fn().mockReturnValue({
inspect: jest.fn().mockRejectedValue(dockerErr),
});
const result = await provider.handle(makeMonitor());
expect(result.code).toBe(NETWORK_ERROR);
});
it("handles inspect error with standard Error (not Docker error)", async () => {
const { provider } = createProvider();
provider["docker"].getContainer = jest.fn().mockReturnValue({
inspect: jest.fn().mockRejectedValue(new Error("ECONNREFUSED")),
});
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(false);
expect(result.message).toBe("ECONNREFUSED");
});
it("handles inspect error with non-Error non-Docker thrown value", async () => {
const { provider } = createProvider();
provider["docker"].getContainer = jest.fn().mockReturnValue({
inspect: jest.fn().mockRejectedValue("string error"),
});
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(false);
expect(result.message).toBe("Failed to fetch Docker container information");
});
});
// ── Outer catch ──────────────────────────────────────────────────────
describe("outer error handling", () => {
it("throws AppError when containerInput is missing", async () => {
const { provider } = createProvider();
await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("Container name or ID is required for Docker monitor");
});
it("throws AppError when listContainers throws", async () => {
const { provider } = createProvider();
provider["docker"].listContainers = jest.fn().mockRejectedValue(new Error("Docker daemon unavailable"));
await expect(provider.handle(makeMonitor())).rejects.toThrow("Docker daemon unavailable");
});
it("throws AppError with default message for non-Error thrown values", async () => {
const { provider } = createProvider();
provider["docker"].listContainers = jest.fn().mockRejectedValue(null);
await expect(provider.handle(makeMonitor())).rejects.toThrow("Error performing Docker request");
});
});
});
@@ -0,0 +1,191 @@
import { describe, expect, it, jest } from "@jest/globals";
import { GameProvider } from "../../../src/service/infrastructure/network/GameProvider.ts";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import { createMockLogger } from "../../helpers/createMockLogger.ts";
import { NETWORK_ERROR } from "../../../src/service/infrastructure/network/utils.ts";
import type { Monitor } from "../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
({
id: "mon-1",
teamId: "team-1",
type: "game",
url: "play.example.com",
port: 25565,
gameId: "minecraft",
...overrides,
}) as Monitor;
const createMockGameDig = (state?: Record<string, any> | null) => ({
query: jest.fn().mockResolvedValue(
state ?? {
name: "My Server",
map: "world",
players: [],
ping: 45,
}
),
});
const createProvider = (gameDig?: any) => {
const logger = createMockLogger();
const dig = gameDig ?? createMockGameDig();
const provider = new GameProvider(logger as any, dig as any);
return { provider, logger, gameDig: dig };
};
// ── Contract ─────────────────────────────────────────────────────────────────
testStatusProviderContract("GameProvider", {
create: () => createProvider().provider,
supportedType: "game",
unsupportedType: "http",
makeMonitor: () => makeMonitor(),
});
// ── Tests ────────────────────────────────────────────────────────────────────
describe("GameProvider", () => {
it("returns success with server state", async () => {
const { provider } = createProvider();
const result = await provider.handle(makeMonitor());
expect(result).toEqual(
expect.objectContaining({
monitorId: "mon-1",
teamId: "team-1",
type: "game",
status: true,
code: 200,
message: "Success",
responseTime: 45,
})
);
});
it("queries with correct host, port, and gameId", async () => {
const { provider, gameDig } = createProvider();
await provider.handle(makeMonitor({ url: "https://play.example.com/path", port: 27015, gameId: "csgo" }));
expect(gameDig.query).toHaveBeenCalledWith({
type: "csgo",
host: "play.example.com",
port: 27015,
});
});
it("strips protocol and path from url for host", async () => {
const { provider, gameDig } = createProvider();
await provider.handle(makeMonitor({ url: "http://game.server.com:8080/info" }));
expect(gameDig.query).toHaveBeenCalledWith(expect.objectContaining({ host: "game.server.com" }));
});
it("defaults gameId to 'unknown' when not set", async () => {
const { provider, gameDig } = createProvider();
await provider.handle(makeMonitor({ gameId: undefined }));
expect(gameDig.query).toHaveBeenCalledWith(expect.objectContaining({ type: "unknown" }));
});
it("defaults port to 0 when not set", async () => {
const { provider, gameDig } = createProvider();
await provider.handle(makeMonitor({ port: undefined }));
expect(gameDig.query).toHaveBeenCalledWith(expect.objectContaining({ port: 0 }));
});
it("defaults responseTime to 0 when ping is undefined", async () => {
const { provider } = createProvider(createMockGameDig({ ping: undefined }));
const result = await provider.handle(makeMonitor());
expect(result.responseTime).toBe(0);
});
it("returns failure when query resolves to undefined (caught error)", async () => {
const gameDig = { query: jest.fn().mockRejectedValue(new Error("Server offline")) };
// GameProvider catches the error internally via .catch()
const { provider, logger } = createProvider(gameDig);
const result = await provider.handle(makeMonitor());
expect(result).toEqual(
expect.objectContaining({
status: false,
code: NETWORK_ERROR,
message: "No response from game server",
responseTime: 0,
})
);
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
message: "Server offline",
method: "handle",
})
);
});
it("logs non-Error thrown values from query", async () => {
const gameDig = { query: jest.fn().mockRejectedValue("string error") };
const { provider, logger } = createProvider(gameDig);
await provider.handle(makeMonitor());
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "string error" }));
});
it("uses empty string as host when url is undefined", async () => {
const { provider, gameDig } = createProvider();
await provider.handle(makeMonitor({ url: undefined }));
expect(gameDig.query).toHaveBeenCalledWith(expect.objectContaining({ host: "" }));
});
it("throws AppError with default message when error stringifies to empty", async () => {
const gameDig = {
query: jest.fn().mockReturnValue({
catch: () => {
throw new Error("");
},
}),
};
const { provider } = createProvider(gameDig);
await expect(provider.handle(makeMonitor())).rejects.toThrow("Error performing game server check");
});
it("throws AppError with default message for non-Error that stringifies to empty", async () => {
const gameDig = {
query: jest.fn().mockReturnValue({
catch: () => {
throw "";
},
}),
};
const { provider } = createProvider(gameDig);
await expect(provider.handle(makeMonitor())).rejects.toThrow("Error performing game server check");
});
it("throws AppError with Error message in outer catch", async () => {
const gameDig = {
query: jest.fn().mockReturnValue({
catch: () => {
throw new Error("unexpected");
},
}),
};
const { provider } = createProvider(gameDig);
await expect(provider.handle(makeMonitor())).rejects.toThrow("unexpected");
});
});
@@ -0,0 +1,378 @@
import { describe, expect, it, jest } from "@jest/globals";
import { GrpcProvider } from "../../../src/service/infrastructure/network/GrpcProvider.ts";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import type { Monitor } from "../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
({
id: "mon-1",
teamId: "team-1",
type: "grpc",
url: "grpc.example.com",
port: 50051,
grpcServiceName: "my.Service",
ignoreTlsErrors: false,
...overrides,
}) as Monitor;
const createMockClient = (behavior: "serving" | "not-serving" | "error" | "grpc-error" = "serving") => {
const close = jest.fn();
const Check = jest.fn((_req: any, _opts: any, cb: Function) => {
if (behavior === "serving") {
cb(null, { status: "SERVING" });
} else if (behavior === "not-serving") {
cb(null, { status: "NOT_SERVING" });
} else if (behavior === "error") {
cb({ code: 14, details: "Connection refused", message: "unavailable" });
} else if (behavior === "grpc-error") {
cb({ code: 2, message: "Unknown error" });
}
});
return { Check, close };
};
const createMockGrpc = (client?: ReturnType<typeof createMockClient>) => {
const c = client ?? createMockClient();
const Health = jest.fn().mockReturnValue(c);
return {
grpc: {
loadPackageDefinition: jest.fn().mockReturnValue({
grpc: { health: { v1: { Health } } },
}),
credentials: {
createInsecure: jest.fn().mockReturnValue("insecure-creds"),
createSsl: jest.fn().mockReturnValue("ssl-creds"),
},
},
protoLoader: {
loadSync: jest.fn().mockReturnValue({}),
},
Health,
client: c,
};
};
const createProvider = (opts?: { behavior?: "serving" | "not-serving" | "error" | "grpc-error" }) => {
const client = createMockClient(opts?.behavior ?? "serving");
const mocks = createMockGrpc(client);
const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any);
return { provider, ...mocks, client };
};
// ── Contract ─────────────────────────────────────────────────────────────────
testStatusProviderContract("GrpcProvider", {
create: () => createProvider().provider,
supportedType: "grpc",
unsupportedType: "http",
makeMonitor: () => makeMonitor(),
});
// ── Tests ────────────────────────────────────────────────────────────────────
describe("GrpcProvider", () => {
// ── Success paths ────────────────────────────────────────────────────
describe("success responses", () => {
it("returns healthy status for SERVING response", async () => {
const { provider } = createProvider({ behavior: "serving" });
const result = await provider.handle(makeMonitor());
expect(result).toEqual(
expect.objectContaining({
monitorId: "mon-1",
teamId: "team-1",
type: "grpc",
status: true,
code: 200,
message: expect.stringContaining("SERVING"),
})
);
expect(result.payload).toEqual(
expect.objectContaining({
grpcStatusCode: 0,
grpcStatusName: "OK",
serviceName: "my.Service",
servingStatus: "SERVING",
})
);
});
it("returns unhealthy status for NOT_SERVING response", async () => {
const { provider } = createProvider({ behavior: "not-serving" });
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(false);
expect(result.code).toBe(5000);
expect(result.message).toContain("NOT_SERVING");
});
it("defaults to UNKNOWN when response has no status", async () => {
const client = { Check: jest.fn((_r: any, _o: any, cb: Function) => cb(null, {})), close: jest.fn() };
const mocks = createMockGrpc(client as any);
const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any);
const result = await provider.handle(makeMonitor());
expect(result.payload).toEqual(expect.objectContaining({ servingStatus: "UNKNOWN" }));
});
it("defaults to UNKNOWN when response is undefined", async () => {
const client = { Check: jest.fn((_r: any, _o: any, cb: Function) => cb(null, undefined)), close: jest.fn() };
const mocks = createMockGrpc(client as any);
const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any);
const result = await provider.handle(makeMonitor());
expect(result.payload).toEqual(expect.objectContaining({ servingStatus: "UNKNOWN" }));
});
});
// ── gRPC errors ──────────────────────────────────────────────────────
describe("gRPC errors", () => {
it("returns failure with gRPC error details", async () => {
const { provider } = createProvider({ behavior: "error" });
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(false);
expect(result.code).toBe(14);
expect(result.message).toBe("Connection refused");
expect(result.payload).toEqual(
expect.objectContaining({
grpcStatusCode: 14,
grpcStatusName: "UNAVAILABLE",
servingStatus: "UNKNOWN",
})
);
});
it("falls back to message when details is missing", async () => {
const client = {
Check: jest.fn((_r: any, _o: any, cb: Function) => cb({ code: 2, message: "Unknown error" })),
close: jest.fn(),
};
const mocks = createMockGrpc(client as any);
const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any);
const result = await provider.handle(makeMonitor());
expect(result.message).toBe("Unknown error");
});
it("uses default message and code when error has no code/details/message", async () => {
const client = {
Check: jest.fn((_r: any, _o: any, cb: Function) => cb({})),
close: jest.fn(),
};
const mocks = createMockGrpc(client as any);
const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any);
const result = await provider.handle(makeMonitor());
expect(result.code).toBe(5000);
expect(result.message).toBe("gRPC error");
expect(result.payload).toEqual(expect.objectContaining({ grpcStatusCode: -1, grpcStatusName: "UNKNOWN" }));
});
it("defaults grpcCode to 5000 when error has no grpcCode", async () => {
const client = {
Check: jest.fn((_r: any, _o: any, cb: Function) => {
const err = new Error("test") as any;
// no grpcPayload or grpcCode
cb(err);
}),
close: jest.fn(),
};
const mocks = createMockGrpc(client as any);
const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any);
const result = await provider.handle(makeMonitor());
expect(result.code).toBe(5000);
});
it("includes grpcPayload in error result", async () => {
const { provider } = createProvider({ behavior: "error" });
const result = await provider.handle(makeMonitor());
expect(result.payload).toEqual(
expect.objectContaining({
grpcStatusCode: 14,
grpcStatusName: "UNAVAILABLE",
})
);
});
it("defaults code, message, and payload when error lacks gRPC properties", async () => {
// client.Check throws synchronously — timeRequest catches a plain object with no gRPC props
const client = {
Check: jest.fn(() => {
throw { unexpected: true };
}),
close: jest.fn(),
};
const mocks = createMockGrpc(client as any);
const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any);
const result = await provider.handle(makeMonitor());
expect(result.code).toBe(5000);
expect(result.message).toBe("gRPC health check failed");
expect(result.payload).toBeNull();
});
});
// ── Credentials ──────────────────────────────────────────────────────
describe("credentials", () => {
it("uses insecure credentials by default", async () => {
const { provider, grpc } = createProvider();
await provider.handle(makeMonitor({ ignoreTlsErrors: false }));
expect(grpc.credentials.createInsecure).toHaveBeenCalled();
});
it("uses SSL credentials with checkServerIdentity override when ignoreTlsErrors is true", async () => {
const { provider, grpc } = createProvider();
await provider.handle(makeMonitor({ ignoreTlsErrors: true }));
expect(grpc.credentials.createSsl).toHaveBeenCalledWith(null, null, null, {
checkServerIdentity: expect.any(Function),
});
// Verify checkServerIdentity returns undefined
const opts = (grpc.credentials.createSsl as jest.Mock).mock.calls[0][3];
expect(opts.checkServerIdentity()).toBeUndefined();
});
});
// ── URL parsing ──────────────────────────────────────────────────────
describe("URL and target construction", () => {
it("strips protocol from URL", async () => {
const { provider, Health } = createProvider();
await provider.handle(makeMonitor({ url: "https://grpc.example.com", port: 443 }));
expect(Health).toHaveBeenCalledWith("grpc.example.com:443", expect.anything());
});
it("uses grpcServiceName in Check request", async () => {
const { provider, client } = createProvider();
await provider.handle(makeMonitor({ grpcServiceName: "health.v1" }));
expect(client.Check).toHaveBeenCalledWith(
{ service: "health.v1" },
expect.objectContaining({ deadline: expect.any(Date) }),
expect.any(Function)
);
});
it("defaults grpcServiceName to empty string when not set", async () => {
const { provider, client } = createProvider();
await provider.handle(makeMonitor({ grpcServiceName: undefined }));
expect(client.Check).toHaveBeenCalledWith({ service: "" }, expect.anything(), expect.any(Function));
});
});
// ── Validation ───────────────────────────────────────────────────────
describe("validation", () => {
it("throws AppError when url is missing", async () => {
const { provider } = createProvider();
await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("Monitor host is required");
});
it("throws AppError when port is missing", async () => {
const { provider } = createProvider();
await expect(provider.handle(makeMonitor({ port: undefined }))).rejects.toThrow("Monitor port is required");
});
});
// ── Outer catch ──────────────────────────────────────────────────────
describe("outer error handling", () => {
it("throws AppError when protoLoader.loadSync throws", async () => {
const mocks = createMockGrpc();
mocks.protoLoader.loadSync = jest.fn().mockImplementation(() => {
throw new Error("Proto file not found");
});
const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any);
await expect(provider.handle(makeMonitor())).rejects.toThrow("Proto file not found");
});
it("throws AppError with default message when Error has empty message", async () => {
const mocks = createMockGrpc();
mocks.protoLoader.loadSync = jest.fn().mockImplementation(() => {
throw new Error("");
});
const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any);
await expect(provider.handle(makeMonitor())).rejects.toThrow("Error performing gRPC health check");
});
it("throws AppError with stringified message for non-Error thrown values", async () => {
const mocks = createMockGrpc();
mocks.protoLoader.loadSync = jest.fn().mockImplementation(() => {
throw "proto load failed";
});
const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any);
await expect(provider.handle(makeMonitor())).rejects.toThrow("proto load failed");
});
});
// ── getGrpcStatusName ────────────────────────────────────────────────
describe("gRPC status name mapping", () => {
const statusCodes: [number, string][] = [
[0, "OK"],
[1, "CANCELLED"],
[2, "UNKNOWN"],
[3, "INVALID_ARGUMENT"],
[4, "DEADLINE_EXCEEDED"],
[5, "NOT_FOUND"],
[6, "ALREADY_EXISTS"],
[7, "PERMISSION_DENIED"],
[8, "RESOURCE_EXHAUSTED"],
[9, "FAILED_PRECONDITION"],
[10, "ABORTED"],
[11, "OUT_OF_RANGE"],
[12, "UNIMPLEMENTED"],
[13, "INTERNAL"],
[14, "UNAVAILABLE"],
[15, "DATA_LOSS"],
[16, "UNAUTHENTICATED"],
[99, "UNKNOWN"],
];
it.each(statusCodes)("maps gRPC code %i to %s", async (code, expectedName) => {
const client = {
Check: jest.fn((_r: any, _o: any, cb: Function) => cb({ code, details: "test" })),
close: jest.fn(),
};
const mocks = createMockGrpc(client as any);
const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any);
const result = await provider.handle(makeMonitor());
expect(result.payload).toEqual(expect.objectContaining({ grpcStatusName: expectedName }));
});
});
});
@@ -0,0 +1,91 @@
import { describe, expect, it, jest } from "@jest/globals";
import { HardwareProvider } from "../../../src/service/infrastructure/network/HardwareProvider.ts";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import type { HttpProvider } from "../../../src/service/infrastructure/network/HttpProvider.ts";
import type { Monitor } from "../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
({
id: "mon-1",
teamId: "team-1",
type: "hardware",
url: "https://capture.example.com",
...overrides,
}) as Monitor;
const createMockHttpProvider = () =>
({
handle: jest.fn().mockResolvedValue({
monitorId: "mon-1",
teamId: "team-1",
type: "hardware",
status: true,
code: 200,
message: "OK",
responseTime: 50,
payload: { data: { cpu: { usage_percent: 0.5 } } },
}),
}) as unknown as jest.Mocked<HttpProvider>;
const createProvider = () => {
const httpProvider = createMockHttpProvider();
const provider = new HardwareProvider(httpProvider);
return { provider, httpProvider };
};
// ── Contract ─────────────────────────────────────────────────────────────────
testStatusProviderContract("HardwareProvider", {
create: () => createProvider().provider,
supportedType: "hardware",
unsupportedType: "http",
makeMonitor: () => makeMonitor(),
});
// ── Tests ────────────────────────────────────────────────────────────────────
describe("HardwareProvider", () => {
it("delegates to httpProvider.handle", async () => {
const { provider, httpProvider } = createProvider();
const monitor = makeMonitor();
await provider.handle(monitor);
expect(httpProvider.handle).toHaveBeenCalledWith(monitor);
});
it("returns the response from httpProvider", async () => {
const { provider } = createProvider();
const result = await provider.handle(makeMonitor());
expect(result).toEqual(
expect.objectContaining({
monitorId: "mon-1",
status: true,
code: 200,
})
);
});
it("throws AppError when url is missing", async () => {
const { provider } = createProvider();
await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("URL is required for Hardware monitor");
});
it("throws AppError when httpProvider.handle throws", async () => {
const { provider, httpProvider } = createProvider();
(httpProvider.handle as jest.Mock).mockRejectedValue(new Error("connection failed"));
await expect(provider.handle(makeMonitor())).rejects.toThrow("connection failed");
});
it("throws AppError with default message for non-Error thrown values", async () => {
const { provider, httpProvider } = createProvider();
(httpProvider.handle as jest.Mock).mockRejectedValue("string error");
await expect(provider.handle(makeMonitor())).rejects.toThrow("Error performing Hardware request");
});
});
@@ -0,0 +1,348 @@
import { describe, expect, it, jest } from "@jest/globals";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import { NETWORK_ERROR } from "../../../src/service/infrastructure/network/utils.ts";
import type { Monitor } from "../../../src/types/index.ts";
// ── Mocks ────────────────────────────────────────────────────────────────────
jest.unstable_mockModule("cacheable-lookup", () => ({
default: jest.fn().mockImplementation(() => ({})),
}));
const mockGot = jest.fn();
// got instance returned by got.extend()
mockGot.mockImplementation(() => Promise.resolve());
(mockGot as any).extend = jest.fn().mockReturnValue(mockGot);
jest.unstable_mockModule("got", () => ({
type: { Got: {} },
HTTPError: class HTTPError extends Error {
response: any;
timings: any;
constructor(msg: string, response?: any, timings?: any) {
super(msg);
this.name = "HTTPError";
this.response = response;
this.timings = timings;
}
},
RequestError: class RequestError extends Error {
response: any;
timings: any;
constructor(msg: string, response?: any, timings?: any) {
super(msg);
this.name = "RequestError";
this.response = response;
this.timings = timings;
}
},
}));
const { HttpProvider } = await import("../../../src/service/infrastructure/network/HttpProvider.ts");
const gotModule = await import("got");
const { HTTPError, RequestError } = gotModule;
// ── Helpers ──────────────────────────────────────────────────────────────────
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
({
id: "mon-1",
teamId: "team-1",
type: "http",
url: "https://example.com",
secret: undefined,
jsonPath: undefined,
ignoreTlsErrors: false,
useAdvancedMatching: false,
matchMethod: undefined,
expectedValue: undefined,
...overrides,
}) as Monitor;
const createMockMatcher = (result?: { ok: boolean; message: string; extracted?: unknown }) => ({
validate: jest.fn().mockReturnValue(result ?? { ok: true, message: "Success" }),
});
const makeGotResponse = (overrides?: Record<string, any>) => ({
ok: true,
statusCode: 200,
statusMessage: "OK",
headers: { "content-type": "text/html" },
body: "<html></html>",
timings: { phases: { total: 100 } },
...overrides,
});
const createProvider = (matcher?: any) => {
const advancedMatcher = matcher ?? createMockMatcher();
const provider = new HttpProvider(mockGot as any, advancedMatcher);
return { provider, advancedMatcher };
};
// ── Contract ─────────────────────────────────────────────────────────────────
testStatusProviderContract("HttpProvider", {
create: () => {
mockGot.mockResolvedValue(makeGotResponse());
return createProvider().provider;
},
supportedType: "http",
unsupportedType: "ping",
makeMonitor: () => makeMonitor(),
});
// ── Tests ────────────────────────────────────────────────────────────────────
describe("HttpProvider", () => {
// ── Success paths ────────────────────────────────────────────────────
describe("success responses", () => {
it("returns success for a standard HTML response", async () => {
mockGot.mockResolvedValue(makeGotResponse());
const { provider } = createProvider();
const result = await provider.handle(makeMonitor());
expect(result).toEqual(
expect.objectContaining({
monitorId: "mon-1",
teamId: "team-1",
type: "http",
status: true,
code: 200,
message: "OK",
responseTime: 100,
})
);
});
it("parses JSON body when content-type is application/json", async () => {
mockGot.mockResolvedValue(
makeGotResponse({
headers: { "content-type": "application/json" },
body: '{"status":"ok"}',
})
);
const { provider } = createProvider();
const result = await provider.handle(makeMonitor());
expect(result.payload).toEqual({ status: "ok" });
});
it("returns raw body when JSON parsing fails", async () => {
mockGot.mockResolvedValue(
makeGotResponse({
headers: { "content-type": "application/json" },
body: "not-json",
})
);
const { provider } = createProvider();
const result = await provider.handle(makeMonitor());
expect(result.payload).toBe("not-json");
});
it("returns raw body for non-JSON content", async () => {
mockGot.mockResolvedValue(makeGotResponse({ body: "<html>test</html>" }));
const { provider } = createProvider();
const result = await provider.handle(makeMonitor());
expect(result.payload).toBe("<html>test</html>");
});
it("passes Authorization header when secret is set", async () => {
mockGot.mockResolvedValue(makeGotResponse());
const { provider } = createProvider();
await provider.handle(makeMonitor({ secret: "my-token" }));
expect(mockGot).toHaveBeenCalledWith(
"https://example.com",
expect.objectContaining({
headers: { Authorization: "Bearer my-token" },
})
);
});
it("passes undefined headers when no secret", async () => {
mockGot.mockResolvedValue(makeGotResponse());
const { provider } = createProvider();
await provider.handle(makeMonitor({ secret: undefined }));
expect(mockGot).toHaveBeenCalledWith(
"https://example.com",
expect.objectContaining({
headers: undefined,
})
);
});
it("defaults responseTime to 0 when timings.phases.total is undefined", async () => {
mockGot.mockResolvedValue(makeGotResponse({ timings: { phases: { total: undefined } } }));
const { provider } = createProvider();
const result = await provider.handle(makeMonitor());
expect(result.responseTime).toBe(0);
});
it("defaults statusMessage to 'OK' when undefined", async () => {
mockGot.mockResolvedValue(makeGotResponse({ statusMessage: undefined }));
const { provider } = createProvider();
const result = await provider.handle(makeMonitor());
expect(result.message).toBe("OK");
});
it("uses empty string for content-type when header is missing", async () => {
mockGot.mockResolvedValue(makeGotResponse({ headers: {} }));
const { provider } = createProvider();
const result = await provider.handle(makeMonitor());
// Should treat as non-JSON
expect(result.payload).toBe("<html></html>");
});
});
// ── jsonPath + non-JSON response ─────────────────────────────────────
describe("jsonPath validation", () => {
it("returns failure when jsonPath is set but response is not JSON", async () => {
mockGot.mockResolvedValue(makeGotResponse({ headers: { "content-type": "text/html" } }));
const { provider } = createProvider();
const result = await provider.handle(makeMonitor({ jsonPath: "status" }));
expect(result.status).toBe(false);
expect(result.message).toBe("Response is not JSON");
});
it("defaults responseTime to 0 in non-JSON jsonPath response when total is undefined", async () => {
mockGot.mockResolvedValue(
makeGotResponse({
headers: { "content-type": "text/html" },
timings: { phases: { total: undefined } },
})
);
const { provider } = createProvider();
const result = await provider.handle(makeMonitor({ jsonPath: "status" }));
expect(result.responseTime).toBe(0);
});
});
// ── AdvancedMatcher integration ──────────────────────────────────────
describe("advanced matching", () => {
it("uses matcher result for status and message", async () => {
mockGot.mockResolvedValue(makeGotResponse());
const matcher = createMockMatcher({ ok: false, message: "Mismatch", extracted: "value" });
const { provider } = createProvider(matcher);
const result = await provider.handle(makeMonitor({ useAdvancedMatching: true }));
expect(result.status).toBe(false);
expect(result.message).toBe("Mismatch");
expect(result.extracted).toBe("value");
});
it("sets status to false when response.ok is false even if matcher passes", async () => {
mockGot.mockResolvedValue(makeGotResponse({ ok: false, statusCode: 301 }));
const matcher = createMockMatcher({ ok: true, message: "Success" });
const { provider } = createProvider(matcher);
const result = await provider.handle(makeMonitor());
// status = response.ok && matchResult.ok
expect(result.status).toBe(false);
});
});
// ── Error handling ───────────────────────────────────────────────────
describe("error handling", () => {
it("handles HTTPError with response and timings", async () => {
const err = new HTTPError("Not Found");
(err as any).response = { statusCode: 404 };
(err as any).timings = { phases: { total: 50 } };
mockGot.mockRejectedValue(err);
const { provider } = createProvider();
const result = await provider.handle(makeMonitor());
expect(result).toEqual(
expect.objectContaining({
status: false,
code: 404,
message: "Not Found",
responseTime: 50,
})
);
});
it("handles RequestError with response and timings", async () => {
const err = new RequestError("ECONNREFUSED");
(err as any).response = { statusCode: undefined };
(err as any).timings = { phases: { total: undefined } };
mockGot.mockRejectedValue(err);
const { provider } = createProvider();
const result = await provider.handle(makeMonitor());
expect(result.code).toBe(NETWORK_ERROR);
expect(result.responseTime).toBe(0);
});
it("handles HTTPError without response (defaults to NETWORK_ERROR)", async () => {
const err = new HTTPError("Timeout");
(err as any).response = undefined;
(err as any).timings = undefined;
mockGot.mockRejectedValue(err);
const { provider } = createProvider();
const result = await provider.handle(makeMonitor());
expect(result.code).toBe(NETWORK_ERROR);
expect(result.responseTime).toBe(0);
});
it("handles generic Error (non-HTTPError/RequestError)", async () => {
mockGot.mockRejectedValue(new Error("DNS lookup failed"));
const { provider } = createProvider();
const result = await provider.handle(makeMonitor());
expect(result).toEqual(
expect.objectContaining({
status: false,
code: NETWORK_ERROR,
message: "DNS lookup failed",
responseTime: 0,
})
);
});
it("handles non-Error thrown values", async () => {
mockGot.mockRejectedValue("string error");
const { provider } = createProvider();
const result = await provider.handle(makeMonitor());
expect(result.message).toBe("string error");
expect(result.code).toBe(NETWORK_ERROR);
});
it("throws when url is missing", async () => {
const { provider } = createProvider();
await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("URL is required for HTTP monitor");
});
});
});
@@ -0,0 +1,165 @@
import { describe, expect, it, jest } from "@jest/globals";
import { PageSpeedProvider } from "../../../src/service/infrastructure/network/PageSpeedProvider.ts";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import { createMockLogger } from "../../helpers/createMockLogger.ts";
import type { HttpProvider } from "../../../src/service/infrastructure/network/HttpProvider.ts";
import type { ISettingsService } from "../../../src/service/system/settingsService.ts";
import type { Monitor } from "../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
({
id: "mon-1",
teamId: "team-1",
type: "pagespeed",
url: "https://example.com",
...overrides,
}) as Monitor;
const createMockHttpProvider = () =>
({
handle: jest.fn().mockResolvedValue({
monitorId: "mon-1",
teamId: "team-1",
type: "pagespeed",
status: true,
code: 200,
message: "OK",
responseTime: 2000,
payload: { lighthouseResult: {} },
}),
}) as unknown as jest.Mocked<HttpProvider>;
const createMockSettingsService = (apiKey?: string) =>
({
getDBSettings: jest.fn().mockResolvedValue({ pagespeedApiKey: apiKey }),
}) as unknown as jest.Mocked<ISettingsService>;
const createProvider = (opts?: { apiKey?: string }) => {
const logger = createMockLogger();
const httpProvider = createMockHttpProvider();
const settingsService = createMockSettingsService(opts?.apiKey);
const provider = new PageSpeedProvider(httpProvider, settingsService, logger as any);
return { provider, httpProvider, settingsService, logger };
};
// ── Contract ─────────────────────────────────────────────────────────────────
testStatusProviderContract("PageSpeedProvider", {
create: () => createProvider({ apiKey: "test-key" }).provider,
supportedType: "pagespeed",
unsupportedType: "http",
makeMonitor: () => makeMonitor(),
});
// ── Tests ────────────────────────────────────────────────────────────────────
describe("PageSpeedProvider", () => {
it("delegates to httpProvider with PageSpeed API URL", async () => {
const { provider, httpProvider } = createProvider({ apiKey: "my-key" });
await provider.handle(makeMonitor({ url: "https://example.com" }));
expect(httpProvider.handle).toHaveBeenCalledWith(
expect.objectContaining({
url: expect.stringContaining("pagespeedonline.googleapis.com"),
})
);
});
it("includes API key in URL when available", async () => {
const { provider, httpProvider } = createProvider({ apiKey: "my-key" });
await provider.handle(makeMonitor());
const calledUrl = (httpProvider.handle as jest.Mock).mock.calls[0][0].url;
expect(calledUrl).toContain("key=my-key");
});
it("omits API key and logs warning when not configured", async () => {
const { provider, httpProvider, logger } = createProvider({ apiKey: undefined });
await provider.handle(makeMonitor());
const calledUrl = (httpProvider.handle as jest.Mock).mock.calls[0][0].url;
expect(calledUrl).not.toContain("key=");
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("API key not found"),
service: "PageSpeedProvider",
})
);
});
it("encodes the monitor URL in the PageSpeed API URL", async () => {
const { provider, httpProvider } = createProvider({ apiKey: "key" });
await provider.handle(makeMonitor({ url: "https://example.com/path?q=1" }));
const calledUrl = (httpProvider.handle as jest.Mock).mock.calls[0][0].url;
expect(calledUrl).toContain(encodeURIComponent("https://example.com/path?q=1"));
});
it("includes all PageSpeed categories in the URL", async () => {
const { provider, httpProvider } = createProvider({ apiKey: "key" });
await provider.handle(makeMonitor());
const calledUrl = (httpProvider.handle as jest.Mock).mock.calls[0][0].url;
expect(calledUrl).toContain("category=seo");
expect(calledUrl).toContain("category=accessibility");
expect(calledUrl).toContain("category=best-practices");
expect(calledUrl).toContain("category=performance");
});
it("passes all monitor properties through to httpProvider", async () => {
const { provider, httpProvider } = createProvider({ apiKey: "key" });
const monitor = makeMonitor({ id: "mon-5", teamId: "team-9" });
await provider.handle(monitor);
expect(httpProvider.handle).toHaveBeenCalledWith(expect.objectContaining({ id: "mon-5", teamId: "team-9" }));
});
it("throws AppError when url is missing", async () => {
const { provider } = createProvider();
await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("URL is required for PageSpeed monitor");
});
it("throws AppError when httpProvider.handle throws", async () => {
const { provider, httpProvider } = createProvider({ apiKey: "key" });
(httpProvider.handle as jest.Mock).mockRejectedValue(new Error("timeout"));
await expect(provider.handle(makeMonitor())).rejects.toThrow("timeout");
});
it("throws AppError with default message for non-Error thrown values", async () => {
const { provider, httpProvider } = createProvider({ apiKey: "key" });
(httpProvider.handle as jest.Mock).mockRejectedValue("string error");
await expect(provider.handle(makeMonitor())).rejects.toThrow("Error performing PageSpeed request");
});
it("handles null apiKey from DB settings", async () => {
const { provider, logger } = createProvider({ apiKey: null as any });
await provider.handle(makeMonitor());
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("API key not found") }));
});
it("handles null dbSettings response", async () => {
const logger = createMockLogger();
const httpProvider = createMockHttpProvider();
const settingsService = {
getDBSettings: jest.fn().mockResolvedValue(null),
} as unknown as jest.Mocked<ISettingsService>;
const provider = new PageSpeedProvider(httpProvider, settingsService, logger as any);
await provider.handle(makeMonitor());
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("API key not found") }));
});
});
@@ -0,0 +1,139 @@
import { describe, expect, it, jest } from "@jest/globals";
import { PingProvider } from "../../../src/service/infrastructure/network/PingProvider.ts";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import type { Monitor } from "../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
({
id: "mon-1",
teamId: "team-1",
type: "ping",
url: "example.com",
...overrides,
}) as Monitor;
const createMockPing = (result?: Partial<{ alive: boolean; time: number | string }>) => ({
promise: {
probe: jest.fn().mockResolvedValue({
alive: true,
time: 25,
...result,
}),
},
});
// ── Contract ─────────────────────────────────────────────────────────────────
testStatusProviderContract("PingProvider", {
create: () => new PingProvider(createMockPing() as any),
supportedType: "ping",
unsupportedType: "http",
makeMonitor: () => makeMonitor(),
});
// ── Tests ────────────────────────────────────────────────────────────────────
describe("PingProvider", () => {
it("returns success when host is alive", async () => {
const provider = new PingProvider(createMockPing({ alive: true, time: 30 }) as any);
const result = await provider.handle(makeMonitor());
expect(result).toEqual(
expect.objectContaining({
monitorId: "mon-1",
teamId: "team-1",
type: "ping",
status: true,
code: 200,
message: "Success",
responseTime: 30,
})
);
});
it("returns failure when host is not alive", async () => {
const provider = new PingProvider(createMockPing({ alive: false, time: 0 }) as any);
const result = await provider.handle(makeMonitor());
expect(result).toEqual(
expect.objectContaining({
status: false,
code: 5000,
message: "Ping failed",
})
);
});
it("parses string time values", async () => {
const provider = new PingProvider(createMockPing({ alive: true, time: "42.5" }) as any);
const result = await provider.handle(makeMonitor());
expect(result.responseTime).toBe(42.5);
});
it("defaults responseTime to 0 for non-numeric time", async () => {
const provider = new PingProvider(createMockPing({ alive: true, time: "unknown" }) as any);
const result = await provider.handle(makeMonitor());
expect(result.responseTime).toBe(0);
});
it("defaults status to false when alive is undefined", async () => {
const provider = new PingProvider(createMockPing({ alive: undefined as any }) as any);
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(false);
});
it("sanitizes URL by stripping protocol and path", async () => {
const mockPing = createMockPing();
const provider = new PingProvider(mockPing as any);
await provider.handle(makeMonitor({ url: "https://example.com/path?query=1" }));
expect(mockPing.promise.probe).toHaveBeenCalledWith("example.com");
});
it("sanitizes URL by stripping port", async () => {
const mockPing = createMockPing();
const provider = new PingProvider(mockPing as any);
await provider.handle(makeMonitor({ url: "example.com:8080" }));
expect(mockPing.promise.probe).toHaveBeenCalledWith("example.com");
});
it("throws AppError when url is missing", async () => {
const provider = new PingProvider(createMockPing() as any);
await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("URL is required for ping monitor");
});
it("throws AppError when ping.probe rejects", async () => {
const mockPing = { promise: { probe: jest.fn().mockRejectedValue(new Error("Host unreachable")) } };
const provider = new PingProvider(mockPing as any);
await expect(provider.handle(makeMonitor())).rejects.toThrow("Host unreachable");
});
it("throws AppError when probe returns no response", async () => {
const mockPing = { promise: { probe: jest.fn().mockResolvedValue(null) } };
const provider = new PingProvider(mockPing as any);
await expect(provider.handle(makeMonitor())).rejects.toThrow("No response from ping");
});
it("throws AppError with stringified message for non-Error throws", async () => {
const mockPing = { promise: { probe: jest.fn().mockRejectedValue(42) } };
const provider = new PingProvider(mockPing as any);
await expect(provider.handle(makeMonitor())).rejects.toThrow("42");
});
});
@@ -0,0 +1,270 @@
import { describe, expect, it, jest } from "@jest/globals";
import { PortProvider } from "../../../src/service/infrastructure/network/PortProvider.ts";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import { NETWORK_ERROR } from "../../../src/service/infrastructure/network/utils.ts";
import type { Monitor } from "../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
({
id: "mon-1",
teamId: "team-1",
type: "port",
url: "example.com",
port: 443,
...overrides,
}) as Monitor;
const createMockNet = (behavior: "connect" | "timeout" | "error" = "connect") => {
const socket = {
setTimeout: jest.fn(),
on: jest.fn(),
end: jest.fn(),
destroy: jest.fn(),
};
const createConnection = jest.fn((opts: any, onConnect: () => void) => {
if (behavior === "connect") {
// Simulate immediate connection
process.nextTick(onConnect);
} else if (behavior === "timeout") {
process.nextTick(() => {
const timeoutCb = socket.on.mock.calls.find((c: any) => c[0] === "timeout");
// Simulate via setTimeout handler
const setTimeoutCb = socket.setTimeout.mock.calls[0];
if (setTimeoutCb) {
// The timeout event handler is registered via socket.on("timeout", ...)
}
});
} else if (behavior === "error") {
process.nextTick(() => {
const errorCb = socket.on.mock.calls.find((c: any) => c[0] === "error");
if (errorCb) {
errorCb[1](new Error("ECONNREFUSED"));
}
});
}
return socket;
});
return { createConnection, __socket: socket } as any;
};
const createSuccessNet = () => {
const socket = {
setTimeout: jest.fn(),
on: jest.fn(),
end: jest.fn(),
destroy: jest.fn(),
};
return {
createConnection: jest.fn((_opts: any, onConnect: () => void) => {
process.nextTick(onConnect);
return socket;
}),
} as any;
};
const createErrorNet = (error: Error = new Error("ECONNREFUSED")) => {
const socket = {
setTimeout: jest.fn(),
on: jest.fn((event: string, cb: Function) => {
if (event === "error") {
process.nextTick(() => cb(error));
}
}),
end: jest.fn(),
destroy: jest.fn(),
};
return {
createConnection: jest.fn(() => socket),
} as any;
};
const createTimeoutNet = () => {
const socket = {
setTimeout: jest.fn(),
on: jest.fn((event: string, cb: Function) => {
if (event === "timeout") {
process.nextTick(() => cb());
}
}),
end: jest.fn(),
destroy: jest.fn(),
};
return {
createConnection: jest.fn(() => socket),
} as any;
};
// ── Contract ─────────────────────────────────────────────────────────────────
testStatusProviderContract("PortProvider", {
create: () => new PortProvider(createSuccessNet()),
supportedType: "port",
unsupportedType: "http",
makeMonitor: () => makeMonitor(),
});
// ── Tests ────────────────────────────────────────────────────────────────────
describe("PortProvider", () => {
it("returns success when port connection succeeds", async () => {
const provider = new PortProvider(createSuccessNet());
const result = await provider.handle(makeMonitor());
expect(result).toEqual(
expect.objectContaining({
monitorId: "mon-1",
teamId: "team-1",
type: "port",
status: true,
code: 200,
message: "Port check successful",
payload: { success: true },
})
);
expect(result.responseTime).toBeGreaterThanOrEqual(0);
});
it("returns failure when connection errors", async () => {
const provider = new PortProvider(createErrorNet());
const result = await provider.handle(makeMonitor());
expect(result).toEqual(
expect.objectContaining({
status: false,
code: NETWORK_ERROR,
message: "ECONNREFUSED",
payload: { success: false },
})
);
});
it("returns failure with generic message for non-Error connection error", async () => {
const net = createErrorNet();
// Override the on handler to throw a non-Error
const socket = {
setTimeout: jest.fn(),
on: jest.fn((event: string, cb: Function) => {
if (event === "error") {
process.nextTick(() => cb("string error"));
}
}),
end: jest.fn(),
destroy: jest.fn(),
};
net.createConnection = jest.fn(() => socket);
const provider = new PortProvider(net);
const result = await provider.handle(makeMonitor());
expect(result.message).toBe("Port check failed");
});
it("returns failure when connection times out", async () => {
const provider = new PortProvider(createTimeoutNet());
const result = await provider.handle(makeMonitor());
expect(result).toEqual(
expect.objectContaining({
status: false,
code: NETWORK_ERROR,
message: "Connection timeout",
})
);
});
it("throws AppError when url is missing", async () => {
const provider = new PortProvider(createSuccessNet());
await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("URL and port are required for port monitoring");
});
it("throws AppError when port is missing", async () => {
const provider = new PortProvider(createSuccessNet());
await expect(provider.handle(makeMonitor({ port: undefined }))).rejects.toThrow("URL and port are required for port monitoring");
});
it("returns failure with generic message when timeRequest catches non-Error", async () => {
const socket = {
setTimeout: jest.fn(),
on: jest.fn((event: string, cb: Function) => {
if (event === "error") {
process.nextTick(() => cb({ notAnError: true }));
}
}),
end: jest.fn(),
destroy: jest.fn(),
};
const net = { createConnection: jest.fn(() => socket) } as any;
const provider = new PortProvider(net);
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(false);
expect(result.message).toBe("Port check failed");
});
it("throws AppError wrapping the original Error message", async () => {
const provider = new PortProvider(createSuccessNet());
try {
await provider.handle(makeMonitor({ url: "" }));
expect.unreachable("should have thrown");
} catch (err: any) {
expect(err.service).toBe("PortProvider");
expect(err.method).toBe("handle");
}
});
it("throws AppError when an unexpected error occurs in outer try", async () => {
const provider = new PortProvider(createSuccessNet());
const monitor = makeMonitor();
let calls = 0;
Object.defineProperty(monitor, "url", {
get() {
calls++;
if (calls === 1) throw new Error("getter exploded");
return "example.com";
},
});
await expect(provider.handle(monitor)).rejects.toThrow("getter exploded");
});
it("throws AppError with fallback message when Error has empty message", async () => {
const provider = new PortProvider(createSuccessNet());
const monitor = makeMonitor();
let calls = 0;
Object.defineProperty(monitor, "url", {
get() {
calls++;
if (calls === 1) throw new Error("");
return "example.com";
},
});
await expect(provider.handle(monitor)).rejects.toThrow("Error performing port check");
});
it("throws AppError with stringified message for non-Error thrown values", async () => {
const provider = new PortProvider(createSuccessNet());
const monitor = makeMonitor();
let calls = 0;
Object.defineProperty(monitor, "url", {
get() {
calls++;
if (calls === 1) throw 42;
return "example.com";
},
});
await expect(provider.handle(monitor)).rejects.toThrow("42");
});
});
+44
View File
@@ -0,0 +1,44 @@
import { describe, expect, it } from "@jest/globals";
import { timeRequest, NETWORK_ERROR, PING_ERROR } from "../../../src/service/infrastructure/network/utils.ts";
describe("network utils", () => {
describe("timeRequest", () => {
it("returns response and responseTime on success", async () => {
const result = await timeRequest(async () => "hello");
expect(result.response).toBe("hello");
expect(result.responseTime).toBeGreaterThanOrEqual(0);
expect(result.error).toBeNull();
});
it("returns error and responseTime on failure", async () => {
const err = new Error("boom");
const result = await timeRequest(async () => {
throw err;
});
expect(result.response).toBeNull();
expect(result.responseTime).toBeGreaterThanOrEqual(0);
expect(result.error).toBe(err);
});
it("measures elapsed time", async () => {
const result = await timeRequest(async () => {
await new Promise((r) => setTimeout(r, 50));
return "done";
});
expect(result.responseTime).toBeGreaterThanOrEqual(40);
});
});
describe("constants", () => {
it("NETWORK_ERROR is 5000", () => {
expect(NETWORK_ERROR).toBe(5000);
});
it("PING_ERROR is 5001", () => {
expect(PING_ERROR).toBe(5001);
});
});
});
@@ -0,0 +1,194 @@
import { describe, expect, it, jest, beforeEach, afterEach } from "@jest/globals";
import { WebSocketProvider } from "../../../src/service/infrastructure/network/WebSocketProvider.ts";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import { NETWORK_ERROR } from "../../../src/service/infrastructure/network/utils.ts";
import type { Monitor } from "../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
({
id: "mon-1",
teamId: "team-1",
type: "websocket",
url: "wss://example.com/ws",
ignoreTlsErrors: false,
...overrides,
}) as Monitor;
const createMockWS = (behavior: "open" | "error" = "open") => {
return jest.fn().mockImplementation((_url: string, _opts: any) => {
const handlers: Record<string, Function> = {};
const ws = {
on: jest.fn((event: string, cb: Function) => {
handlers[event] = cb;
if (behavior === "open" && event === "open") {
process.nextTick(() => cb());
}
if (behavior === "error" && event === "error") {
process.nextTick(() => cb(new Error("Connection failed")));
}
}),
close: jest.fn(),
};
return ws;
}) as any;
};
// ── Contract ─────────────────────────────────────────────────────────────────
testStatusProviderContract("WebSocketProvider", {
create: () => new WebSocketProvider(createMockWS("open")),
supportedType: "websocket",
unsupportedType: "http",
makeMonitor: () => makeMonitor(),
});
// ── Tests ────────────────────────────────────────────────────────────────────
describe("WebSocketProvider", () => {
it("returns success when connection opens", async () => {
const provider = new WebSocketProvider(createMockWS("open"));
const result = await provider.handle(makeMonitor());
expect(result).toEqual(
expect.objectContaining({
monitorId: "mon-1",
teamId: "team-1",
type: "websocket",
status: true,
code: 200,
message: "WebSocket check successful",
payload: { connected: true },
})
);
});
it("returns failure when connection errors", async () => {
const provider = new WebSocketProvider(createMockWS("error"));
const result = await provider.handle(makeMonitor());
expect(result).toEqual(
expect.objectContaining({
status: false,
code: NETWORK_ERROR,
message: "Connection failed",
payload: { connected: false },
})
);
});
it("returns failure with generic message for non-Error connection error", async () => {
const WS = jest.fn().mockImplementation(() => ({
on: jest.fn((event: string, cb: Function) => {
if (event === "error") {
process.nextTick(() => cb("string error"));
}
}),
close: jest.fn(),
})) as any;
const provider = new WebSocketProvider(WS);
const result = await provider.handle(makeMonitor());
expect(result.message).toBe("WebSocket check failed");
});
it("passes rejectUnauthorized: false when ignoreTlsErrors is true", async () => {
const WS = jest.fn().mockImplementation((_url: string, opts: any) => {
expect(opts.rejectUnauthorized).toBe(false);
return {
on: jest.fn((event: string, cb: Function) => {
if (event === "open") process.nextTick(() => cb());
}),
close: jest.fn(),
};
}) as any;
const provider = new WebSocketProvider(WS);
await provider.handle(makeMonitor({ ignoreTlsErrors: true }));
expect(WS).toHaveBeenCalled();
});
it("throws AppError when url is missing", async () => {
const provider = new WebSocketProvider(createMockWS("open"));
await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("URL is required for WebSocket monitoring");
});
it("throws AppError when an unexpected error occurs in outer try", async () => {
const provider = new WebSocketProvider(createMockWS("open"));
const monitor = makeMonitor();
let calls = 0;
Object.defineProperty(monitor, "url", {
get() {
calls++;
if (calls === 1) throw new Error("getter failed");
return "wss://example.com";
},
});
await expect(provider.handle(monitor)).rejects.toThrow("getter failed");
});
it("throws AppError with fallback message when Error has empty message", async () => {
const provider = new WebSocketProvider(createMockWS("open"));
const monitor = makeMonitor();
let calls = 0;
Object.defineProperty(monitor, "url", {
get() {
calls++;
if (calls === 1) throw new Error("");
return "wss://example.com";
},
});
await expect(provider.handle(monitor)).rejects.toThrow("Error performing WebSocket check");
});
it("throws AppError with stringified message for non-Error thrown values", async () => {
const provider = new WebSocketProvider(createMockWS("open"));
const monitor = makeMonitor();
let calls = 0;
Object.defineProperty(monitor, "url", {
get() {
calls++;
if (calls === 1) throw 99;
return "wss://example.com";
},
});
await expect(provider.handle(monitor)).rejects.toThrow("99");
});
describe("connection timeout", () => {
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
it("returns failure on connection timeout", async () => {
// WS that never fires open or error
const WS = jest.fn().mockImplementation(() => ({
on: jest.fn(),
close: jest.fn(),
})) as any;
const provider = new WebSocketProvider(WS);
const promise = provider.handle(makeMonitor());
await jest.advanceTimersByTimeAsync(10000);
const result = await promise;
expect(result).toEqual(
expect.objectContaining({
status: false,
code: NETWORK_ERROR,
message: "WebSocket connection timeout",
})
);
});
});
});