diff --git a/server/src/config/services.worker.ts b/server/src/config/services.worker.ts index fdab162f8..994156978 100644 --- a/server/src/config/services.worker.ts +++ b/server/src/config/services.worker.ts @@ -1,5 +1,4 @@ import { Resolver } from "dns/promises"; -import axios from "axios"; import got from "got"; import ping from "ping"; import Docker from "dockerode"; diff --git a/server/src/domain/egress/egress.service.ts b/server/src/domain/egress/egress.service.ts index c66d1b325..a9f9d69d2 100644 --- a/server/src/domain/egress/egress.service.ts +++ b/server/src/domain/egress/egress.service.ts @@ -54,7 +54,7 @@ export class EgressService implements IEgressService { private settingsService: ISettingsService, private egressStateRepository: IEgressStateRepository, private jobsRepository: IJobsRepository, - private networkService: IProviderRegistry, + private providerRegistry: IProviderRegistry, private proxyResolver: IProxyResolver, private logger: ILogger ) {} @@ -100,7 +100,7 @@ export class EgressService implements IEgressService { const { response, responseTime, error } = await timeRequest(async () => { const proxyUrl = await this.proxyResolver.resolve(monitor); return Promise.race([ - this.networkService.probe(monitor, { proxyUrl }), + this.providerRegistry.probe(monitor, { proxyUrl }), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`Egress probe timed out after ${PROBE_TIMEOUT_MS}ms`)), PROBE_TIMEOUT_MS); }), diff --git a/server/src/service/networkProviders/providerRegistry.ts b/server/src/service/networkProviders/providerRegistry.ts index f7cc9c63a..38895e67d 100644 --- a/server/src/service/networkProviders/providerRegistry.ts +++ b/server/src/service/networkProviders/providerRegistry.ts @@ -2,7 +2,7 @@ import type { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import type { CheckContext, MonitorPayloadMap, MonitorStatusResponse } from "@/types/network.js"; import { NETWORK_ERROR } from "@/types/network.js"; import { IStatusProvider } from "./IStatusProvider.js"; -const SERVICE_NAME = "NetworkService"; +const SERVICE_NAME = "ProviderRegistry"; export interface IProviderRegistry { probe(monitor: Monitor & { type: T }, ctx?: CheckContext): Promise>; diff --git a/server/test/helpers/heartbeatTestHarness.ts b/server/test/helpers/heartbeatTestHarness.ts index f5b9ab9fa..79e0686c9 100644 --- a/server/test/helpers/heartbeatTestHarness.ts +++ b/server/test/helpers/heartbeatTestHarness.ts @@ -60,7 +60,7 @@ export interface HeartbeatTestHarness { statusService: StatusService; incidentService: IncidentService; notificationsService: { handleNotifications: jest.Mock }; - networkService: { requestStatus: jest.Mock }; + providerRegistry: { probe: jest.Mock }; bufferStub: { addToBuffer: jest.Mock; addGeoCheckToBuffer: jest.Mock; scheduleNextFlush: jest.Mock }; maintenanceWindowsRepo: { findByMonitorId: jest.Mock }; messageBuilder: { buildThresholdBreachMessage: jest.Mock }; @@ -87,8 +87,8 @@ export function createHeartbeatTestHarness(): HeartbeatTestHarness { let nextResponse: MonitorStatusResponse | null = null; let nextStatus = true; let nextCode = 200; - const networkService = { - requestStatus: jest.fn().mockImplementation(() => { + const providerRegistry = { + probe: jest.fn().mockImplementation(() => { if (nextResponse) { return Promise.resolve(nextResponse); } @@ -137,7 +137,7 @@ export function createHeartbeatTestHarness(): HeartbeatTestHarness { checksRepository: { findUnevaluatedByMonitorId: jest.fn() } as any, jobsRepository: { upsertEvaluate: jest.fn(), pullEvaluated: jest.fn() } as any, checkService: checkService as any, - networkService: networkService as any, + providerRegistry: providerRegistry as any, proxyResolver: proxyResolver as any, bufferService: bufferStub as any, dockerLogsService: dockerLogsService as any, @@ -163,7 +163,7 @@ export function createHeartbeatTestHarness(): HeartbeatTestHarness { statusService, incidentService, notificationsService, - networkService, + providerRegistry, bufferStub, maintenanceWindowsRepo, messageBuilder, diff --git a/server/test/helpers/statusProviderContract.ts b/server/test/helpers/statusProviderContract.ts index 707d8b3c3..eac4f1aaa 100644 --- a/server/test/helpers/statusProviderContract.ts +++ b/server/test/helpers/statusProviderContract.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "@jest/globals"; -import type { IStatusProvider } from "../../src/service/network/IStatusProvider.ts"; +import type { IStatusProvider } from "../../src/service/networkProviders/IStatusProvider.ts"; import type { Monitor } from "../../src/domain/monitors/monitor.type.ts"; /** diff --git a/server/test/integration/evaluateCursorMultiWorker.test.ts b/server/test/integration/evaluateCursorMultiWorker.test.ts index 5f40d2eef..5c9d163f8 100644 --- a/server/test/integration/evaluateCursorMultiWorker.test.ts +++ b/server/test/integration/evaluateCursorMultiWorker.test.ts @@ -126,7 +126,7 @@ describe("Evaluate cursor with more than one processing worker", () => { checksRepository, jobsRepository, checkService, - networkService: { requestStatus: jest.fn() } as any, + providerRegistry: { probe: jest.fn() } as any, proxyResolver: { resolve: jest.fn() } as any, bufferService, dockerLogsService: { buildDockerLogs: jest.fn() } as any, diff --git a/server/test/integration/heartbeatJobMaintenance.test.ts b/server/test/integration/heartbeatJobMaintenance.test.ts index 3e51814db..af83279e9 100644 --- a/server/test/integration/heartbeatJobMaintenance.test.ts +++ b/server/test/integration/heartbeatJobMaintenance.test.ts @@ -44,7 +44,7 @@ describe("Heartbeat job: maintenance windows", () => { expect(storedMonitor.status).toBe("maintenance"); // No network request should have been made - expect(h.networkService.requestStatus).not.toHaveBeenCalled(); + expect(h.providerRegistry.probe).not.toHaveBeenCalled(); // No check should have been buffered expect(h.bufferStub.addToBuffer).not.toHaveBeenCalled(); @@ -68,7 +68,7 @@ describe("Heartbeat job: maintenance windows", () => { // since status was already "maintenance" const storedMonitor = await h.monitorsRepo.findById("mon-1", "team-1"); expect(storedMonitor.status).toBe("maintenance"); - expect(h.networkService.requestStatus).not.toHaveBeenCalled(); + expect(h.providerRegistry.probe).not.toHaveBeenCalled(); }); it("resumes normal checks when maintenance window expires", async () => { @@ -88,7 +88,7 @@ describe("Heartbeat job: maintenance windows", () => { await h.heartbeatJob(monitor); // Network request should have been made this time - expect(h.networkService.requestStatus).toHaveBeenCalledTimes(1); + expect(h.providerRegistry.probe).toHaveBeenCalledTimes(1); // Check should have been buffered expect(h.bufferStub.addToBuffer).toHaveBeenCalledTimes(1); @@ -105,7 +105,7 @@ describe("Heartbeat job: maintenance windows", () => { await h.heartbeatJob(monitor); // Normal check should proceed since window is inactive - expect(h.networkService.requestStatus).toHaveBeenCalledTimes(1); + expect(h.providerRegistry.probe).toHaveBeenCalledTimes(1); expect(h.bufferStub.addToBuffer).toHaveBeenCalledTimes(1); }); @@ -123,7 +123,7 @@ describe("Heartbeat job: maintenance windows", () => { expect((await h.monitorsRepo.findById("mon-a", "team-1")).status).toBe("maintenance"); expect((await h.monitorsRepo.findById("mon-b", "team-1")).status).toBe("maintenance"); - expect(h.networkService.requestStatus).not.toHaveBeenCalled(); + expect(h.providerRegistry.probe).not.toHaveBeenCalled(); }); it("skips checks when window is in the past (expired)", async () => { @@ -141,6 +141,6 @@ describe("Heartbeat job: maintenance windows", () => { await h.heartbeatJob(monitor); // Window is in the past — normal check should proceed - expect(h.networkService.requestStatus).toHaveBeenCalledTimes(1); + expect(h.providerRegistry.probe).toHaveBeenCalledTimes(1); }); }); diff --git a/server/test/unit/providers/network/advancedMatcher.test.ts b/server/test/unit/providers/network/advancedMatcher.test.ts index b66d5ed56..65aa98212 100644 --- a/server/test/unit/providers/network/advancedMatcher.test.ts +++ b/server/test/unit/providers/network/advancedMatcher.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, jest } from "@jest/globals"; -import { AdvancedMatcher } from "../../../../src/service/network/AdvancedMatcher.ts"; +import { AdvancedMatcher } from "../../../../src/service/networkProviders/AdvancedMatcher.ts"; import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; // ── Helpers ────────────────────────────────────────────────────────────────── diff --git a/server/test/unit/providers/network/dnsProvider.test.ts b/server/test/unit/providers/network/dnsProvider.test.ts index c6cfff23b..d4aa5b28d 100644 --- a/server/test/unit/providers/network/dnsProvider.test.ts +++ b/server/test/unit/providers/network/dnsProvider.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; import { NETWORK_ERROR } from "../../../../src/types/network.ts"; import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; -import { DNSProvider } from "../../../../src/service/network/DNSProvider.ts"; +import { DNSProvider } from "../../../../src/service/networkProviders/DNSProvider.ts"; // ── Mocks ──────────────────────────────────────────────────────────────────── diff --git a/server/test/unit/providers/network/dockerProvider.test.ts b/server/test/unit/providers/network/dockerProvider.test.ts index 50e666a1d..a1440654a 100644 --- a/server/test/unit/providers/network/dockerProvider.test.ts +++ b/server/test/unit/providers/network/dockerProvider.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, jest } from "@jest/globals"; import https from "node:https"; -import { DockerProvider } from "../../../../src/service/network/DockerProvider.ts"; +import { DockerProvider } from "../../../../src/service/networkProviders/DockerProvider.ts"; import type { IEncryptionService } from "../../../../src/service/encryption/encryptionService.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; import { createMockLogger } from "../../../helpers/createMockLogger.ts"; diff --git a/server/test/unit/providers/network/gameProvider.test.ts b/server/test/unit/providers/network/gameProvider.test.ts index eac775b3b..d0adfb20d 100644 --- a/server/test/unit/providers/network/gameProvider.test.ts +++ b/server/test/unit/providers/network/gameProvider.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, jest } from "@jest/globals"; -import { GameProvider } from "../../../../src/service/network/GameProvider.ts"; +import { GameProvider } from "../../../../src/service/networkProviders/GameProvider.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; import { createMockLogger } from "../../../helpers/createMockLogger.ts"; import { NETWORK_ERROR } from "../../../../src/types/network.ts"; diff --git a/server/test/unit/providers/network/grpcProvider.test.ts b/server/test/unit/providers/network/grpcProvider.test.ts index 0d53943d2..4fd96e329 100644 --- a/server/test/unit/providers/network/grpcProvider.test.ts +++ b/server/test/unit/providers/network/grpcProvider.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, jest } from "@jest/globals"; -import { GrpcProvider } from "../../../../src/service/network/GrpcProvider.ts"; +import { GrpcProvider } from "../../../../src/service/networkProviders/GrpcProvider.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; diff --git a/server/test/unit/providers/network/hardwareProvider.test.ts b/server/test/unit/providers/network/hardwareProvider.test.ts index b8bb66531..a4730a07c 100644 --- a/server/test/unit/providers/network/hardwareProvider.test.ts +++ b/server/test/unit/providers/network/hardwareProvider.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, jest } from "@jest/globals"; -import { HardwareProvider } from "../../../../src/service/network/HardwareProvider.ts"; +import { HardwareProvider } from "../../../../src/service/networkProviders/HardwareProvider.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; -import type { HttpProvider } from "../../../../src/service/network/HttpProvider.ts"; +import type { HttpProvider } from "../../../../src/service/networkProviders/HttpProvider.ts"; import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; // ── Helpers ────────────────────────────────────────────────────────────────── diff --git a/server/test/unit/providers/network/httpProvider.test.ts b/server/test/unit/providers/network/httpProvider.test.ts index 928c46f43..90a3111b9 100644 --- a/server/test/unit/providers/network/httpProvider.test.ts +++ b/server/test/unit/providers/network/httpProvider.test.ts @@ -39,7 +39,7 @@ jest.unstable_mockModule("got", () => ({ }, })); -const { HttpProvider } = await import("../../../../src/service/network/HttpProvider.ts"); +const { HttpProvider } = await import("../../../../src/service/networkProviders/HttpProvider.ts"); const gotModule = await import("got"); const { HTTPError, RequestError } = gotModule; diff --git a/server/test/unit/providers/network/pageSpeedProvider.test.ts b/server/test/unit/providers/network/pageSpeedProvider.test.ts index 7aa4ab0bc..d1b83f515 100644 --- a/server/test/unit/providers/network/pageSpeedProvider.test.ts +++ b/server/test/unit/providers/network/pageSpeedProvider.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, jest } from "@jest/globals"; -import { PageSpeedProvider } from "../../../../src/service/network/PageSpeedProvider.ts"; +import { PageSpeedProvider } from "../../../../src/service/networkProviders/PageSpeedProvider.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; import { createMockLogger } from "../../../helpers/createMockLogger.ts"; -import type { HttpProvider } from "../../../../src/service/network/HttpProvider.ts"; +import type { HttpProvider } from "../../../../src/service/networkProviders/HttpProvider.ts"; import type { ISettingsService } from "../../../../src/domain/app-settings/app-settings.service.ts"; import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; diff --git a/server/test/unit/providers/network/pingProvider.test.ts b/server/test/unit/providers/network/pingProvider.test.ts index eaa6227b5..c8e843f12 100644 --- a/server/test/unit/providers/network/pingProvider.test.ts +++ b/server/test/unit/providers/network/pingProvider.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, jest } from "@jest/globals"; -import { PingProvider } from "../../../../src/service/network/PingProvider.ts"; +import { PingProvider } from "../../../../src/service/networkProviders/PingProvider.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; import * as net from "net"; diff --git a/server/test/unit/providers/network/portProvider.test.ts b/server/test/unit/providers/network/portProvider.test.ts index caaf9ad3d..fd1e215eb 100644 --- a/server/test/unit/providers/network/portProvider.test.ts +++ b/server/test/unit/providers/network/portProvider.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, jest } from "@jest/globals"; -import { PortProvider } from "../../../../src/service/network/PortProvider.ts"; +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"; diff --git a/server/test/unit/providers/network/utils.test.ts b/server/test/unit/providers/network/utils.test.ts index 5a0d33972..5c49bd4eb 100644 --- a/server/test/unit/providers/network/utils.test.ts +++ b/server/test/unit/providers/network/utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "@jest/globals"; -import { timeRequest, isStatusUp, peerAnsweredFromError } from "../../../../src/service/network/utils.ts"; +import { timeRequest, isStatusUp, peerAnsweredFromError } from "../../../../src/service/networkProviders/utils.ts"; import { NETWORK_ERROR } from "../../../../src/types/network.ts"; describe("network utils", () => { diff --git a/server/test/unit/providers/network/webSocketProvider.test.ts b/server/test/unit/providers/network/webSocketProvider.test.ts index 13dc5190b..ee30f1f15 100644 --- a/server/test/unit/providers/network/webSocketProvider.test.ts +++ b/server/test/unit/providers/network/webSocketProvider.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, jest, beforeEach, afterEach } from "@jest/globals"; -import { WebSocketProvider } from "../../../../src/service/network/WebSocketProvider.ts"; +import { WebSocketProvider } from "../../../../src/service/networkProviders/WebSocketProvider.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"; diff --git a/server/test/unit/services/egressService.test.ts b/server/test/unit/services/egressService.test.ts index adc79feb4..47e73ee63 100644 --- a/server/test/unit/services/egressService.test.ts +++ b/server/test/unit/services/egressService.test.ts @@ -42,7 +42,7 @@ const makeJob = (overrides?: Partial): Job => ({ ...overrides, }); -// requestStatus mock that answers per target url +// probe mock that answers per target url const statusFor = (reachableTargets: string[]) => jest.fn().mockImplementation(async (monitor: any) => ({ monitorId: monitor.id, @@ -78,7 +78,7 @@ const createService = (overrides?: Record) => { upsertCleanupJob: jest.fn().mockResolvedValue(true), deleteGlobalJobIfUnchanged: jest.fn().mockResolvedValue(true), }, - networkService: { requestStatus: statusFor(["1.1.1.1", "8.8.8.8"]) }, + providerRegistry: { probe: statusFor(["1.1.1.1", "8.8.8.8"]) }, proxyResolver: { resolve: jest.fn().mockResolvedValue(undefined) }, logger: createMockLogger(), ...overrides, @@ -87,15 +87,15 @@ const createService = (overrides?: Record) => { defaults.settingsService as any, defaults.egressStateRepository as any, defaults.jobsRepository as any, - defaults.networkService as any, + defaults.providerRegistry as any, defaults.proxyResolver as any, defaults.logger as any ); return { service, defaults }; }; -const probedUrls = (defaults: { networkService: { requestStatus: unknown } }) => - (defaults.networkService.requestStatus as jest.Mock).mock.calls.map((call) => (call[0] as any).url); +const probedUrls = (defaults: { providerRegistry: { probe: unknown } }) => + (defaults.providerRegistry.probe as jest.Mock).mock.calls.map((call) => (call[0] as any).url); // ── Tests ──────────────────────────────────────────────────────────────────── @@ -112,7 +112,7 @@ describe("EgressService", () => { await service.probeTargets(["1.1.1.1", "8.8.8.8:53", "https://example.com/health", "[2606:4700::1111]:443", "2606:4700::1111"]); - const monitors = (defaults.networkService.requestStatus as jest.Mock).mock.calls.map((call) => call[0] as any); + const monitors = (defaults.providerRegistry.probe as jest.Mock).mock.calls.map((call) => call[0] as any); expect(monitors).toEqual([ expect.objectContaining({ type: "ping", url: "1.1.1.1" }), expect.objectContaining({ type: "port", url: "8.8.8.8", port: 53 }), @@ -129,13 +129,13 @@ describe("EgressService", () => { await service.probeTargets(["https://example.com/health"]); expect(resolve).toHaveBeenCalledWith(expect.objectContaining({ type: "http", proxyMode: "inherit" })); - expect(defaults.networkService.requestStatus).toHaveBeenCalledWith(expect.objectContaining({ type: "http" }), { + expect(defaults.providerRegistry.probe).toHaveBeenCalledWith(expect.objectContaining({ type: "http" }), { proxyUrl: "http://proxy.internal:3128", }); }); it("reports reachable only when the provider says status is true", async () => { - const { service } = createService({ networkService: { requestStatus: statusFor(["1.1.1.1"]) } }); + const { service } = createService({ providerRegistry: { probe: statusFor(["1.1.1.1"]) } }); const results = await service.probeTargets(["1.1.1.1", "8.8.8.8"]); @@ -146,7 +146,7 @@ describe("EgressService", () => { }); it("treats any HTTP response, even 4xx/5xx, as reachable but a transport failure as unreachable", async () => { - const requestStatus = jest.fn().mockImplementation(async (monitor: any) => ({ + const probe = jest.fn().mockImplementation(async (monitor: any) => ({ monitorId: monitor.id, teamId: monitor.teamId, type: monitor.type, @@ -155,7 +155,7 @@ describe("EgressService", () => { message: monitor.url.includes("maintenance") ? "Service Unavailable" : "ECONNREFUSED", responseTime: 3, })); - const { service } = createService({ networkService: { requestStatus } }); + const { service } = createService({ providerRegistry: { probe } }); const results = await service.probeTargets(["https://example.com/maintenance", "https://example.com/refused", "8.8.8.8:53"]); @@ -172,11 +172,11 @@ describe("EgressService", () => { const results = await service.probeTargets(["[1.1.1.1"]); expect(results).toEqual([expect.objectContaining({ target: "[1.1.1.1", reachable: false, message: "Invalid egress target" })]); - expect(defaults.networkService.requestStatus).not.toHaveBeenCalled(); + expect(defaults.providerRegistry.probe).not.toHaveBeenCalled(); }); it("counts a thrown provider error as unreachable", async () => { - const { service } = createService({ networkService: { requestStatus: jest.fn().mockRejectedValue(new Error("ENETUNREACH")) } }); + const { service } = createService({ providerRegistry: { probe: jest.fn().mockRejectedValue(new Error("ENETUNREACH")) } }); const results = await service.probeTargets(["1.1.1.1"]); @@ -185,7 +185,7 @@ describe("EgressService", () => { it("counts a hung provider as unreachable once the probe timeout elapses", async () => { jest.useFakeTimers(); - const { service } = createService({ networkService: { requestStatus: jest.fn().mockReturnValue(new Promise(() => {})) } }); + const { service } = createService({ providerRegistry: { probe: jest.fn().mockReturnValue(new Promise(() => {})) } }); const pending = service.probeTargets(["1.1.1.1"]); await jest.advanceTimersByTimeAsync(5000); @@ -206,13 +206,13 @@ describe("EgressService", () => { const result = await service.assessAfterFailure(); expect(result).toBeNull(); - expect(defaults.networkService.requestStatus).not.toHaveBeenCalled(); + expect(defaults.providerRegistry.probe).not.toHaveBeenCalled(); expect(defaults.egressStateRepository.findSingleton).not.toHaveBeenCalled(); expect(defaults.egressStateRepository.recordProbe).not.toHaveBeenCalled(); }); it("returns ok and records the probe when any target is reachable", async () => { - const { service, defaults } = createService({ networkService: { requestStatus: statusFor(["8.8.8.8"]) } }); + const { service, defaults } = createService({ providerRegistry: { probe: statusFor(["8.8.8.8"]) } }); const result = await service.assessAfterFailure(); @@ -227,7 +227,7 @@ describe("EgressService", () => { }); it("marks degraded, warns, schedules the recovery job and returns degraded when every target is unreachable", async () => { - const { service, defaults } = createService({ networkService: { requestStatus: statusFor([]) } }); + const { service, defaults } = createService({ providerRegistry: { probe: statusFor([]) } }); const result = await service.assessAfterFailure(); @@ -255,7 +255,7 @@ describe("EgressService", () => { it("still returns degraded and leaves the job scheduled when another worker performed the transition first", async () => { const { service, defaults } = createService({ - networkService: { requestStatus: statusFor([]) }, + providerRegistry: { probe: statusFor([]) }, egressStateRepository: degradedRepository({ findSingleton: jest.fn().mockResolvedValue(makeState()), markDegraded: jest.fn().mockResolvedValue(null), @@ -271,7 +271,7 @@ describe("EgressService", () => { it("does not mark degraded when the recovery job cannot be scheduled", async () => { const { service, defaults } = createService({ - networkService: { requestStatus: statusFor([]) }, + providerRegistry: { probe: statusFor([]) }, jobsRepository: { upsertJob: jest.fn(), upsertCleanupJob: jest.fn().mockRejectedValue(new Error("db down")), @@ -289,7 +289,7 @@ describe("EgressService", () => { const result = await service.assessAfterFailure(); expect(result).toBe("degraded"); - expect(defaults.networkService.requestStatus).not.toHaveBeenCalled(); + expect(defaults.providerRegistry.probe).not.toHaveBeenCalled(); expect(defaults.egressStateRepository.markDegraded).not.toHaveBeenCalled(); expect(defaults.egressStateRepository.recordProbe).not.toHaveBeenCalled(); expect(defaults.jobsRepository.upsertJob).toHaveBeenCalledWith(expect.objectContaining({ id: "egress", intervalMs: 5_000 })); @@ -322,19 +322,19 @@ describe("EgressService", () => { describe("shared assessment", () => { it("shares one probe between concurrent failures", async () => { - const { service, defaults } = createService({ networkService: { requestStatus: statusFor(["8.8.8.8"]) } }); + const { service, defaults } = createService({ providerRegistry: { probe: statusFor(["8.8.8.8"]) } }); const results = await Promise.all([service.assessAfterFailure(), service.assessAfterFailure(), service.assessAfterFailure()]); expect(results).toEqual(["ok", "ok", "ok"]); expect(defaults.settingsService.getCachedDBSettings).toHaveBeenCalledTimes(1); - expect(defaults.networkService.requestStatus).toHaveBeenCalledTimes(2); // one call per target, once + expect(defaults.providerRegistry.probe).toHaveBeenCalledTimes(2); // one call per target, once expect(defaults.egressStateRepository.recordProbe).toHaveBeenCalledTimes(1); }); it("keeps sharing an in-flight assessment that outlives the reuse window", async () => { jest.useFakeTimers(); - const { service, defaults } = createService({ networkService: { requestStatus: jest.fn().mockReturnValue(new Promise(() => {})) } }); + const { service, defaults } = createService({ providerRegistry: { probe: jest.fn().mockReturnValue(new Promise(() => {})) } }); const first = service.assessAfterFailure(); await jest.advanceTimersByTimeAsync(4_900); // probes still hanging, past the 5 s reuse window from the first call @@ -342,12 +342,12 @@ describe("EgressService", () => { await jest.advanceTimersByTimeAsync(200); // probe timeout fires, both settle expect(await Promise.all([first, second])).toEqual(["degraded", "degraded"]); - expect(defaults.networkService.requestStatus).toHaveBeenCalledTimes(2); // one round of two targets, not two rounds + expect(defaults.providerRegistry.probe).toHaveBeenCalledTimes(2); // one round of two targets, not two rounds }); it("reuses a settled assessment within the reuse window and probes again after it", async () => { jest.useFakeTimers(); - const { service, defaults } = createService({ networkService: { requestStatus: statusFor(["8.8.8.8"]) } }); + const { service, defaults } = createService({ providerRegistry: { probe: statusFor(["8.8.8.8"]) } }); await service.assessAfterFailure(); await jest.advanceTimersByTimeAsync(1_000); @@ -385,7 +385,7 @@ describe("EgressService", () => { it("marks recovered, releases the job and logs when a target becomes reachable", async () => { const { service, defaults } = createService({ egressStateRepository: degradedRepository(), - networkService: { requestStatus: statusFor(["1.1.1.1"]) }, + providerRegistry: { probe: statusFor(["1.1.1.1"]) }, }); const job = makeJob({ nextScheduledAt: 123_456 }); @@ -403,7 +403,7 @@ describe("EgressService", () => { it("releases the job but does not log a recovery when another process already recorded it", async () => { const { service, defaults } = createService({ egressStateRepository: degradedRepository({ markRecovered: jest.fn().mockResolvedValue(null) }), - networkService: { requestStatus: statusFor(["1.1.1.1"]) }, + providerRegistry: { probe: statusFor(["1.1.1.1"]) }, }); await service.checkRecovery(makeJob()); @@ -417,7 +417,7 @@ describe("EgressService", () => { it("records the probe and keeps the job while every target stays unreachable", async () => { const { service, defaults } = createService({ egressStateRepository: degradedRepository(), - networkService: { requestStatus: statusFor([]) }, + providerRegistry: { probe: statusFor([]) }, }); await service.checkRecovery(makeJob()); @@ -432,7 +432,7 @@ describe("EgressService", () => { await service.checkRecovery(makeJob()); - expect(defaults.networkService.requestStatus).not.toHaveBeenCalled(); + expect(defaults.providerRegistry.probe).not.toHaveBeenCalled(); expect(defaults.egressStateRepository.markRecovered).not.toHaveBeenCalled(); expect(defaults.jobsRepository.deleteGlobalJobIfUnchanged).toHaveBeenCalledWith("egress", 1_000); }); @@ -445,7 +445,7 @@ describe("EgressService", () => { await service.checkRecovery(makeJob()); - expect(defaults.networkService.requestStatus).not.toHaveBeenCalled(); + expect(defaults.providerRegistry.probe).not.toHaveBeenCalled(); expect(defaults.jobsRepository.deleteGlobalJobIfUnchanged).toHaveBeenCalledWith("egress", 1_000); }); diff --git a/server/test/unit/services/networkService.test.ts b/server/test/unit/services/networkService.test.ts deleted file mode 100644 index b33983bf9..000000000 --- a/server/test/unit/services/networkService.test.ts +++ /dev/null @@ -1,482 +0,0 @@ -import { describe, expect, it, jest } from "@jest/globals"; -import { NetworkService } from "../../../src/service/networkService.ts"; -import { createMockLogger } from "../../helpers/createMockLogger.ts"; -import { NETWORK_ERROR } from "../../../src/types/network.ts"; -import type { IStatusProvider } from "../../../src/service/network/IStatusProvider.ts"; -import type { Monitor, MonitorType } from "../../../src/domain/monitors/monitor.type.ts"; -import type { AxiosStatic } from "axios"; - -// ── Helpers ────────────────────────────────────────────────────────────────── - -const createMockAxios = () => - ({ - post: jest.fn(), - }) as unknown as jest.Mocked; - -const createMockProvider = (type: string, result?: unknown) => { - const provider: jest.Mocked> = { - type, - supports: jest.fn((t: MonitorType) => t === type) as any, - handle: jest.fn().mockResolvedValue( - result ?? { - monitorId: "mon-1", - teamId: "team-1", - type, - status: true, - code: 200, - message: "OK", - } - ), - }; - return provider; -}; - -const createService = (providers: IStatusProvider[] = []) => { - const logger = createMockLogger(); - const axios = createMockAxios(); - const service = new NetworkService(axios, logger as any, providers); - return { service, logger, axios }; -}; - -// ── Tests ──────────────────────────────────────────────────────────────────── - -describe("NetworkService", () => { - // ── Static / instance properties ───────────────────────────────────────── - - // ── requestStatus ──────────────────────────────────────────────────────── - - describe("requestStatus", () => { - it("delegates to the matching provider", async () => { - const provider = createMockProvider("http"); - const { service } = createService([provider]); - const monitor = { type: "http", _id: "mon-1" } as unknown as Monitor & { type: "http" }; - - const result = await service.requestStatus(monitor); - - expect(provider.supports).toHaveBeenCalledWith("http"); - expect(provider.handle).toHaveBeenCalledWith(monitor, undefined); - expect(result).toEqual( - expect.objectContaining({ - monitorId: "mon-1", - status: true, - code: 200, - }) - ); - }); - - it("selects the correct provider when multiple are registered", async () => { - const httpProvider = createMockProvider("http"); - const pingProvider = createMockProvider("ping"); - const { service } = createService([httpProvider, pingProvider]); - const monitor = { type: "ping", _id: "mon-2" } as unknown as Monitor & { type: "ping" }; - - await service.requestStatus(monitor); - - expect(httpProvider.handle).not.toHaveBeenCalled(); - expect(pingProvider.handle).toHaveBeenCalledWith(monitor, undefined); - }); - - it("forwards the check context to the provider", async () => { - const provider = createMockProvider("http"); - const { service } = createService([provider]); - const monitor = { type: "http", _id: "mon-1" } as unknown as Monitor & { type: "http" }; - const ctx = { proxyUrl: "http://proxy.example.com:8080" }; - - await service.requestStatus(monitor, ctx); - - expect(provider.handle).toHaveBeenCalledWith(monitor, ctx); - }); - - it("returns unsupported-type response when no provider matches", async () => { - const { service } = createService([]); - const monitor = { type: "unknown_type" as any, _id: "mon-1" } as unknown as Monitor & { type: any }; - - const result = await service.requestStatus(monitor); - - expect(result).toEqual({ - monitorId: "unknown", - teamId: "unknown", - type: "unknown", - status: false, - code: NETWORK_ERROR, - message: "Unsupported type: unknown_type", - }); - }); - }); - - // ── requestWebhook ─────────────────────────────────────────────────────── - - describe("requestWebhook", () => { - it("sends a POST and returns success response", async () => { - const { service, axios } = createService(); - (axios.post as jest.Mock).mockResolvedValue({ status: 200, data: { ok: true } }); - - const result = await service.requestWebhook("slack", "https://hooks.example.com", { text: "hello" }); - - expect(axios.post).toHaveBeenCalledWith("https://hooks.example.com", { text: "hello" }, { headers: { "Content-Type": "application/json" } }); - expect(result).toEqual({ - type: "webhook", - status: true, - code: 200, - message: "Successfully sent slack notification", - payload: { ok: true }, - }); - }); - - it("returns failure with response data on axios error with response", async () => { - const { service, axios, logger } = createService(); - const axiosError = Object.assign(new Error("Request failed"), { - response: { status: 403, data: { error: "forbidden" } }, - }); - (axios.post as jest.Mock).mockRejectedValue(axiosError); - - const result = await service.requestWebhook("discord", "https://hooks.example.com", {}); - - expect(result).toEqual({ - type: "webhook", - status: false, - code: 403, - message: "Failed to send discord notification", - payload: { error: "forbidden" }, - }); - expect(logger.warn).toHaveBeenCalledWith( - expect.objectContaining({ - message: "Request failed", - service: "NetworkService", - method: "requestWebhook", - }) - ); - }); - - it("returns failure with NETWORK_ERROR when axios error has no response status", async () => { - const { service, axios } = createService(); - const axiosError = Object.assign(new Error("timeout"), { - response: { status: undefined, data: undefined }, - }); - (axios.post as jest.Mock).mockRejectedValue(axiosError); - - const result = await service.requestWebhook("slack", "https://hooks.example.com", {}); - - expect(result.code).toBe(NETWORK_ERROR); - }); - - it("returns failure with NETWORK_ERROR on non-axios error (no response property)", async () => { - const { service, axios } = createService(); - (axios.post as jest.Mock).mockRejectedValue(new Error("DNS failure")); - - const result = await service.requestWebhook("slack", "https://hooks.example.com", {}); - - expect(result).toEqual({ - type: "webhook", - status: false, - code: NETWORK_ERROR, - message: "Failed to send slack notification", - }); - }); - - it("handles non-Error thrown values", async () => { - const { service, axios, logger } = createService(); - (axios.post as jest.Mock).mockRejectedValue("string error"); - - const result = await service.requestWebhook("slack", "https://hooks.example.com", {}); - - expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "string error" })); - expect(result).toEqual({ - type: "webhook", - status: false, - code: NETWORK_ERROR, - message: "Failed to send slack notification", - }); - }); - - it("returns NETWORK_ERROR when error has response key but response is undefined", async () => { - const { service, axios } = createService(); - const axiosError = Object.assign(new Error("fail"), { response: undefined }); - (axios.post as jest.Mock).mockRejectedValue(axiosError); - - const result = await service.requestWebhook("slack", "https://hooks.example.com", {}); - - expect(result.code).toBe(NETWORK_ERROR); - expect(result.payload).toBeUndefined(); - }); - }); - - // ── requestPagerDuty ───────────────────────────────────────────────────── - - describe("requestPagerDuty", () => { - const args = { - message: "Server down", - routingKey: "routing-key-123", - monitorUrl: "https://monitor.example.com", - }; - - it("sends correct payload and returns true on success", async () => { - const { service, axios } = createService(); - (axios.post as jest.Mock).mockResolvedValue({ data: { status: "success" } }); - - const result = await service.requestPagerDuty(args); - - expect(result).toBe(true); - expect(axios.post).toHaveBeenCalledWith( - "https://events.pagerduty.com/v2/enqueue", - expect.objectContaining({ - routing_key: "routing-key-123", - event_action: "trigger", - payload: expect.objectContaining({ - summary: "Server down", - severity: "critical", - source: "https://monitor.example.com", - }), - }) - ); - }); - - it("returns false when response status is not success", async () => { - const { service, axios } = createService(); - (axios.post as jest.Mock).mockResolvedValue({ data: { status: "invalid event" } }); - - const result = await service.requestPagerDuty(args); - - expect(result).toBe(false); - }); - - it("returns false when response data is undefined", async () => { - const { service, axios } = createService(); - (axios.post as jest.Mock).mockResolvedValue({ data: undefined }); - - const result = await service.requestPagerDuty(args); - - expect(result).toBe(false); - }); - - it("throws AppError on axios failure with response data", async () => { - const { service, axios } = createService(); - const axiosError = Object.assign(new Error("Request failed"), { - response: { data: { message: "invalid routing key" } }, - }); - (axios.post as jest.Mock).mockRejectedValue(axiosError); - - await expect(service.requestPagerDuty(args)).rejects.toThrow("Request failed"); - }); - - it("throws AppError on network error without response", async () => { - const { service, axios } = createService(); - (axios.post as jest.Mock).mockRejectedValue(new Error("ECONNREFUSED")); - - await expect(service.requestPagerDuty(args)).rejects.toThrow("ECONNREFUSED"); - }); - - it("throws AppError with default message for non-Error thrown value", async () => { - const { service, axios } = createService(); - (axios.post as jest.Mock).mockRejectedValue(null); - - await expect(service.requestPagerDuty(args)).rejects.toThrow("null"); - }); - - it("uses fallback message when Error has empty string message", async () => { - const { service, axios } = createService(); - (axios.post as jest.Mock).mockRejectedValue(new Error("")); - - try { - await service.requestPagerDuty(args); - expect.unreachable("should have thrown"); - } catch (err: any) { - expect(err.message).toBe("Error sending PagerDuty notification"); - } - }); - - it("includes responseData when non-Error object has response property", async () => { - const { service, axios } = createService(); - const errorObj = { response: { data: { detail: "bad key" } } }; - (axios.post as jest.Mock).mockRejectedValue(errorObj); - - try { - await service.requestPagerDuty(args); - expect.unreachable("should have thrown"); - } catch (err: any) { - expect(err.details).toEqual({ responseData: { detail: "bad key" } }); - } - }); - - it("sets responseData to undefined when error is a primitive (non-object)", async () => { - const { service, axios } = createService(); - (axios.post as jest.Mock).mockRejectedValue(42); - - try { - await service.requestPagerDuty(args); - expect.unreachable("should have thrown"); - } catch (err: any) { - expect(err.details).toEqual({ responseData: undefined }); - } - }); - - it("sets responseData to undefined when object has response key set to undefined", async () => { - const { service, axios } = createService(); - (axios.post as jest.Mock).mockRejectedValue({ response: undefined }); - - try { - await service.requestPagerDuty(args); - expect.unreachable("should have thrown"); - } catch (err: any) { - expect(err.details).toEqual({ responseData: undefined }); - } - }); - - it("includes responseData in AppError details when available", async () => { - const { service, axios } = createService(); - const axiosError = Object.assign(new Error("Bad request"), { - response: { data: { errors: ["invalid key"] } }, - }); - (axios.post as jest.Mock).mockRejectedValue(axiosError); - - try { - await service.requestPagerDuty(args); - expect.unreachable("should have thrown"); - } catch (err: any) { - expect(err.details).toEqual({ - responseData: { errors: ["invalid key"] }, - }); - } - }); - - it("sets responseData to undefined in AppError when error has no response", async () => { - const { service, axios } = createService(); - (axios.post as jest.Mock).mockRejectedValue(new Error("timeout")); - - try { - await service.requestPagerDuty(args); - expect.unreachable("should have thrown"); - } catch (err: any) { - expect(err.details).toEqual({ responseData: undefined }); - } - }); - }); - - // ── requestMatrix ──────────────────────────────────────────────────────── - - describe("requestMatrix", () => { - const args = { - homeserverUrl: "https://matrix.example.com", - accessToken: "token-abc", - roomId: "!room:example.com", - message: "Alert", - }; - - it("sends correct payload and returns success response", async () => { - const { service, axios } = createService(); - (axios.post as jest.Mock).mockResolvedValue({ status: 200, data: { event_id: "$abc" } }); - - const result = await service.requestMatrix(args); - - expect(result).toEqual({ - status: true, - code: 200, - message: "Successfully sent Matrix notification", - }); - expect(axios.post).toHaveBeenCalledWith( - "https://matrix.example.com/_matrix/client/v3/rooms/!room:example.com/send/m.room.message?access_token=token-abc", - { - msgtype: "m.text", - body: "Alert", - format: "org.matrix.custom.html", - formatted_body: "Alert", - }, - { headers: { "Content-Type": "application/json" } } - ); - }); - - it("returns failure with response data on axios error with response (Error instance)", async () => { - const { service, axios, logger } = createService(); - const axiosError = Object.assign(new Error("Forbidden"), { - response: { status: 403, data: { errcode: "M_FORBIDDEN" } }, - }); - (axios.post as jest.Mock).mockRejectedValue(axiosError); - - const result = await service.requestMatrix(args); - - expect(result).toEqual({ - status: false, - code: 403, - message: "Failed to send Matrix notification", - payload: { errcode: "M_FORBIDDEN" }, - }); - expect(logger.warn).toHaveBeenCalledWith( - expect.objectContaining({ - message: "Forbidden", - service: "NetworkService", - method: "requestMatrix", - }) - ); - }); - - it("returns NETWORK_ERROR when axios error response has no status", async () => { - const { service, axios } = createService(); - const axiosError = Object.assign(new Error("timeout"), { - response: { status: 0, data: undefined }, - }); - (axios.post as jest.Mock).mockRejectedValue(axiosError); - - const result = await service.requestMatrix(args); - - expect(result.code).toBe(NETWORK_ERROR); - }); - - it("returns failure with NETWORK_ERROR on Error without response property", async () => { - const { service, axios, logger } = createService(); - (axios.post as jest.Mock).mockRejectedValue(new Error("DNS failure")); - - const result = await service.requestMatrix(args); - - expect(result).toEqual({ - status: false, - code: NETWORK_ERROR, - message: "Failed to send Matrix notification", - }); - // First warn call is from the Error instanceof branch - expect(logger.warn).toHaveBeenCalledTimes(2); - }); - - it("returns failure with NETWORK_ERROR on non-Error thrown value", async () => { - const { service, axios, logger } = createService(); - (axios.post as jest.Mock).mockRejectedValue("string error"); - - const result = await service.requestMatrix(args); - - expect(result).toEqual({ - status: false, - code: NETWORK_ERROR, - message: "Failed to send Matrix notification", - }); - expect(logger.warn).toHaveBeenCalledWith( - expect.objectContaining({ - message: "string error", - service: "NetworkService", - method: "requestMatrix", - }) - ); - }); - - it("returns NETWORK_ERROR when Error has response key but response is undefined", async () => { - const { service, axios } = createService(); - const axiosError = Object.assign(new Error("fail"), { response: undefined }); - (axios.post as jest.Mock).mockRejectedValue(axiosError); - - const result = await service.requestMatrix(args); - - expect(result.code).toBe(NETWORK_ERROR); - }); - - it("returns NETWORK_ERROR when Error response has undefined status", async () => { - const { service, axios } = createService(); - const axiosError = Object.assign(new Error("fail"), { - response: { status: undefined, data: { error: "unknown" } }, - }); - (axios.post as jest.Mock).mockRejectedValue(axiosError); - - const result = await service.requestMatrix(args); - - expect(result.code).toBe(NETWORK_ERROR); - expect(result.payload).toEqual({ error: "unknown" }); - }); - }); -}); diff --git a/server/test/unit/services/providerRegistry.test.ts b/server/test/unit/services/providerRegistry.test.ts new file mode 100644 index 000000000..cb352c637 --- /dev/null +++ b/server/test/unit/services/providerRegistry.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import { ProviderRegistry } from "../../../src/service/networkProviders/providerRegistry.ts"; +import { NETWORK_ERROR } from "../../../src/types/network.ts"; +import type { IStatusProvider } from "../../../src/service/networkProviders/IStatusProvider.ts"; +import type { Monitor, MonitorType } from "../../../src/domain/monitors/monitor.type.ts"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const createMockProvider = (type: string, result?: unknown) => { + const provider: jest.Mocked> = { + type, + supports: jest.fn((t: MonitorType) => t === type) as any, + handle: jest.fn().mockResolvedValue( + result ?? { + monitorId: "mon-1", + teamId: "team-1", + type, + status: true, + code: 200, + message: "OK", + } + ), + }; + return provider; +}; + +const createRegistry = (providers: IStatusProvider[] = []) => new ProviderRegistry(providers); + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("ProviderRegistry", () => { + describe("probe", () => { + it("delegates to the matching provider", async () => { + const provider = createMockProvider("http"); + const registry = createRegistry([provider]); + const monitor = { type: "http", _id: "mon-1" } as unknown as Monitor & { type: "http" }; + + const result = await registry.probe(monitor); + + expect(provider.supports).toHaveBeenCalledWith("http"); + expect(provider.handle).toHaveBeenCalledWith(monitor, undefined); + expect(result).toEqual( + expect.objectContaining({ + monitorId: "mon-1", + status: true, + code: 200, + }) + ); + }); + + it("selects the correct provider when multiple are registered", async () => { + const httpProvider = createMockProvider("http"); + const pingProvider = createMockProvider("ping"); + const registry = createRegistry([httpProvider, pingProvider]); + const monitor = { type: "ping", _id: "mon-2" } as unknown as Monitor & { type: "ping" }; + + await registry.probe(monitor); + + expect(httpProvider.handle).not.toHaveBeenCalled(); + expect(pingProvider.handle).toHaveBeenCalledWith(monitor, undefined); + }); + + it("forwards the check context to the provider", async () => { + const provider = createMockProvider("http"); + const registry = createRegistry([provider]); + const monitor = { type: "http", _id: "mon-1" } as unknown as Monitor & { type: "http" }; + const ctx = { proxyUrl: "http://proxy.example.com:8080" }; + + await registry.probe(monitor, ctx); + + expect(provider.handle).toHaveBeenCalledWith(monitor, ctx); + }); + + it("returns unsupported-type response when no provider matches", async () => { + const registry = createRegistry([]); + const monitor = { type: "unknown_type" as any, _id: "mon-1" } as unknown as Monitor & { type: any }; + + const result = await registry.probe(monitor); + + expect(result).toEqual({ + monitorId: "unknown", + teamId: "unknown", + type: "unknown", + status: false, + code: NETWORK_ERROR, + message: "Unsupported type: unknown_type", + }); + }); + }); +}); diff --git a/server/test/unit/services/proxyResolver.test.ts b/server/test/unit/services/proxyResolver.test.ts index 7c428aa94..0d9f0a410 100644 --- a/server/test/unit/services/proxyResolver.test.ts +++ b/server/test/unit/services/proxyResolver.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, jest } from "@jest/globals"; -import { ProxyResolver, buildProxyUrl } from "../../../src/service/network/ProxyResolver.ts"; +import { ProxyResolver, buildProxyUrl } from "../../../src/service/networkProviders/ProxyResolver.ts"; import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; import type { Proxy } from "../../../src/domain/proxies/proxy.type.ts"; import { createMockLogger } from "../../helpers/createMockLogger.ts"; diff --git a/server/test/unit/services/workerPipeline.test.ts b/server/test/unit/services/workerPipeline.test.ts index 7410c04bc..54779c7e9 100644 --- a/server/test/unit/services/workerPipeline.test.ts +++ b/server/test/unit/services/workerPipeline.test.ts @@ -92,7 +92,7 @@ const createPipeline = (overrides?: Record) => { toCheck: jest.fn().mockReturnValue({ id: "check-1" }), createChecks: jest.fn().mockResolvedValue([]), }, - networkService: { requestStatus: jest.fn().mockResolvedValue({ monitorId: "m1", status: true, code: 200, message: "OK" }) }, + providerRegistry: { probe: jest.fn().mockResolvedValue({ monitorId: "m1", status: true, code: 200, message: "OK" }) }, proxyResolver: { resolve: jest.fn().mockResolvedValue(undefined) }, bufferService: { addToBuffer: jest.fn(), addDockerLogToBuffer: jest.fn() }, dockerLogsService: { buildDockerLogs: jest.fn().mockResolvedValue([]) }, @@ -115,7 +115,7 @@ describe("WorkerPipeline", () => { await pipeline.handleCheck(makeJob()); expect(deps.monitorsRepository.findByIdLean).toHaveBeenCalledWith("m1"); - expect(deps.networkService.requestStatus).toHaveBeenCalled(); + expect(deps.providerRegistry.probe).toHaveBeenCalled(); expect(deps.bufferService.addToBuffer).toHaveBeenCalledWith({ id: "check-1" }); }); @@ -125,7 +125,7 @@ describe("WorkerPipeline", () => { await pipeline.handleCheck(makeJob({ refId: null })); expect(deps.monitorsRepository.findByIdLean).not.toHaveBeenCalled(); - expect(deps.networkService.requestStatus).not.toHaveBeenCalled(); + expect(deps.providerRegistry.probe).not.toHaveBeenCalled(); }); it("does nothing when the monitor no longer exists", async () => { @@ -135,7 +135,7 @@ describe("WorkerPipeline", () => { await pipeline.handleCheck(makeJob()); - expect(deps.networkService.requestStatus).not.toHaveBeenCalled(); + expect(deps.providerRegistry.probe).not.toHaveBeenCalled(); }); }); @@ -156,7 +156,7 @@ describe("WorkerPipeline", () => { expect(result).toBeNull(); expect(deps.monitorsRepository.updateById).toHaveBeenCalledWith("m1", "team", { status: "maintenance", statusWindow: [] }); - expect(deps.networkService.requestStatus).not.toHaveBeenCalled(); + expect(deps.providerRegistry.probe).not.toHaveBeenCalled(); expect(deps.bufferService.addToBuffer).not.toHaveBeenCalled(); }); @@ -185,7 +185,7 @@ describe("WorkerPipeline", () => { // The exit used to be a side effect of evaluating the resulting check, which the degraded-egress // short-circuit skips. Left implicit, a monitor could sit in "maintenance" for a whole egress outage. expect(deps.monitorsRepository.updateById).toHaveBeenCalledWith("m1", "team", { status: "initializing" }); - expect(deps.networkService.requestStatus).toHaveBeenCalled(); + expect(deps.providerRegistry.probe).toHaveBeenCalled(); }); it("does not touch the status of a monitor that was not in maintenance", async () => { @@ -203,12 +203,12 @@ describe("WorkerPipeline", () => { await pipeline.produce(makeMonitor()); - expect(deps.networkService.requestStatus).toHaveBeenCalled(); + expect(deps.providerRegistry.probe).toHaveBeenCalled(); }); // ── proxy resolution ────────────────────────────────────────────────── - it("passes the resolved proxy url into requestStatus", async () => { + it("passes the resolved proxy url into probe", async () => { const { pipeline, deps } = createPipeline({ proxyResolver: { resolve: jest.fn().mockResolvedValue("http://proxy.example.com:8080") }, }); @@ -216,7 +216,7 @@ describe("WorkerPipeline", () => { await pipeline.produce(monitor); - expect(deps.networkService.requestStatus).toHaveBeenCalledWith(monitor, { proxyUrl: "http://proxy.example.com:8080" }); + expect(deps.providerRegistry.probe).toHaveBeenCalledWith(monitor, { proxyUrl: "http://proxy.example.com:8080" }); }); it("still produces a check when the resolver returns undefined", async () => { @@ -225,7 +225,7 @@ describe("WorkerPipeline", () => { const result = await pipeline.produce(monitor); - expect(deps.networkService.requestStatus).toHaveBeenCalledWith(monitor, { proxyUrl: undefined }); + expect(deps.providerRegistry.probe).toHaveBeenCalledWith(monitor, { proxyUrl: undefined }); expect(result).toEqual({ status: expect.objectContaining({ monitorId: "m1" }), check: { id: "check-1" } }); }); @@ -240,7 +240,7 @@ describe("WorkerPipeline", () => { await pipeline.produce(monitor); expect(deps.monitorsRepository.findDockerTlsKeyById).toHaveBeenCalledWith("m1"); - expect(deps.networkService.requestStatus).toHaveBeenCalledWith(monitor, { proxyUrl: undefined, dockerTlsKey: "v1.abc123.iv.tag.data" }); + expect(deps.providerRegistry.probe).toHaveBeenCalledWith(monitor, { proxyUrl: undefined, dockerTlsKey: "v1.abc123.iv.tag.data" }); }); it("does not fetch a key for a docker monitor without one set", async () => { @@ -250,7 +250,7 @@ describe("WorkerPipeline", () => { await pipeline.produce(monitor); expect(deps.monitorsRepository.findDockerTlsKeyById).not.toHaveBeenCalled(); - expect(deps.networkService.requestStatus).toHaveBeenCalledWith(monitor, { proxyUrl: undefined, dockerTlsKey: undefined }); + expect(deps.providerRegistry.probe).toHaveBeenCalledWith(monitor, { proxyUrl: undefined, dockerTlsKey: undefined }); }); it("does not fetch a key for non-docker monitors even if the flag is set", async () => { @@ -268,14 +268,14 @@ describe("WorkerPipeline", () => { await pipeline.produce(monitor); expect(deps.monitorsRepository.findDockerTlsKeyById).toHaveBeenCalledWith("m1"); - expect(deps.networkService.requestStatus).toHaveBeenCalledWith(monitor, { proxyUrl: undefined, dockerTlsKey: undefined }); + expect(deps.providerRegistry.probe).toHaveBeenCalledWith(monitor, { proxyUrl: undefined, dockerTlsKey: undefined }); }); // ── acquire / record ────────────────────────────────────────────────── it("throws when the network response is null", async () => { const { pipeline } = createPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue(null) }, + providerRegistry: { probe: jest.fn().mockResolvedValue(null) }, }); await expect(pipeline.produce(makeMonitor())).rejects.toThrow("No network response"); @@ -297,7 +297,7 @@ describe("WorkerPipeline", () => { const status = { monitorId: "m1", status: false, code: 500, message: "Error" }; const check = { id: "check-1" }; const { pipeline, deps } = createPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue(status) }, + providerRegistry: { probe: jest.fn().mockResolvedValue(status) }, checkService: { toCheck: jest.fn().mockReturnValue(check) }, }); @@ -311,7 +311,7 @@ describe("WorkerPipeline", () => { const status = { type: "docker", monitorId: "m1", teamId: "team", status: true, code: 200, message: "OK", payload: {} }; const dockerLogs = [{ id: "docker-log-1" }, { id: "docker-log-2" }]; const { pipeline, deps } = createPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue(status) }, + providerRegistry: { probe: jest.fn().mockResolvedValue(status) }, dockerLogsService: { buildDockerLogs: jest.fn().mockResolvedValue(dockerLogs) }, }); @@ -354,7 +354,7 @@ describe("WorkerPipeline", () => { ]) { const check: Record = { id: "check-1" }; const { pipeline, deps } = createPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue(status) }, + providerRegistry: { probe: jest.fn().mockResolvedValue(status) }, checkService: { toCheck: jest.fn().mockReturnValue(check) }, egressService: { assessAfterFailure: jest.fn().mockResolvedValue("degraded") }, }); @@ -370,7 +370,7 @@ describe("WorkerPipeline", () => { it("consults egress for a timeout or network error", async () => { const check: Record = { id: "check-1" }; const { pipeline, deps } = createPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue(failingStatus) }, + providerRegistry: { probe: jest.fn().mockResolvedValue(failingStatus) }, checkService: { toCheck: jest.fn().mockReturnValue(check) }, egressService: { assessAfterFailure: jest.fn().mockResolvedValue("degraded") }, }); @@ -385,7 +385,7 @@ describe("WorkerPipeline", () => { for (const url of ["unix:///var/run/docker.sock", "/var/run/docker.sock"]) { const check: Record = { id: "check-1" }; const { pipeline, deps } = createPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue({ ...failingStatus, type: "docker" }) }, + providerRegistry: { probe: jest.fn().mockResolvedValue({ ...failingStatus, type: "docker" }) }, checkService: { toCheck: jest.fn().mockReturnValue(check) }, egressService: { assessAfterFailure: jest.fn().mockResolvedValue("degraded") }, }); @@ -406,7 +406,7 @@ describe("WorkerPipeline", () => { ]) { const check: Record = { id: "check-1" }; const { pipeline, deps } = createPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue({ ...failingStatus, type: monitor.type }) }, + providerRegistry: { probe: jest.fn().mockResolvedValue({ ...failingStatus, type: monitor.type }) }, checkService: { toCheck: jest.fn().mockReturnValue(check) }, egressService: { assessAfterFailure: jest.fn().mockResolvedValue("degraded") }, }); @@ -424,7 +424,7 @@ describe("WorkerPipeline", () => { // them from a connection failure. const check: Record = { id: "check-1" }; const { pipeline, deps } = createPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue({ ...failingStatus, peerResponded: true }) }, + providerRegistry: { probe: jest.fn().mockResolvedValue({ ...failingStatus, peerResponded: true }) }, checkService: { toCheck: jest.fn().mockReturnValue(check) }, egressService: { assessAfterFailure: jest.fn().mockResolvedValue("degraded") }, }); @@ -438,7 +438,7 @@ describe("WorkerPipeline", () => { it("consults egress when the provider reports that nothing answered, despite an HTTP-shaped code", async () => { const check: Record = { id: "check-1" }; const { pipeline, deps } = createPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue({ ...failingStatus, code: 503, peerResponded: false }) }, + providerRegistry: { probe: jest.fn().mockResolvedValue({ ...failingStatus, code: 503, peerResponded: false }) }, checkService: { toCheck: jest.fn().mockReturnValue(check) }, egressService: { assessAfterFailure: jest.fn().mockResolvedValue("degraded") }, }); @@ -453,7 +453,7 @@ describe("WorkerPipeline", () => { it("flags a failing check as degraded when the egress service reports degraded egress", async () => { const check: Record = { id: "check-1" }; const { pipeline, deps } = createPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue(failingStatus) }, + providerRegistry: { probe: jest.fn().mockResolvedValue(failingStatus) }, checkService: { toCheck: jest.fn().mockReturnValue(check) }, egressService: { assessAfterFailure: jest.fn().mockResolvedValue("degraded") }, }); @@ -472,7 +472,7 @@ describe("WorkerPipeline", () => { it("clears the status window on the first degraded check of a spell", async () => { const { pipeline, deps } = createPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue(failingStatus) }, + providerRegistry: { probe: jest.fn().mockResolvedValue(failingStatus) }, egressService: { assessAfterFailure: jest.fn().mockResolvedValue("degraded") }, }); @@ -483,7 +483,7 @@ describe("WorkerPipeline", () => { it("does not re-write the status window for later degraded checks in the same spell", async () => { const { pipeline, deps } = createPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue(failingStatus) }, + providerRegistry: { probe: jest.fn().mockResolvedValue(failingStatus) }, egressService: { assessAfterFailure: jest.fn().mockResolvedValue("degraded") }, }); @@ -494,7 +494,7 @@ describe("WorkerPipeline", () => { it("leaves the status window alone when the probe found egress fine", async () => { const { pipeline, deps } = createPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue(failingStatus) }, + providerRegistry: { probe: jest.fn().mockResolvedValue(failingStatus) }, egressService: { assessAfterFailure: jest.fn().mockResolvedValue("ok") }, }); @@ -506,7 +506,7 @@ describe("WorkerPipeline", () => { it("flags a failing check as ok when the probe found egress fine", async () => { const check: Record = { id: "check-1" }; const { pipeline } = createPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue(failingStatus) }, + providerRegistry: { probe: jest.fn().mockResolvedValue(failingStatus) }, checkService: { toCheck: jest.fn().mockReturnValue(check) }, egressService: { assessAfterFailure: jest.fn().mockResolvedValue("ok") }, }); @@ -519,7 +519,7 @@ describe("WorkerPipeline", () => { it("leaves the field unset on a failing check when the egress check is disabled", async () => { const check: Record = { id: "check-1" }; const { pipeline, deps } = createPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue(failingStatus) }, + providerRegistry: { probe: jest.fn().mockResolvedValue(failingStatus) }, checkService: { toCheck: jest.fn().mockReturnValue(check) }, });