add tests for globalPingService

This commit is contained in:
Alex Holliday committed 2026-04-09 10:03:23 -07:00
1 parent db833b441a
commit dca09daf35
1 file changed
+499
@@ -0,0 +1,499 @@
import { describe, expect, it, jest, beforeEach, afterEach } from "@jest/globals";
import { createMockLogger } from "../../helpers/createMockLogger.ts";
import type { GeoContinent } from "../../../src/types/geoCheck.ts";
// ── got mock ─────────────────────────────────────────────────────────────────
const mockGotPost = jest.fn();
const mockGotGet = jest.fn();
jest.unstable_mockModule("got", () => ({
default: {
post: mockGotPost,
get: mockGotGet,
},
}));
// Dynamic import AFTER mock registration
const { GlobalPingService } = await import("../../../src/service/infrastructure/globalPingService.ts");
// ── Helpers ──────────────────────────────────────────────────────────────────
const createService = () => {
const logger = createMockLogger();
const service = new GlobalPingService(logger as any);
return { service, logger };
};
const makeProbeResult = (overrides?: Record<string, any>) => ({
probe: {
continent: "NA" as GeoContinent,
region: "Northern America",
country: "US",
state: "CA",
city: "San Francisco",
longitude: -122.4,
latitude: 37.77,
},
result: {
status: "finished",
statusCode: 200,
statusCodeName: "OK",
timings: {
total: 150,
dns: 10,
tcp: 20,
tls: 30,
firstByte: 50,
download: 40,
},
},
...overrides,
});
// ── Tests ────────────────────────────────────────────────────────────────────
describe("GlobalPingService", () => {
beforeEach(() => {
jest.useFakeTimers();
mockGotPost.mockReset();
mockGotGet.mockReset();
});
afterEach(() => {
jest.useRealTimers();
});
// ── Static / instance properties ─────────────────────────────────────
describe("serviceName", () => {
it("returns GlobalPingService from static property", () => {
expect(GlobalPingService.SERVICE_NAME).toBe("GlobalPingService");
});
it("returns GlobalPingService from instance getter", () => {
const { service } = createService();
expect(service.serviceName).toBe("GlobalPingService");
});
});
// ── createMeasurement ────────────────────────────────────────────────
describe("createMeasurement", () => {
it("creates a measurement and returns the id", async () => {
const { service, logger } = createService();
mockGotPost.mockResolvedValue({ body: { id: "meas-123" } });
const result = await service.createMeasurement("http", "https://example.com", ["NA", "EU"] as GeoContinent[]);
expect(result).toBe("meas-123");
expect(mockGotPost).toHaveBeenCalledWith(
"https://api.globalping.io/v1/measurements",
expect.objectContaining({
json: {
type: "http",
target: "example.com",
locations: [{ continent: "NA" }, { continent: "EU" }],
limit: 2,
},
responseType: "json",
timeout: { request: 10000 },
})
);
expect(logger.debug).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("meas-123"),
method: "createMeasurement",
})
);
});
it("strips http:// protocol from target URL", async () => {
const { service } = createService();
mockGotPost.mockResolvedValue({ body: { id: "meas-1" } });
await service.createMeasurement("http", "http://example.com", ["NA"] as GeoContinent[]);
expect(mockGotPost).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
json: expect.objectContaining({ target: "example.com" }),
})
);
});
it("strips https:// protocol from target URL", async () => {
const { service } = createService();
mockGotPost.mockResolvedValue({ body: { id: "meas-1" } });
await service.createMeasurement("http", "https://example.com/path", ["EU"] as GeoContinent[]);
expect(mockGotPost).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
json: expect.objectContaining({ target: "example.com/path" }),
})
);
});
it("returns null and logs error for unsupported monitor type", async () => {
const { service, logger } = createService();
const result = await service.createMeasurement("hardware" as any, "https://example.com", ["NA"] as GeoContinent[]);
expect(result).toBeNull();
expect(logger.error).toHaveBeenCalledWith(
expect.objectContaining({
message: "GlobalPing API unavailable, skipping geo check",
method: "createMeasurement",
})
);
});
it("returns null and logs error when API call fails", async () => {
const { service, logger } = createService();
mockGotPost.mockRejectedValue(new Error("Network error"));
const result = await service.createMeasurement("http", "https://example.com", ["NA"] as GeoContinent[]);
expect(result).toBeNull();
expect(logger.error).toHaveBeenCalledWith(
expect.objectContaining({
message: "GlobalPing API unavailable, skipping geo check",
method: "createMeasurement",
})
);
});
it("logs stack as undefined for non-Error thrown values", async () => {
const { service, logger } = createService();
mockGotPost.mockRejectedValue("string error");
await service.createMeasurement("http", "https://example.com", ["NA"] as GeoContinent[]);
expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ stack: undefined }));
});
it("supports ping monitor type", async () => {
const { service } = createService();
mockGotPost.mockResolvedValue({ body: { id: "meas-ping" } });
const result = await service.createMeasurement("ping", "example.com", ["NA"] as GeoContinent[]);
expect(result).toBe("meas-ping");
});
});
// ── pollForResults ───────────────────────────────────────────────────
describe("pollForResults", () => {
it("returns transformed results when measurement is finished", async () => {
const { service, logger } = createService();
mockGotGet.mockResolvedValue({
body: {
status: "finished",
results: [makeProbeResult()],
},
});
const results = await service.pollForResults("meas-123");
expect(results).toHaveLength(1);
expect(results[0]).toEqual(
expect.objectContaining({
location: expect.objectContaining({
continent: "NA",
city: "San Francisco",
}),
status: true,
statusCode: 200,
timings: expect.objectContaining({
total: 150,
dns: 10,
}),
})
);
expect(logger.debug).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("meas-123"),
method: "pollForResults",
})
);
});
it("defaults results to empty array when finished with no results", async () => {
const { service } = createService();
mockGotGet.mockResolvedValue({
body: { status: "finished", results: undefined },
});
const results = await service.pollForResults("meas-123");
expect(results).toEqual([]);
});
it("returns empty array when measurement has failed", async () => {
const { service, logger } = createService();
mockGotGet.mockResolvedValue({
body: { status: "failed" },
});
const results = await service.pollForResults("meas-123");
expect(results).toEqual([]);
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("failed"),
method: "pollForResults",
})
);
});
it("polls again when status is in-progress, then returns on finished", async () => {
const { service } = createService();
mockGotGet.mockResolvedValueOnce({ body: { status: "in-progress" } }).mockResolvedValueOnce({
body: {
status: "finished",
results: [makeProbeResult()],
},
});
const promise = service.pollForResults("meas-123");
// Advance past the sleep(2000)
await jest.advanceTimersByTimeAsync(2000);
const results = await promise;
expect(mockGotGet).toHaveBeenCalledTimes(2);
expect(results).toHaveLength(1);
});
it("returns empty array and logs error when API call throws", async () => {
const { service, logger } = createService();
mockGotGet.mockRejectedValue(new Error("Connection refused"));
const results = await service.pollForResults("meas-123");
expect(results).toEqual([]);
expect(logger.error).toHaveBeenCalledWith(
expect.objectContaining({
message: "Error polling GlobalPing API",
method: "pollForResults",
})
);
});
it("logs stack as undefined for non-Error thrown values in poll", async () => {
const { service, logger } = createService();
mockGotGet.mockRejectedValue(42);
await service.pollForResults("meas-123");
expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ stack: undefined }));
});
it("returns empty array and logs warning on timeout", async () => {
const { service, logger } = createService();
// Always return in-progress so we hit the timeout
mockGotGet.mockImplementation(async () => ({ body: { status: "in-progress" } }));
const promise = service.pollForResults("meas-123", 100);
// Advance time well past the timeout
await jest.advanceTimersByTimeAsync(35000);
const results = await promise;
expect(results).toEqual([]);
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("timeout"),
method: "pollForResults",
details: expect.objectContaining({ measurementId: "meas-123", timeoutMs: 100 }),
})
);
});
});
// ── transformResults (via pollForResults) ────────────────────────────
describe("transformResults", () => {
it("skips probes with non-finished status", async () => {
const { service } = createService();
mockGotGet.mockResolvedValue({
body: {
status: "finished",
results: [makeProbeResult({ result: { status: "failed" } }), makeProbeResult({ result: { status: "timeout" } })],
},
});
const results = await service.pollForResults("meas-123");
expect(results).toEqual([]);
});
it("transforms HTTP results with statusCode and timings", async () => {
const { service } = createService();
mockGotGet.mockResolvedValue({
body: {
status: "finished",
results: [makeProbeResult()],
},
});
const results = await service.pollForResults("meas-123");
expect(results[0].status).toBe(true);
expect(results[0].statusCode).toBe(200);
expect(results[0].timings.total).toBe(150);
});
it("marks HTTP result as failed for non-2xx status codes", async () => {
const { service } = createService();
mockGotGet.mockResolvedValue({
body: {
status: "finished",
results: [
makeProbeResult({
result: {
status: "finished",
statusCode: 500,
timings: { total: 100, dns: 5, tcp: 10, tls: 15, firstByte: 30, download: 40 },
},
}),
],
},
});
const results = await service.pollForResults("meas-123");
expect(results[0].status).toBe(false);
expect(results[0].statusCode).toBe(500);
});
it("transforms ping results with stats (no loss)", async () => {
const { service } = createService();
mockGotGet.mockResolvedValue({
body: {
status: "finished",
results: [
makeProbeResult({
result: {
status: "finished",
stats: { min: 10, max: 30, avg: 20, total: 3, loss: 0, rcv: 3, drop: 0 },
},
}),
],
},
});
const results = await service.pollForResults("meas-123");
expect(results[0].status).toBe(true);
expect(results[0].statusCode).toBe(200);
expect(results[0].timings.total).toBe(20);
expect(results[0].timings.dns).toBe(0);
});
it("transforms ping results with loss as failed", async () => {
const { service } = createService();
mockGotGet.mockResolvedValue({
body: {
status: "finished",
results: [
makeProbeResult({
result: {
status: "finished",
stats: { min: 10, max: 30, avg: 20, total: 3, loss: 2, rcv: 1, drop: 2 },
},
}),
],
},
});
const results = await service.pollForResults("meas-123");
expect(results[0].status).toBe(false);
expect(results[0].statusCode).toBe(5000);
});
it("uses empty string for null state in location", async () => {
const { service } = createService();
mockGotGet.mockResolvedValue({
body: {
status: "finished",
results: [
makeProbeResult({
probe: {
continent: "EU",
region: "Western Europe",
country: "DE",
state: null,
city: "Berlin",
longitude: 13.4,
latitude: 52.5,
},
}),
],
},
});
const results = await service.pollForResults("meas-123");
expect(results[0].location.state).toBe("");
});
it("skips probes with no statusCode/timings and no stats", async () => {
const { service } = createService();
mockGotGet.mockResolvedValue({
body: {
status: "finished",
results: [
makeProbeResult({
result: { status: "finished" },
}),
],
},
});
const results = await service.pollForResults("meas-123");
expect(results).toEqual([]);
});
it("transforms multiple probes from different locations", async () => {
const { service } = createService();
mockGotGet.mockResolvedValue({
body: {
status: "finished",
results: [
makeProbeResult(),
makeProbeResult({
probe: {
continent: "EU",
region: "Western Europe",
country: "GB",
state: null,
city: "London",
longitude: -0.12,
latitude: 51.5,
},
result: {
status: "finished",
statusCode: 200,
timings: { total: 80, dns: 5, tcp: 10, tls: 15, firstByte: 30, download: 20 },
},
}),
],
},
});
const results = await service.pollForResults("meas-123");
expect(results).toHaveLength(2);
expect(results[0].location.continent).toBe("NA");
expect(results[1].location.continent).toBe("EU");
});
});
});