diff --git a/server/src/config/services.worker.ts b/server/src/config/services.worker.ts index b0bdff2d6..9b4ea8609 100644 --- a/server/src/config/services.worker.ts +++ b/server/src/config/services.worker.ts @@ -26,7 +26,7 @@ import { NotificationReactor } from "@/worker/reactors/reactor.notification.js"; import { IncidentReactor } from "@/worker/reactors/reactor.incident.js"; import { ReactorDispatcher } from "@/worker/reactors/reactor.dispatcher.js"; import { DBQueueWorker } from "@/worker/worker.db-queue.js"; - +import { ProxyResolver } from "@/service/network/ProxyResolver.js"; // Network providers import { PingProvider } from "@/service/network/PingProvider.js"; import { HttpProvider } from "@/service/network/HttpProvider.js"; @@ -64,6 +64,7 @@ export const buildWorker = async (shared: SharedServices, envSettings: EnvConfig incidentsRepository, teamsRepository, maintenanceWindowsRepository, + proxiesRepository, } = shared; // *********************** @@ -93,6 +94,7 @@ export const buildWorker = async (shared: SharedServices, envSettings: EnvConfig dnsProvider, ]); + const proxyResolver = new ProxyResolver(proxiesRepository, settingsService, logger); const bufferService = new BufferService(logger, checkService, geoChecksService, settingsService, jobsRepository); const statusService = new StatusService(logger, monitorsRepository, monitorStatsRepository); const monitorStatusPolicy = new MonitorStatusPolicy(); @@ -110,7 +112,15 @@ export const buildWorker = async (shared: SharedServices, envSettings: EnvConfig // Check producer/evaluator // Handles creating and evaluatiog checks // *********************** - const checkProducer = new CheckProducer(monitorsRepository, maintenanceWindowsRepository, checkService, networkService, bufferService, logger); + const checkProducer = new CheckProducer( + monitorsRepository, + maintenanceWindowsRepository, + checkService, + networkService, + proxyResolver, + bufferService, + logger + ); const checkEvaluator = new CheckEvaluator(statusService, monitorStatusPolicy); const geoCheckPipeline = new GeoChecksPipeline(maintenanceWindowsRepository, geoChecksService, bufferService, logger); diff --git a/server/src/service/network/IStatusProvider.ts b/server/src/service/network/IStatusProvider.ts index 6da91b30f..e8f63f671 100644 --- a/server/src/service/network/IStatusProvider.ts +++ b/server/src/service/network/IStatusProvider.ts @@ -1,8 +1,8 @@ import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { MonitorStatusResponse } from "@/types/network.js"; - +import { CheckContext } from "@/types/network.js"; export interface IStatusProvider { type: string; supports: (type: MonitorType) => boolean; - handle(monitor: Monitor): Promise>; + handle(monitor: Monitor, ctx?: CheckContext): Promise>; } diff --git a/server/src/service/networkService.ts b/server/src/service/networkService.ts index 75ff66bda..69e97b092 100644 --- a/server/src/service/networkService.ts +++ b/server/src/service/networkService.ts @@ -1,5 +1,5 @@ import type { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; -import type { MonitorPayloadMap, MonitorStatusResponse } from "@/types/network.js"; +import type { CheckContext, MonitorPayloadMap, MonitorStatusResponse } from "@/types/network.js"; import type { AxiosStatic } from "axios"; import { AppError } from "@/utils/AppError.js"; import { NETWORK_ERROR } from "@/types/network.js"; @@ -8,7 +8,7 @@ import { IStatusProvider } from "./network/IStatusProvider.js"; const SERVICE_NAME = "NetworkService"; export interface INetworkService { - requestStatus(monitor: Monitor & { type: T }): Promise>; + requestStatus(monitor: Monitor & { type: T }, ctx?: CheckContext): Promise>; requestWebhook( type: string, url: string, @@ -39,12 +39,15 @@ export class NetworkService implements INetworkService { } // Main entry point - async requestStatus(monitor: Monitor & { type: T }): Promise> { + async requestStatus( + monitor: Monitor & { type: T }, + ctx?: CheckContext + ): Promise> { const provider = this.providers.find((p) => p.supports(monitor.type)); if (!provider) { return this.handleUnsupportedType(monitor.type) as Promise>; } - return provider.handle(monitor) as Promise>; + return provider.handle(monitor, ctx) as Promise>; } private async handleUnsupportedType(type: string): Promise { diff --git a/server/src/worker/worker.check-producer.ts b/server/src/worker/worker.check-producer.ts index b73e2c6de..f297d01be 100644 --- a/server/src/worker/worker.check-producer.ts +++ b/server/src/worker/worker.check-producer.ts @@ -9,6 +9,7 @@ import { ICheckService } from "@/domain/checks/check.service.js"; import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js"; import { IMaintenanceWindowsRepository } from "@/domain/maintenance-windows/maintenance-window.repository.interface.js"; import { isWindowActive } from "@/utils/maintenanceWindow.js"; +import { IProxyResolver } from "@/service/network/ProxyResolver.js"; export interface ICheckProducer { produce(monitor: Monitor): Promise<{ status: MonitorStatusResponse; check: Check } | null>; @@ -22,6 +23,7 @@ export class CheckProducer implements ICheckProducer { private maintenanceWindowsRepository: IMaintenanceWindowsRepository, private checkService: ICheckService, private networkService: INetworkService, + private proxyResolver: IProxyResolver, private bufferService: IBufferService, private logger: ILogger ) {} @@ -56,7 +58,8 @@ export class CheckProducer implements ICheckProducer { } // Step 1b: Acquire status - const status = await this.networkService.requestStatus(monitor); + const proxyUrl = await this.proxyResolver.resolve(monitor); + const status = await this.networkService.requestStatus(monitor, { proxyUrl }); if (!status) { throw new Error("No network response"); } diff --git a/server/test/helpers/heartbeatTestHarness.ts b/server/test/helpers/heartbeatTestHarness.ts index 898c3c91e..9c2161790 100644 --- a/server/test/helpers/heartbeatTestHarness.ts +++ b/server/test/helpers/heartbeatTestHarness.ts @@ -127,12 +127,14 @@ export function createHeartbeatTestHarness(): HeartbeatTestHarness { }; const maintenanceWindowsRepo = { findByMonitorId: jest.fn().mockResolvedValue([]) }; + const proxyResolver = { resolve: jest.fn().mockResolvedValue(undefined) }; const checkProducer = new CheckProducer( monitorsRepo as any, maintenanceWindowsRepo as any, checkService as any, networkService as any, + proxyResolver as any, bufferStub as any, logger ); diff --git a/server/test/unit/services/checkProducer.test.ts b/server/test/unit/services/checkProducer.test.ts index dacff3d9b..2a1aa29f1 100644 --- a/server/test/unit/services/checkProducer.test.ts +++ b/server/test/unit/services/checkProducer.test.ts @@ -26,6 +26,7 @@ const createProducer = (overrides?: Record) => { maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([]) }, checkService: { toCheck: jest.fn().mockReturnValue({ id: "check-1" }) }, networkService: { requestStatus: jest.fn().mockResolvedValue({ monitorId: "m1", status: true, code: 200, message: "OK" }) }, + proxyResolver: { resolve: jest.fn().mockResolvedValue(undefined) }, buffer: { addToBuffer: jest.fn() }, ...overrides, }; @@ -34,6 +35,7 @@ const createProducer = (overrides?: Record) => { defaults.maintenanceWindowsRepository as any, defaults.checkService as any, defaults.networkService as any, + defaults.proxyResolver as any, defaults.buffer as any, defaults.logger as any ); @@ -82,6 +84,29 @@ describe("CheckProducer", () => { expect(defaults.networkService.requestStatus).toHaveBeenCalled(); }); + // ── proxy resolution ────────────────────────────────────────────────────── + + it("passes the resolved proxy url into requestStatus", async () => { + const { producer, defaults } = createProducer({ + proxyResolver: { resolve: jest.fn().mockResolvedValue("http://proxy.example.com:8080") }, + }); + const monitor = makeMonitor(); + + await producer.produce(monitor); + + expect(defaults.networkService.requestStatus).toHaveBeenCalledWith(monitor, { proxyUrl: "http://proxy.example.com:8080" }); + }); + + it("still produces a check when the resolver returns undefined", async () => { + const { producer, defaults } = createProducer(); + const monitor = makeMonitor(); + + const result = await producer.produce(monitor); + + expect(defaults.networkService.requestStatus).toHaveBeenCalledWith(monitor, { proxyUrl: undefined }); + expect(result).toEqual({ status: expect.objectContaining({ monitorId: "m1" }), check: { id: "check-1" } }); + }); + // ── acquire / record ────────────────────────────────────────────────────── it("throws when the network response is null", async () => { diff --git a/server/test/unit/services/networkService.test.ts b/server/test/unit/services/networkService.test.ts index 2e6627452..b33983bf9 100644 --- a/server/test/unit/services/networkService.test.ts +++ b/server/test/unit/services/networkService.test.ts @@ -54,7 +54,7 @@ describe("NetworkService", () => { const result = await service.requestStatus(monitor); expect(provider.supports).toHaveBeenCalledWith("http"); - expect(provider.handle).toHaveBeenCalledWith(monitor); + expect(provider.handle).toHaveBeenCalledWith(monitor, undefined); expect(result).toEqual( expect.objectContaining({ monitorId: "mon-1", @@ -73,7 +73,18 @@ describe("NetworkService", () => { await service.requestStatus(monitor); expect(httpProvider.handle).not.toHaveBeenCalled(); - expect(pingProvider.handle).toHaveBeenCalledWith(monitor); + 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 () => {