508 lines
23 KiB
TypeScript
508 lines
23 KiB
TypeScript
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
|
|
import { IncidentService } from "../../../src/domain/incidents/incident.service.ts";
|
|
import { createMockLogger } from "../../helpers/createMockLogger.ts";
|
|
import type { IIncidentsRepository } from "../../../src/domain/incidents/incident.repository.interface.ts";
|
|
import type { IMonitorsRepository } from "../../../src/domain/monitors/monitor.repository.interface.ts";
|
|
import type { IUsersRepository } from "../../../src/domain/users/user.repository.interface.ts";
|
|
import type { INotificationMessageBuilder } from "../../../src/domain/notifications/notification.message-builder.ts";
|
|
import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts";
|
|
import type { Incident } from "../../../src/domain/incidents/incident.type.ts";
|
|
import type { MonitorActionDecision } from "../../../src/worker/worker.interface.ts";
|
|
import type { Check } from "../../../src/domain/checks/check.type.ts";
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
const createIncidentsRepo = () =>
|
|
({
|
|
findActiveByMonitorId: jest.fn(),
|
|
findActiveByIncidentId: jest.fn(),
|
|
create: jest.fn(),
|
|
updateById: jest.fn(),
|
|
findById: jest.fn(),
|
|
findByTeamId: jest.fn(),
|
|
countByTeamId: jest.fn(),
|
|
findSummaryByTeamId: jest.fn(),
|
|
}) as unknown as jest.Mocked<IIncidentsRepository>;
|
|
|
|
const createMonitorsRepo = () =>
|
|
({
|
|
findById: jest.fn(),
|
|
}) as unknown as jest.Mocked<IMonitorsRepository>;
|
|
|
|
const createUsersRepo = () =>
|
|
({
|
|
findById: jest.fn(),
|
|
}) as unknown as jest.Mocked<IUsersRepository>;
|
|
|
|
const createMessageBuilder = () =>
|
|
({
|
|
extractThresholdBreaches: jest.fn(),
|
|
}) as unknown as jest.Mocked<INotificationMessageBuilder>;
|
|
|
|
const createService = (overrides?: {
|
|
logger?: ReturnType<typeof createMockLogger>;
|
|
incidentsRepository?: ReturnType<typeof createIncidentsRepo>;
|
|
monitorsRepository?: ReturnType<typeof createMonitorsRepo>;
|
|
usersRepository?: ReturnType<typeof createUsersRepo>;
|
|
notificationMessageBuilder?: ReturnType<typeof createMessageBuilder>;
|
|
}) => {
|
|
const logger = overrides?.logger ?? createMockLogger();
|
|
const incidentsRepository = overrides?.incidentsRepository ?? createIncidentsRepo();
|
|
const monitorsRepository = overrides?.monitorsRepository ?? createMonitorsRepo();
|
|
const usersRepository = overrides?.usersRepository ?? createUsersRepo();
|
|
const notificationMessageBuilder = overrides?.notificationMessageBuilder ?? createMessageBuilder();
|
|
|
|
const service = new IncidentService(logger as any, incidentsRepository, monitorsRepository, usersRepository, notificationMessageBuilder);
|
|
return { service, logger, incidentsRepository, monitorsRepository, usersRepository, notificationMessageBuilder };
|
|
};
|
|
|
|
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
|
|
({
|
|
id: "mon-1",
|
|
teamId: "team-1",
|
|
name: "Test Monitor",
|
|
type: "http",
|
|
url: "https://example.com",
|
|
status: "down",
|
|
...overrides,
|
|
}) as Monitor;
|
|
|
|
const makeIncident = (overrides?: Partial<Incident>): Incident =>
|
|
({
|
|
id: "inc-1",
|
|
monitorId: "mon-1",
|
|
teamId: "team-1",
|
|
startTime: "1700000000000",
|
|
status: true,
|
|
statusCode: 500,
|
|
createdAt: "2026-01-01T00:00:00Z",
|
|
updatedAt: "2026-01-01T00:00:00Z",
|
|
...overrides,
|
|
}) as Incident;
|
|
|
|
const makeCheck = (overrides?: Partial<Check>): Check =>
|
|
({
|
|
id: "check-1",
|
|
metadata: { monitorId: "mon-1", teamId: "team-1", type: "http" },
|
|
status: true,
|
|
statusCode: 200,
|
|
responseTime: 100,
|
|
message: "OK",
|
|
createdAt: "2026-01-01T00:00:00.000Z",
|
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
...overrides,
|
|
}) as Check;
|
|
|
|
const makeDecision = (overrides?: Partial<MonitorActionDecision>): MonitorActionDecision => ({
|
|
shouldCreateIncident: false,
|
|
shouldResolveIncident: false,
|
|
shouldSendNotification: false,
|
|
incidentReason: null,
|
|
notificationReason: null,
|
|
...overrides,
|
|
});
|
|
|
|
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
describe("IncidentService", () => {
|
|
// ── handleIncident ───────────────────────────────────────────────────────
|
|
|
|
describe("handleIncident", () => {
|
|
it("returns null when neither create nor resolve is requested", async () => {
|
|
const { service } = createService();
|
|
const result = await service.handleIncident(makeMonitor(), 200, makeDecision(), makeCheck());
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("returns existing active incident when shouldCreateIncident and one already exists", async () => {
|
|
const existing = makeIncident();
|
|
const { service, incidentsRepository } = createService();
|
|
(incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(existing);
|
|
|
|
const result = await service.handleIncident(
|
|
makeMonitor(),
|
|
500,
|
|
makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }),
|
|
makeCheck()
|
|
);
|
|
|
|
expect(result).toBe(existing);
|
|
expect(incidentsRepository.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("creates a new incident when shouldCreateIncident and no active incident exists", async () => {
|
|
const created = makeIncident();
|
|
const { service, incidentsRepository } = createService();
|
|
(incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(null);
|
|
(incidentsRepository.create as jest.Mock).mockResolvedValue(created);
|
|
|
|
const result = await service.handleIncident(
|
|
makeMonitor(),
|
|
500,
|
|
makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }),
|
|
makeCheck()
|
|
);
|
|
|
|
expect(result).toBe(created);
|
|
expect(incidentsRepository.create).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
monitorId: "mon-1",
|
|
teamId: "team-1",
|
|
status: true,
|
|
statusCode: 500,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("uses status code 9999 and builds message for threshold_breach incidents", async () => {
|
|
const created = makeIncident();
|
|
const { service, incidentsRepository, notificationMessageBuilder } = createService();
|
|
(incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(null);
|
|
(incidentsRepository.create as jest.Mock).mockResolvedValue(created);
|
|
(notificationMessageBuilder.extractThresholdBreaches as jest.Mock).mockReturnValue([
|
|
{ metric: "cpu", formattedValue: "95%", threshold: 80, unit: "%" },
|
|
]);
|
|
|
|
const monitor = makeMonitor({ type: "hardware" });
|
|
const check = makeCheck({ metadata: { monitorId: "mon-1", teamId: "team-1", type: "hardware" }, cpu: { usage_percent: 0.95 } });
|
|
const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" });
|
|
await service.handleIncident(monitor, 200, decision, check);
|
|
|
|
expect(notificationMessageBuilder.extractThresholdBreaches).toHaveBeenCalledWith(monitor, check);
|
|
expect(incidentsRepository.create).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
statusCode: 9999,
|
|
message: "CPU: 95% (threshold: 80%)",
|
|
})
|
|
);
|
|
});
|
|
|
|
it("uses fallback message when extractThresholdBreaches returns empty array", async () => {
|
|
const created = makeIncident();
|
|
const { service, incidentsRepository, notificationMessageBuilder } = createService();
|
|
(incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(null);
|
|
(incidentsRepository.create as jest.Mock).mockResolvedValue(created);
|
|
(notificationMessageBuilder.extractThresholdBreaches as jest.Mock).mockReturnValue([]);
|
|
|
|
const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" });
|
|
await service.handleIncident(makeMonitor(), 200, decision, makeCheck());
|
|
|
|
expect(incidentsRepository.create).toHaveBeenCalledWith(expect.objectContaining({ message: "Threshold breach detected" }));
|
|
});
|
|
|
|
it("builds the container list for docker threshold_breach incidents", async () => {
|
|
const created = makeIncident();
|
|
const { service, incidentsRepository, notificationMessageBuilder } = createService();
|
|
(incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(null);
|
|
(incidentsRepository.create as jest.Mock).mockResolvedValue(created);
|
|
|
|
const monitor = makeMonitor({ type: "docker", status: "breached", dockerAlertOnStopped: true, dockerAlertOnUnhealthy: true });
|
|
const check = makeCheck({
|
|
metadata: { monitorId: "mon-1", teamId: "team-1", type: "docker" },
|
|
containers: [
|
|
{ id: "1", name: "db", image: "postgres", state: "exited", status: "Exited (1)", health: "none", exitCode: 1 },
|
|
{ id: "2", name: "api", image: "api", state: "running", status: "Up", health: "unhealthy" },
|
|
{ id: "3", name: "web", image: "nginx", state: "running", status: "Up", health: "healthy" },
|
|
],
|
|
containerSummary: { total: 3, running: 2, stopped: 1, unhealthy: 1 },
|
|
});
|
|
const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" });
|
|
await service.handleIncident(monitor, 200, decision, check);
|
|
|
|
expect(incidentsRepository.create).toHaveBeenCalledWith(
|
|
expect.objectContaining({ statusCode: 9999, message: "db: stopped (exit code 1), api: unhealthy" })
|
|
);
|
|
expect(notificationMessageBuilder.extractThresholdBreaches).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("joins multiple threshold breaches with commas", async () => {
|
|
const created = makeIncident();
|
|
const { service, incidentsRepository, notificationMessageBuilder } = createService();
|
|
(incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(null);
|
|
(incidentsRepository.create as jest.Mock).mockResolvedValue(created);
|
|
(notificationMessageBuilder.extractThresholdBreaches as jest.Mock).mockReturnValue([
|
|
{ metric: "cpu", formattedValue: "95%", threshold: 80, unit: "%" },
|
|
{ metric: "memory", formattedValue: "90%", threshold: 80, unit: "%" },
|
|
]);
|
|
|
|
const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" });
|
|
await service.handleIncident(makeMonitor(), 200, decision, makeCheck());
|
|
|
|
expect(incidentsRepository.create).toHaveBeenCalledWith(
|
|
expect.objectContaining({ message: "CPU: 95% (threshold: 80%), MEMORY: 90% (threshold: 80%)" })
|
|
);
|
|
});
|
|
|
|
it("resolves active incident when shouldResolveIncident", async () => {
|
|
const active = makeIncident();
|
|
const resolved = makeIncident({ status: false, endTime: "123", resolutionType: "automatic" });
|
|
const { service, incidentsRepository } = createService();
|
|
(incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(active);
|
|
(incidentsRepository.updateById as jest.Mock).mockResolvedValue(resolved);
|
|
|
|
const result = await service.handleIncident(makeMonitor(), 200, makeDecision({ shouldResolveIncident: true }), makeCheck());
|
|
|
|
expect(result).toBe(resolved);
|
|
expect(incidentsRepository.updateById).toHaveBeenCalledWith(
|
|
"inc-1",
|
|
"team-1",
|
|
expect.objectContaining({ status: false, resolutionType: "automatic" })
|
|
);
|
|
});
|
|
|
|
it("returns null when shouldResolveIncident but no active incident exists", async () => {
|
|
const { service, incidentsRepository } = createService();
|
|
(incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(null);
|
|
|
|
const result = await service.handleIncident(makeMonitor(), 200, makeDecision({ shouldResolveIncident: true }), makeCheck());
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ── resolveIncident ──────────────────────────────────────────────────────
|
|
|
|
describe("resolveIncident", () => {
|
|
it("resolves incident with manual resolution", async () => {
|
|
const active = makeIncident();
|
|
const resolved = makeIncident({ status: false });
|
|
const { service, incidentsRepository } = createService();
|
|
(incidentsRepository.findActiveByIncidentId as jest.Mock).mockResolvedValue(active);
|
|
(incidentsRepository.updateById as jest.Mock).mockResolvedValue(resolved);
|
|
|
|
const result = await service.resolveIncident("inc-1", "user-1", "team-1", "Fixed it", "user@test.com");
|
|
|
|
expect(result).toBe(resolved);
|
|
expect(incidentsRepository.updateById).toHaveBeenCalledWith(
|
|
"inc-1",
|
|
"team-1",
|
|
expect.objectContaining({
|
|
resolutionType: "manual",
|
|
status: false,
|
|
resolvedBy: "user-1",
|
|
resolvedByEmail: "user@test.com",
|
|
comment: "Fixed it",
|
|
})
|
|
);
|
|
});
|
|
|
|
it("sets resolvedByEmail and comment to null when not provided", async () => {
|
|
const active = makeIncident();
|
|
const resolved = makeIncident({ status: false });
|
|
const { service, incidentsRepository } = createService();
|
|
(incidentsRepository.findActiveByIncidentId as jest.Mock).mockResolvedValue(active);
|
|
(incidentsRepository.updateById as jest.Mock).mockResolvedValue(resolved);
|
|
|
|
await service.resolveIncident("inc-1", "user-1", "team-1");
|
|
|
|
expect(incidentsRepository.updateById).toHaveBeenCalledWith(
|
|
"inc-1",
|
|
"team-1",
|
|
expect.objectContaining({
|
|
resolvedByEmail: null,
|
|
comment: null,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("throws when incidentId is missing", async () => {
|
|
const { service } = createService();
|
|
await expect(service.resolveIncident("", "user-1", "team-1")).rejects.toThrow("No incident ID in request");
|
|
});
|
|
|
|
it("throws when userId is missing", async () => {
|
|
const { service } = createService();
|
|
await expect(service.resolveIncident("inc-1", "", "team-1")).rejects.toThrow("No user ID in request");
|
|
});
|
|
|
|
it("throws when teamId is missing", async () => {
|
|
const { service } = createService();
|
|
await expect(service.resolveIncident("inc-1", "user-1", "")).rejects.toThrow("No team ID in request");
|
|
});
|
|
|
|
it("throws when incident is not found", async () => {
|
|
const { service, incidentsRepository } = createService();
|
|
(incidentsRepository.findActiveByIncidentId as jest.Mock).mockResolvedValue(null);
|
|
|
|
await expect(service.resolveIncident("inc-1", "user-1", "team-1")).rejects.toThrow("Incident not found");
|
|
});
|
|
|
|
it("throws when incident is already resolved", async () => {
|
|
const { service, incidentsRepository } = createService();
|
|
(incidentsRepository.findActiveByIncidentId as jest.Mock).mockResolvedValue(makeIncident({ status: false }));
|
|
|
|
await expect(service.resolveIncident("inc-1", "user-1", "team-1")).rejects.toThrow("Incident is already resolved");
|
|
});
|
|
|
|
it("logs error and rethrows on unexpected failure", async () => {
|
|
const { service, logger, incidentsRepository } = createService();
|
|
(incidentsRepository.findActiveByIncidentId as jest.Mock).mockRejectedValue(new Error("db error"));
|
|
|
|
await expect(service.resolveIncident("inc-1", "user-1", "team-1")).rejects.toThrow("db error");
|
|
expect(logger.error).toHaveBeenCalledWith(
|
|
expect.objectContaining({ service: "incidentService", method: "resolveIncident", message: "db error" })
|
|
);
|
|
});
|
|
|
|
it("logs 'Unknown error' for non-Error exceptions", async () => {
|
|
const { service, logger, incidentsRepository } = createService();
|
|
(incidentsRepository.findActiveByIncidentId as jest.Mock).mockRejectedValue("string error");
|
|
|
|
await expect(service.resolveIncident("inc-1", "user-1", "team-1")).rejects.toBe("string error");
|
|
expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ message: "Unknown error", stack: undefined }));
|
|
});
|
|
});
|
|
|
|
// ── getIncidentsByTeam ───────────────────────────────────────────────────
|
|
|
|
describe("getIncidentsByTeam", () => {
|
|
it("returns incidents and count", async () => {
|
|
const incidents = [makeIncident()];
|
|
const { service, incidentsRepository } = createService();
|
|
(incidentsRepository.findByTeamId as jest.Mock).mockResolvedValue(incidents);
|
|
(incidentsRepository.countByTeamId as jest.Mock).mockResolvedValue(1);
|
|
|
|
const result = await service.getIncidentsByTeam("team-1", "desc", "day", 0, 20, undefined, undefined, undefined);
|
|
|
|
expect(result).toEqual({ incidents, count: 1 });
|
|
});
|
|
|
|
it("defaults page to 0 and rowsPerPage to 20 when nullish", async () => {
|
|
const { service, incidentsRepository } = createService();
|
|
(incidentsRepository.findByTeamId as jest.Mock).mockResolvedValue([]);
|
|
(incidentsRepository.countByTeamId as jest.Mock).mockResolvedValue(0);
|
|
|
|
await service.getIncidentsByTeam("team-1", "desc", "day", null as any, null as any, undefined, undefined, undefined);
|
|
|
|
expect(incidentsRepository.findByTeamId).toHaveBeenCalledWith("team-1", expect.anything(), 0, 20, "desc", undefined, undefined, undefined);
|
|
});
|
|
|
|
it("passes filter parameters through to repository", async () => {
|
|
const { service, incidentsRepository } = createService();
|
|
(incidentsRepository.findByTeamId as jest.Mock).mockResolvedValue([]);
|
|
(incidentsRepository.countByTeamId as jest.Mock).mockResolvedValue(0);
|
|
|
|
await service.getIncidentsByTeam("team-1", "asc", "week", 2, 10, true, "mon-1", "manual");
|
|
|
|
expect(incidentsRepository.findByTeamId).toHaveBeenCalledWith("team-1", expect.any(Date), 2, 10, "asc", true, "mon-1", "manual");
|
|
expect(incidentsRepository.countByTeamId).toHaveBeenCalledWith("team-1", expect.any(Date), true, "mon-1", "manual");
|
|
});
|
|
|
|
it("throws when teamId is missing", async () => {
|
|
const { service } = createService();
|
|
await expect(service.getIncidentsByTeam("", "desc", "day", 0, 20, undefined, undefined, undefined)).rejects.toThrow("No team ID in request");
|
|
});
|
|
|
|
it("logs error and rethrows on unexpected failure", async () => {
|
|
const { service, logger, incidentsRepository } = createService();
|
|
(incidentsRepository.findByTeamId as jest.Mock).mockRejectedValue(new Error("db error"));
|
|
|
|
await expect(service.getIncidentsByTeam("team-1", "desc", "day", 0, 20, undefined, undefined, undefined)).rejects.toThrow("db error");
|
|
expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ service: "incidentService", method: "getIncidentsByTeam" }));
|
|
});
|
|
|
|
it("logs 'Unknown error' for non-Error exceptions", async () => {
|
|
const { service, logger, incidentsRepository } = createService();
|
|
(incidentsRepository.findByTeamId as jest.Mock).mockRejectedValue(42);
|
|
|
|
await expect(service.getIncidentsByTeam("team-1", "desc", "day", 0, 20, undefined, undefined, undefined)).rejects.toBe(42);
|
|
expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ message: "Unknown error", stack: undefined }));
|
|
});
|
|
});
|
|
|
|
// ── getIncidentSummary ───────────────────────────────────────────────────
|
|
|
|
describe("getIncidentSummary", () => {
|
|
it("returns summary from repository", async () => {
|
|
const summary = { recentIncidents: [], totalIncidents: 0 };
|
|
const { service, incidentsRepository } = createService();
|
|
(incidentsRepository.findSummaryByTeamId as jest.Mock).mockResolvedValue(summary);
|
|
|
|
const result = await service.getIncidentSummary("team-1", 5);
|
|
|
|
expect(result).toBe(summary);
|
|
expect(incidentsRepository.findSummaryByTeamId).toHaveBeenCalledWith("team-1", 5);
|
|
});
|
|
|
|
it("defaults limit to 10 when not provided", async () => {
|
|
const { service, incidentsRepository } = createService();
|
|
(incidentsRepository.findSummaryByTeamId as jest.Mock).mockResolvedValue({});
|
|
|
|
await service.getIncidentSummary("team-1");
|
|
|
|
expect(incidentsRepository.findSummaryByTeamId).toHaveBeenCalledWith("team-1", 10);
|
|
});
|
|
|
|
it("throws when teamId is missing", async () => {
|
|
const { service } = createService();
|
|
await expect(service.getIncidentSummary("")).rejects.toThrow("No team ID in request");
|
|
});
|
|
|
|
it("logs error and rethrows on unexpected failure", async () => {
|
|
const { service, logger, incidentsRepository } = createService();
|
|
(incidentsRepository.findSummaryByTeamId as jest.Mock).mockRejectedValue(new Error("db error"));
|
|
|
|
await expect(service.getIncidentSummary("team-1")).rejects.toThrow("db error");
|
|
expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ service: "incidentService", method: "getIncidentSummary" }));
|
|
});
|
|
|
|
it("logs 'Unknown error' for non-Error exceptions", async () => {
|
|
const { service, logger, incidentsRepository } = createService();
|
|
(incidentsRepository.findSummaryByTeamId as jest.Mock).mockRejectedValue("boom");
|
|
|
|
await expect(service.getIncidentSummary("team-1")).rejects.toBe("boom");
|
|
expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ message: "Unknown error", stack: undefined }));
|
|
});
|
|
});
|
|
|
|
// ── getIncidentById ──────────────────────────────────────────────────────
|
|
|
|
describe("getIncidentById", () => {
|
|
it("returns incident, monitor, and user when resolvedBy exists", async () => {
|
|
const incident = makeIncident({ resolvedBy: "user-1" });
|
|
const monitor = makeMonitor();
|
|
const user = { id: "user-1", email: "user@test.com" };
|
|
const { service, incidentsRepository, monitorsRepository, usersRepository } = createService();
|
|
(incidentsRepository.findById as jest.Mock).mockResolvedValue(incident);
|
|
(monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor);
|
|
(usersRepository.findById as jest.Mock).mockResolvedValue(user);
|
|
|
|
const result = await service.getIncidentById("inc-1", "team-1");
|
|
|
|
expect(result).toEqual({ incident, monitor, user });
|
|
expect(usersRepository.findById).toHaveBeenCalledWith("user-1");
|
|
});
|
|
|
|
it("returns user as null when resolvedBy is not set", async () => {
|
|
const incident = makeIncident({ resolvedBy: undefined });
|
|
const monitor = makeMonitor();
|
|
const { service, incidentsRepository, monitorsRepository, usersRepository } = createService();
|
|
(incidentsRepository.findById as jest.Mock).mockResolvedValue(incident);
|
|
(monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor);
|
|
|
|
const result = await service.getIncidentById("inc-1", "team-1");
|
|
|
|
expect(result.user).toBeNull();
|
|
expect(usersRepository.findById).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("logs error and rethrows on unexpected failure", async () => {
|
|
const { service, logger, incidentsRepository } = createService();
|
|
(incidentsRepository.findById as jest.Mock).mockRejectedValue(new Error("not found"));
|
|
|
|
await expect(service.getIncidentById("inc-1", "team-1")).rejects.toThrow("not found");
|
|
expect(logger.error).toHaveBeenCalledWith(
|
|
expect.objectContaining({ service: "incidentService", method: "getIncidentById", details: { incidentId: "inc-1" } })
|
|
);
|
|
});
|
|
|
|
it("logs 'Unknown error' for non-Error exceptions", async () => {
|
|
const { service, logger, incidentsRepository } = createService();
|
|
(incidentsRepository.findById as jest.Mock).mockRejectedValue(null);
|
|
|
|
await expect(service.getIncidentById("inc-1", "team-1")).rejects.toBeNull();
|
|
expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ message: "Unknown error", stack: undefined }));
|
|
});
|
|
});
|
|
});
|