- The egress job runs through WorkerHelper.getEgressRecoveryJob(), like
the cleanup jobs, instead of a direct service call from the queue
- Toggling egressCheckEnabled resets the egress state and removes any
pending recovery job
- Probes resolve and pass the proxy, so http targets honour the global
proxy as monitors do
- PingProvider keeps IPv6 addresses intact instead of truncating at the
first colon
- Egress targets are parsed by one function shared by the validator and
the probe; malformed addresses such as unbalanced brackets are rejected
- Notification id pattern drops the /i flag, which produced an invalid
OpenAPI pattern
- EgressState findSingleton reads before it writes, so a read no longer
bumps updatedAt
- "disabled" is a distinct assessment result and is cached like any other
- Concurrent failures share an in-flight assessment for as long as it
runs, then reuse the settled result for 5 s
- The recovery job row is only removed if nobody rescheduled it since
this run claimed it, so a concurrent new episode keeps its job
- Deleting a notification removes it from egressNotifications
🔧
211 lines
9.5 KiB
TypeScript
211 lines
9.5 KiB
TypeScript
import { describe, expect, it, jest } from "@jest/globals";
|
|
import { WorkerHelper } from "../../../src/worker/worker.helper.ts";
|
|
import { createMockLogger } from "../../helpers/createMockLogger.ts";
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
//
|
|
// WorkerHelper is now thin glue: it wires the geo job to the GeoChecksPipeline and
|
|
// owns the cleanup jobs. The pipeline internals are tested in checkPipeline.test.ts;
|
|
// here we test only the glue + cleanup jobs.
|
|
|
|
const createHelper = (overrides?: Record<string, unknown>) => {
|
|
const monitorsRepository = {
|
|
updateById: jest.fn().mockResolvedValue({}),
|
|
findAllMonitorIds: jest.fn().mockResolvedValue(["m1"]),
|
|
deleteByTeamIdsNotIn: jest.fn().mockResolvedValue(0),
|
|
};
|
|
const jobsRepository = {
|
|
deleteByMonitorIdsNotIn: jest.fn().mockResolvedValue(0),
|
|
};
|
|
const teamsRepository = {
|
|
findAllTeamIds: jest.fn().mockResolvedValue(["team"]),
|
|
};
|
|
const monitorStatsRepository = {
|
|
deleteByMonitorIdsNotIn: jest.fn().mockResolvedValue(0),
|
|
};
|
|
const checksRepository = {
|
|
deleteByMonitorIdsNotIn: jest.fn().mockResolvedValue(0),
|
|
};
|
|
const incidentsRepository = {
|
|
deleteByMonitorIdsNotIn: jest.fn().mockResolvedValue(0),
|
|
};
|
|
const geoChecksRepository = {
|
|
deleteByMonitorIdsNotIn: jest.fn().mockResolvedValue(0),
|
|
};
|
|
const dockerLogsRepository = {
|
|
deleteByMonitorIdsNotIn: jest.fn().mockResolvedValue(0),
|
|
};
|
|
const settingsService = {
|
|
getDBSettings: jest.fn().mockResolvedValue({ checkTTL: 30 }),
|
|
};
|
|
const checkService = {
|
|
deleteOlderThan: jest.fn().mockResolvedValue(0),
|
|
};
|
|
const egressService = {
|
|
checkRecovery: jest.fn().mockResolvedValue(undefined),
|
|
};
|
|
|
|
const defaults = {
|
|
logger: createMockLogger(),
|
|
checkService,
|
|
settingsService,
|
|
monitorsRepository,
|
|
jobsRepository,
|
|
teamsRepository,
|
|
monitorStatsRepository,
|
|
checksRepository,
|
|
incidentsRepository,
|
|
geoChecksRepository,
|
|
dockerLogsRepository,
|
|
egressService,
|
|
...overrides,
|
|
};
|
|
|
|
const helper = new WorkerHelper(
|
|
defaults.logger as any,
|
|
defaults.checkService as any,
|
|
defaults.settingsService as any,
|
|
defaults.monitorsRepository as any,
|
|
defaults.jobsRepository as any,
|
|
defaults.teamsRepository as any,
|
|
defaults.monitorStatsRepository as any,
|
|
defaults.checksRepository as any,
|
|
defaults.incidentsRepository as any,
|
|
defaults.geoChecksRepository as any,
|
|
defaults.dockerLogsRepository as any,
|
|
defaults.egressService as any
|
|
);
|
|
return { helper, defaults };
|
|
};
|
|
|
|
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
describe("WorkerHelper", () => {
|
|
// ── getCleanupOrphanedJob ────────────────────────────────────────────────
|
|
|
|
describe("getCleanupOrphanedJob", () => {
|
|
it("cleans up orphaned data across all repositories", async () => {
|
|
const { helper, defaults } = createHelper();
|
|
(defaults.monitorsRepository.deleteByTeamIdsNotIn as jest.Mock).mockResolvedValue(2);
|
|
(defaults.monitorStatsRepository.deleteByMonitorIdsNotIn as jest.Mock).mockResolvedValue(3);
|
|
(defaults.checksRepository.deleteByMonitorIdsNotIn as jest.Mock).mockResolvedValue(4);
|
|
(defaults.incidentsRepository.deleteByMonitorIdsNotIn as jest.Mock).mockResolvedValue(1);
|
|
(defaults.geoChecksRepository.deleteByMonitorIdsNotIn as jest.Mock).mockResolvedValue(5);
|
|
(defaults.dockerLogsRepository.deleteByMonitorIdsNotIn as jest.Mock).mockResolvedValue(7);
|
|
(defaults.jobsRepository.deleteByMonitorIdsNotIn as jest.Mock).mockResolvedValue(6);
|
|
|
|
const job = helper.getCleanupOrphanedJob();
|
|
await job();
|
|
|
|
expect(defaults.teamsRepository.findAllTeamIds).toHaveBeenCalled();
|
|
expect(defaults.monitorsRepository.deleteByTeamIdsNotIn).toHaveBeenCalledWith(["team"]);
|
|
expect(defaults.monitorsRepository.findAllMonitorIds).toHaveBeenCalled();
|
|
expect(defaults.monitorStatsRepository.deleteByMonitorIdsNotIn).toHaveBeenCalledWith(["m1"]);
|
|
expect(defaults.checksRepository.deleteByMonitorIdsNotIn).toHaveBeenCalledWith(["m1"]);
|
|
expect(defaults.incidentsRepository.deleteByMonitorIdsNotIn).toHaveBeenCalledWith(["m1"]);
|
|
expect(defaults.geoChecksRepository.deleteByMonitorIdsNotIn).toHaveBeenCalledWith(["m1"]);
|
|
expect(defaults.dockerLogsRepository.deleteByMonitorIdsNotIn).toHaveBeenCalledWith(["m1"]);
|
|
expect(defaults.jobsRepository.deleteByMonitorIdsNotIn).toHaveBeenCalledWith(["m1"]);
|
|
expect(defaults.logger.info).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("2 orphaned monitors") }));
|
|
expect(defaults.logger.info).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("3 orphaned monitor stats") }));
|
|
expect(defaults.logger.info).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("4 orphaned checks") }));
|
|
expect(defaults.logger.info).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("1 orphaned incidents") }));
|
|
expect(defaults.logger.info).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("5 orphaned geo checks") }));
|
|
expect(defaults.logger.info).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("7 orphaned docker logs") }));
|
|
expect(defaults.logger.info).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("6 orphaned jobs") }));
|
|
});
|
|
|
|
it("skips info logs when no orphaned data is found", async () => {
|
|
const { helper, defaults } = createHelper();
|
|
const job = helper.getCleanupOrphanedJob();
|
|
await job();
|
|
|
|
const infoMessages = (defaults.logger.info as jest.Mock).mock.calls.map((c: any) => c[0].message);
|
|
expect(infoMessages).toContain("Starting cleanup of orphaned data");
|
|
expect(infoMessages).toContain("Cleanup of orphaned data completed");
|
|
expect(infoMessages).not.toContain(expect.stringContaining("orphaned monitors"));
|
|
});
|
|
|
|
it("logs warning and rethrows on error", async () => {
|
|
const { helper, defaults } = createHelper();
|
|
(defaults.teamsRepository.findAllTeamIds as jest.Mock).mockRejectedValue(new Error("db down"));
|
|
const job = helper.getCleanupOrphanedJob();
|
|
|
|
await expect(job()).rejects.toThrow("db down");
|
|
expect(defaults.logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "db down" }));
|
|
});
|
|
|
|
it("logs 'Unknown error' for non-Error exceptions", async () => {
|
|
const { helper, defaults } = createHelper();
|
|
(defaults.teamsRepository.findAllTeamIds as jest.Mock).mockRejectedValue(null);
|
|
const job = helper.getCleanupOrphanedJob();
|
|
|
|
await expect(job()).rejects.toBeNull();
|
|
expect(defaults.logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Unknown error", stack: undefined }));
|
|
});
|
|
});
|
|
|
|
// ── getCleanupRetentionJob ───────────────────────────────────────────────
|
|
|
|
describe("getCleanupRetentionJob", () => {
|
|
it("deletes checks older than TTL cutoff", async () => {
|
|
const { helper, defaults } = createHelper();
|
|
const job = helper.getCleanupRetentionJob();
|
|
await job();
|
|
|
|
expect(defaults.checkService.deleteOlderThan).toHaveBeenCalledWith(expect.any(Date));
|
|
expect(defaults.logger.info).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("Deleted") }));
|
|
});
|
|
|
|
it("skips cleanup when checkTTL is sentinel value (366)", async () => {
|
|
const { helper, defaults } = createHelper({
|
|
settingsService: { getDBSettings: jest.fn().mockResolvedValue({ checkTTL: 366 }) },
|
|
});
|
|
const job = helper.getCleanupRetentionJob();
|
|
await job();
|
|
|
|
expect(defaults.checkService.deleteOlderThan).not.toHaveBeenCalled();
|
|
expect(defaults.logger.info).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("unlimited") }));
|
|
});
|
|
|
|
it("logs error on failure without rethrowing", async () => {
|
|
const { helper, defaults } = createHelper({
|
|
settingsService: { getDBSettings: jest.fn().mockRejectedValue(new Error("db error")) },
|
|
});
|
|
const job = helper.getCleanupRetentionJob();
|
|
|
|
await job(); // Should not throw
|
|
|
|
expect(defaults.logger.error).toHaveBeenCalledWith(expect.objectContaining({ message: "db error" }));
|
|
});
|
|
|
|
it("logs 'Unknown error' for non-Error exceptions", async () => {
|
|
const { helper, defaults } = createHelper({
|
|
settingsService: { getDBSettings: jest.fn().mockRejectedValue("boom") },
|
|
});
|
|
const job = helper.getCleanupRetentionJob();
|
|
await job();
|
|
|
|
expect(defaults.logger.error).toHaveBeenCalledWith(expect.objectContaining({ message: "Unknown error", stack: undefined }));
|
|
});
|
|
});
|
|
|
|
describe("getEgressRecoveryJob", () => {
|
|
it("returns a function that hands the claimed job to the egress service", async () => {
|
|
const { helper, defaults } = createHelper();
|
|
const job = { id: "egress", type: "egress", refId: null, nextScheduledAt: 1000 } as any;
|
|
|
|
await helper.getEgressRecoveryJob()(job);
|
|
|
|
expect(defaults.egressService.checkRecovery).toHaveBeenCalledWith(job);
|
|
});
|
|
|
|
it("lets the egress service's error propagate so the queue records the failure", async () => {
|
|
const { helper, defaults } = createHelper();
|
|
(defaults.egressService.checkRecovery as jest.Mock).mockRejectedValue(new Error("db down"));
|
|
|
|
await expect(helper.getEgressRecoveryJob()({ id: "egress" } as any)).rejects.toThrow("db down");
|
|
});
|
|
});
|
|
});
|