Files
2026-09-25 11:39:16 -07:00

309 lines
9.3 KiB
TypeScript

import { describe, expect, it, jest } from "@jest/globals";
import { PortProvider } from "../../../../src/service/networkProviders/PortProvider.ts";
import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts";
import { NETWORK_ERROR } from "../../../../src/types/network.ts";
import type { Monitor } from "../../../../src/domain/monitors/monitor.type.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");
});
// ── Peer reachability ────────────────────────────────────────────────────
// A check is only recorded as degraded when nothing answered, so a service on the local network that
// refuses connections has to stay distinguishable from the instance losing its own egress.
it.each([["ECONNREFUSED"], ["ECONNRESET"]])("reports the peer as having answered for %s", async (code) => {
const provider = new PortProvider(createErrorNet(Object.assign(new Error("connect failed"), { code })));
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(false);
expect(result.code).toBe(NETWORK_ERROR);
expect(result.peerResponded).toBe(true);
});
it.each([["ETIMEDOUT"], ["EHOSTUNREACH"], ["ENETUNREACH"], ["ENOTFOUND"]])("reports the peer as silent for %s", async (code) => {
const provider = new PortProvider(createErrorNet(Object.assign(new Error("connect failed"), { code })));
const result = await provider.handle(makeMonitor());
expect(result.peerResponded).toBe(false);
});
it("reports the peer as silent on a connection timeout", async () => {
const provider = new PortProvider(createTimeoutNet());
const result = await provider.handle(makeMonitor());
expect(result.peerResponded).toBe(false);
});
it("reports the peer as having answered when the port accepts the connection", async () => {
const provider = new PortProvider(createSuccessNet());
const result = await provider.handle(makeMonitor());
expect(result.peerResponded).toBe(true);
});
});