- 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
🔧
101 lines
3.7 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|