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

432 lines
16 KiB
TypeScript

import { describe, expect, it, jest } from "@jest/globals";
import { GrpcProvider } from "../../../../src/service/networkProviders/GrpcProvider.ts";
import { testStatusProviderContract } from "../../../helpers/statusProviderContract.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: "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"],
];
// ── Peer reachability ────────────────────────────────────────────────
// NOT_SERVING and a connection failure both report NETWORK_ERROR, and gRPC codes are not HTTP statuses,
// so `peerResponded` is what stops the egress check reading a server's answer as a transport failure.
it("reports the peer as having answered when the server replies NOT_SERVING", 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.peerResponded).toBe(true);
});
it.each([[5], [7], [12], [16], [2]])("reports the peer as having answered for gRPC code %i", async (code) => {
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.peerResponded).toBe(true);
});
it.each([[4], [14]])("reports the peer as unreachable for gRPC code %i", async (code) => {
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.peerResponded).toBe(false);
});
it("reports the peer as unreachable when the failure carries no gRPC code", async () => {
const client = {
Check: jest.fn((_r: any, _o: any, cb: Function) => cb({ message: "socket hang up" })),
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.peerResponded).toBe(false);
});
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 }));
});
});
});