import { describe, expect, it, jest } from "@jest/globals"; import { GeoChecksService } from "../../../src/domain/geo-checks/geo-check.service.ts"; import { createMockLogger } from "../../helpers/createMockLogger.ts"; import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; import type { GeoCheckResult } from "../../../src/domain/geo-checks/geo-check.type.ts"; // ── Helpers ────────────────────────────────────────────────────────────────── const makeMonitor = (overrides?: Partial): Monitor => ({ id: "mon-1", teamId: "team-1", type: "http", url: "https://example.com", geoCheckEnabled: true, geoCheckLocations: ["NA", "EU"], ...overrides, }) as Monitor; const makeGeoResult = (overrides?: Partial): GeoCheckResult => ({ continent: "NA", country: "US", city: "New York", status: "up", responseTime: 50, ...overrides, }); const createService = (overrides?: Record) => { const logger = createMockLogger(); const geoChecksRepository = { createGeoChecks: jest.fn().mockResolvedValue([]), findByMonitorId: jest.fn().mockResolvedValue({ geoChecksCount: 0, geoChecks: [] }), }; const globalPingService = { createMeasurement: jest.fn().mockResolvedValue("measurement-123"), pollForResults: jest.fn().mockResolvedValue([makeGeoResult()]), }; const monitorsRepository = { findById: jest.fn().mockResolvedValue(makeMonitor()), }; const defaults = { logger, geoChecksRepository, globalPingService, monitorsRepository, ...overrides }; const service = new GeoChecksService(defaults as any); return { service, ...defaults }; }; // ── Tests ──────────────────────────────────────────────────────────────────── describe("GeoChecksService", () => { // ── buildGeoCheck ─────────────────────────────────────────────────────── describe("buildGeoCheck", () => { it("returns a geo check document on success", async () => { const { service } = createService(); const result = await service.buildGeoCheck(makeMonitor()); expect(result).not.toBeNull(); expect(result!.metadata).toEqual({ monitorId: "mon-1", teamId: "team-1", type: "http" }); expect(result!.results).toHaveLength(1); expect(result!.results[0].continent).toBe("NA"); expect(result!.id).toBeDefined(); expect(result!.expiry).toBeDefined(); }); it("returns null and warns when monitor has no URL", async () => { const { service, logger } = createService(); const result = await service.buildGeoCheck(makeMonitor({ url: "" })); expect(result).toBeNull(); expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Monitor missing URL for geo check" })); }); it("returns null and warns when geoCheckLocations is empty", async () => { const { service, logger } = createService(); const result = await service.buildGeoCheck(makeMonitor({ geoCheckLocations: [] })); expect(result).toBeNull(); expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Monitor missing geo check locations" })); }); it("returns null and warns when geoCheckLocations is undefined", async () => { const { service, logger } = createService(); const result = await service.buildGeoCheck(makeMonitor({ geoCheckLocations: undefined as any })); expect(result).toBeNull(); expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Monitor missing geo check locations" })); }); it("returns null when createMeasurement returns null (API unavailable)", async () => { const { service, logger } = createService({ globalPingService: { createMeasurement: jest.fn().mockResolvedValue(null), pollForResults: jest.fn(), }, }); const result = await service.buildGeoCheck(makeMonitor()); expect(result).toBeNull(); expect(logger.debug).toHaveBeenCalledWith(expect.objectContaining({ message: "Skipping geo check due to API unavailability" })); }); it("returns null when pollForResults returns empty array", async () => { const { service, logger } = createService({ globalPingService: { createMeasurement: jest.fn().mockResolvedValue("measurement-123"), pollForResults: jest.fn().mockResolvedValue([]), }, }); const result = await service.buildGeoCheck(makeMonitor()); expect(result).toBeNull(); expect(logger.debug).toHaveBeenCalledWith(expect.objectContaining({ message: "No successful geo check results" })); }); it("returns null and logs error when an exception is thrown", async () => { const { service, logger } = createService({ globalPingService: { createMeasurement: jest.fn().mockRejectedValue(new Error("API error")), pollForResults: jest.fn(), }, }); const result = await service.buildGeoCheck(makeMonitor()); expect(result).toBeNull(); expect(logger.error).toHaveBeenCalledWith( expect.objectContaining({ message: "Error executing geo check", details: expect.objectContaining({ error: "API error" }), }) ); }); it("logs 'Unknown error' for non-Error exceptions", async () => { const { service, logger } = createService({ globalPingService: { createMeasurement: jest.fn().mockRejectedValue("string error"), pollForResults: jest.fn(), }, }); const result = await service.buildGeoCheck(makeMonitor()); expect(result).toBeNull(); expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ details: expect.objectContaining({ error: "Unknown error" }) })); }); }); // ── createGeoChecks ───────────────────────────────────────────────────── describe("createGeoChecks", () => { it("delegates to repository", async () => { const { service, geoChecksRepository } = createService(); const geoChecks = [{ id: "gc-1" }] as any; await service.createGeoChecks(geoChecks); expect(geoChecksRepository.createGeoChecks).toHaveBeenCalledWith(geoChecks); }); }); // ── getGeoChecksByMonitor ──────────────────────────────────────────────── describe("getGeoChecksByMonitor", () => { it("returns geo checks from repository", async () => { const expected = { geoChecksCount: 1, geoChecks: [{ id: "gc-1" }] }; const { service, geoChecksRepository } = createService(); (geoChecksRepository.findByMonitorId as jest.Mock).mockResolvedValue(expected); const result = await service.getGeoChecksByMonitor({ monitorId: "mon-1", teamId: "team-1", sortOrder: "desc", dateRange: "day", continent: "NA" as any, }); expect(result).toEqual(expected); expect(geoChecksRepository.findByMonitorId).toHaveBeenCalledWith("mon-1", "desc", "day", 0, 5, ["NA"]); }); it("passes array of continents through", async () => { const { service, geoChecksRepository } = createService(); await service.getGeoChecksByMonitor({ monitorId: "mon-1", teamId: "team-1", sortOrder: "desc", dateRange: "day", continent: ["NA", "EU"] as any, }); expect(geoChecksRepository.findByMonitorId).toHaveBeenCalledWith("mon-1", "desc", "day", 0, 5, ["NA", "EU"]); }); it("passes undefined continents when not provided", async () => { const { service, geoChecksRepository } = createService(); await service.getGeoChecksByMonitor({ monitorId: "mon-1", teamId: "team-1", sortOrder: "desc", dateRange: "day", continent: undefined as any, }); expect(geoChecksRepository.findByMonitorId).toHaveBeenCalledWith("mon-1", "desc", "day", 0, 5, undefined); }); it("uses provided page and rowsPerPage", async () => { const { service, geoChecksRepository } = createService(); await service.getGeoChecksByMonitor({ monitorId: "mon-1", teamId: "team-1", sortOrder: "asc", dateRange: "week", page: 3, rowsPerPage: 20, continent: "EU" as any, }); expect(geoChecksRepository.findByMonitorId).toHaveBeenCalledWith("mon-1", "asc", "week", 3, 20, ["EU"]); }); it("throws when monitorId is missing", async () => { const { service } = createService(); await expect( service.getGeoChecksByMonitor({ monitorId: "", teamId: "team-1", sortOrder: "desc", dateRange: "day", continent: "NA" as any }) ).rejects.toThrow("No monitor ID in request"); }); it("throws when teamId is missing", async () => { const { service } = createService(); await expect( service.getGeoChecksByMonitor({ monitorId: "mon-1", teamId: "", sortOrder: "desc", dateRange: "day", continent: "NA" as any }) ).rejects.toThrow("No team ID in request"); }); it("throws 404 when monitor is not found", async () => { const { service, monitorsRepository } = createService(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(null); await expect( service.getGeoChecksByMonitor({ monitorId: "mon-1", teamId: "team-1", sortOrder: "desc", dateRange: "day", continent: "NA" as any }) ).rejects.toThrow("Monitor with ID mon-1 not found."); }); }); });