Files
Checkmate/server/test/unit/services/egressStateService.test.ts
Craig Lawson b68367b04f Harden the egress recovery job after a second review pass (#3937)
- Schedule the recovery job before recording the degraded transition,
  so a crash between the two leaves a stray row (removed on the job's
  first run) rather than a degraded state nothing is polling
- Count any HTTP response, including 4xx/5xx, as reachable for http
  targets; only a transport failure counts against egress
- Cascade a deleted notification out of egressNotifications with an
  atomic $pull in the settings repository, after the team-scoped delete
- Handle two processes racing the first egress state insert
- Address global job rows by one key (deleteGlobalJob on _id) and
  restore the deleteByIdAndType signature
- Inject net into PingProvider like PortProvider
- Integration tests for MongoEgressStateRepository and the global-row
  delete methods on the jobs repository

🔧
2026-09-15 08:59:13 +01:00

101 lines
3.7 KiB
TypeScript

import { describe, expect, it, jest } from "@jest/globals";
import { EgressStateService } from "../../../src/domain/egress/egress-state.service.ts";
import type { EgressState } from "../../../src/domain/egress/egress.type.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
const createEgressStateRepo = () => ({
findSingleton: jest.fn(),
recordProbe: jest.fn(),
markDegraded: jest.fn(),
markRecovered: jest.fn(),
reset: jest.fn(),
});
const createJobsRepo = () => ({
deleteGlobalJob: jest.fn().mockResolvedValue(true),
});
const createService = () => {
const egressStateRepository = createEgressStateRepo();
const jobsRepository = createJobsRepo();
const service = new EgressStateService(egressStateRepository as any, jobsRepository as any);
return { service, egressStateRepository, jobsRepository };
};
const makeState = (overrides?: Partial<EgressState>): EgressState => ({
id: "egress-1",
status: "ok",
degradedSince: null,
lastRecoveredAt: null,
lastProbeAt: null,
lastProbeResults: [],
createdAt: "2026-01-01T00:00:00Z",
updatedAt: "2026-01-01T00:00:00Z",
...overrides,
});
// ── Tests ────────────────────────────────────────────────────────────────────
describe("EgressStateService", () => {
describe("getState", () => {
it("returns the singleton state from the repository", async () => {
const { service, egressStateRepository } = createService();
const state = makeState({
status: "degraded",
degradedSince: "2026-01-01T00:05:00Z",
lastProbeAt: "2026-01-01T00:05:00Z",
lastProbeResults: [{ target: "1.1.1.1", reachable: false, responseTime: 0, message: "timeout" }],
});
(egressStateRepository.findSingleton as jest.Mock).mockResolvedValue(state);
const result = await service.getState();
expect(egressStateRepository.findSingleton).toHaveBeenCalledTimes(1);
expect(result).toEqual(state);
});
it("does not call any transition method", async () => {
const { service, egressStateRepository } = createService();
(egressStateRepository.findSingleton as jest.Mock).mockResolvedValue(makeState());
await service.getState();
expect(egressStateRepository.recordProbe).not.toHaveBeenCalled();
expect(egressStateRepository.markDegraded).not.toHaveBeenCalled();
expect(egressStateRepository.markRecovered).not.toHaveBeenCalled();
});
it("propagates repository errors", async () => {
const { service, egressStateRepository } = createService();
(egressStateRepository.findSingleton as jest.Mock).mockRejectedValue(new Error("db down"));
await expect(service.getState()).rejects.toThrow("db down");
});
});
describe("reset", () => {
it("removes any pending recovery job and returns the state to ok", async () => {
const { service, egressStateRepository, jobsRepository } = createService();
const reset = makeState();
(egressStateRepository.reset as jest.Mock).mockResolvedValue(reset);
const result = await service.reset();
expect(jobsRepository.deleteGlobalJob).toHaveBeenCalledWith("egress");
expect(egressStateRepository.reset).toHaveBeenCalledTimes(1);
expect(result).toEqual(reset);
});
it("does not touch the probe-driven transitions", async () => {
const { service, egressStateRepository } = createService();
(egressStateRepository.reset as jest.Mock).mockResolvedValue(makeState());
await service.reset();
expect(egressStateRepository.markDegraded).not.toHaveBeenCalled();
expect(egressStateRepository.markRecovered).not.toHaveBeenCalled();
});
});
});