Files

222 lines
9.6 KiB
TypeScript

import { describe, expect, it, jest, beforeEach } from "@jest/globals";
import { createMockLogger } from "../../../helpers/createMockLogger.ts";
import { makeNotification, makeMessage, makeMessageWithThresholds, makeMessageWithIncident } from "../../../helpers/notificationMessage.ts";
import { testNotificationProviderContract } from "../../../helpers/notificationProviderContract.ts";
const mockGotPut = jest.fn().mockResolvedValue({});
const mockGotGet = jest.fn().mockResolvedValue({ body: { room_id: "!resolved:example.com" } });
jest.unstable_mockModule("got", () => ({ default: { put: mockGotPut, get: mockGotGet } }));
jest.unstable_mockModule("crypto", () => ({ randomUUID: () => "test-uuid-1234" }));
const { MatrixProvider } = await import("../../../../src/domain/notifications/providers/matrix.ts");
const createProvider = () => {
const logger = createMockLogger();
return { provider: new MatrixProvider(logger as any), logger };
};
testNotificationProviderContract("MatrixProvider", {
create: () => {
mockGotPut.mockResolvedValue({});
return createProvider().provider;
},
makeNotification: () => makeNotification(),
});
describe("MatrixProvider", () => {
beforeEach(() => {
mockGotPut.mockReset().mockResolvedValue({});
mockGotGet.mockReset().mockResolvedValue({ body: { room_id: "!resolved:example.com" } });
});
describe("sendTestAlert", () => {
it("sends to Matrix API and returns true", async () => {
expect(await createProvider().provider.sendTestAlert(makeNotification())).toBe(true);
expect(mockGotPut).toHaveBeenCalledWith(
expect.stringContaining("matrix.example.com/_matrix/client/v3/rooms/!room%3Aexample.com/send/m.room.message/test-uuid-1234"),
expect.objectContaining({
headers: expect.objectContaining({ Authorization: "Bearer token-abc" }),
})
);
});
it("returns false when homeserverUrl is missing", async () => {
expect(await createProvider().provider.sendTestAlert(makeNotification({ homeserverUrl: undefined }))).toBe(false);
});
it("returns false when accessToken is missing", async () => {
expect(await createProvider().provider.sendTestAlert(makeNotification({ accessToken: undefined }))).toBe(false);
});
it("returns false when roomId is missing", async () => {
expect(await createProvider().provider.sendTestAlert(makeNotification({ roomId: undefined }))).toBe(false);
});
it("returns false and logs on error", async () => {
mockGotPut.mockRejectedValue(new Error("fail"));
const { provider, logger } = createProvider();
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
expect(logger.warn).toHaveBeenCalled();
});
it("handles non-Error thrown values", async () => {
mockGotPut.mockRejectedValue({ message: "custom obj" });
const { provider, logger } = createProvider();
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ stack: undefined }));
});
});
describe("sendMessage", () => {
it("sends formatted HTML and plain text", async () => {
const { provider } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(true);
const body = mockGotPut.mock.calls[0][1].json;
expect(body.msgtype).toBe("m.text");
expect(body.format).toBe("org.matrix.custom.html");
expect(body.body).toContain("Monitor Down");
expect(body.formatted_body).toContain("<h2");
});
it("returns false when homeserverUrl is missing", async () => {
expect(await createProvider().provider.sendMessage(makeNotification({ homeserverUrl: undefined }) as any, makeMessage())).toBe(false);
});
it("returns false and logs on error", async () => {
mockGotPut.mockRejectedValue(new Error("fail"));
const { provider, logger } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false);
expect(logger.warn).toHaveBeenCalled();
});
it("includes threshold breaches", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessageWithThresholds());
const body = mockGotPut.mock.calls[0][1].json;
expect(body.body).toContain("CPU");
expect(body.formatted_body).toContain("CPU");
});
it("includes incident link", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessageWithIncident());
const body = mockGotPut.mock.calls[0][1].json;
expect(body.body).toContain("View Incident");
expect(body.formatted_body).toContain("View Incident");
});
it("links the incident to the monitor's incidents page", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessageWithIncident());
const body = mockGotPut.mock.calls[0][1].json;
expect(body.body).toContain("View Incident: https://app.example.com/incidents/mon-1");
expect(body.formatted_body).toContain('href="https://app.example.com/incidents/mon-1"');
});
it("omits optional sections when not present", async () => {
const { provider } = createProvider();
const msg = makeMessage();
msg.content.thresholds = undefined;
msg.content.details = undefined;
msg.content.incident = undefined;
await provider.sendMessage(makeNotification() as any, msg);
const body = mockGotPut.mock.calls[0][1].json;
expect(body.body).not.toContain("Threshold");
expect(body.body).not.toContain("Additional");
expect(body.body).not.toContain("View Incident");
});
it("escapes HTML special characters", async () => {
const { provider } = createProvider();
const msg = makeMessage();
msg.monitor.name = '<script>alert("xss")</script>';
await provider.sendMessage(makeNotification() as any, msg);
const html = mockGotPut.mock.calls[0][1].json.formatted_body;
expect(html).not.toContain("<script>");
expect(html).toContain("&lt;script&gt;");
});
it("maps all severity levels to colors", async () => {
const { provider } = createProvider();
for (const severity of ["critical", "warning", "success", "info"] as const) {
mockGotPut.mockClear();
await provider.sendMessage(makeNotification() as any, makeMessage({ severity }));
const html = mockGotPut.mock.calls[0][1].json.formatted_body;
expect(html).toContain("color:");
}
});
it("uses default color for unknown severity", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessage({ severity: "unknown" as any }));
const html = mockGotPut.mock.calls[0][1].json.formatted_body;
expect(html).toContain("#808080");
});
});
describe("room identifiers", () => {
const putUrl = () => mockGotPut.mock.calls[0][0] as string;
it("percent-encodes the room ID in the send path", async () => {
const { provider } = createProvider();
await provider.sendTestAlert(makeNotification({ roomId: "!abc123:example.com" }));
expect(putUrl()).toContain("/rooms/!abc123%3Aexample.com/send/");
});
it("does not resolve a room ID through the directory", async () => {
const { provider } = createProvider();
await provider.sendTestAlert(makeNotification({ roomId: "!abc123:example.com" }));
expect(mockGotGet).not.toHaveBeenCalled();
});
// An unresolved alias breaks the request outright: the "#" opens a URI fragment, so
// the PUT lands on /rooms/ instead of the send endpoint.
it("resolves a room alias through the directory before sending", async () => {
const { provider } = createProvider();
expect(await provider.sendTestAlert(makeNotification({ roomId: "#alerts:example.com" }))).toBe(true);
expect(mockGotGet).toHaveBeenCalledWith(
"https://matrix.example.com/_matrix/client/v3/directory/room/%23alerts%3Aexample.com",
expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer token-abc" }) })
);
expect(putUrl()).toContain("/rooms/!resolved%3Aexample.com/send/");
});
it("never leaves an unencoded # in the request URL", async () => {
const { provider } = createProvider();
await provider.sendTestAlert(makeNotification({ roomId: "#alerts:example.com" }));
expect(putUrl()).not.toContain("#");
expect(new URL(putUrl()).hash).toBe("");
});
it("returns false and logs when the alias cannot be resolved", async () => {
mockGotGet.mockRejectedValue(new Error("M_NOT_FOUND"));
const { provider, logger } = createProvider();
expect(await provider.sendTestAlert(makeNotification({ roomId: "#missing:example.com" }))).toBe(false);
expect(mockGotPut).not.toHaveBeenCalled();
expect(logger.warn).toHaveBeenCalled();
});
it("returns false when the directory response carries no room_id", async () => {
mockGotGet.mockResolvedValue({ body: {} });
const { provider, logger } = createProvider();
expect(await provider.sendTestAlert(makeNotification({ roomId: "#alerts:example.com" }))).toBe(false);
expect(mockGotPut).not.toHaveBeenCalled();
expect(logger.warn).toHaveBeenCalled();
});
it("strips a trailing slash from the homeserver URL", async () => {
const { provider } = createProvider();
await provider.sendTestAlert(makeNotification({ homeserverUrl: "https://matrix.example.com/" }));
expect(putUrl()).toContain("https://matrix.example.com/_matrix/");
expect(putUrl()).not.toContain("com//_matrix");
});
it("resolves aliases for real alerts, not just test alerts", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification({ roomId: "#alerts:example.com" }) as any, makeMessage());
expect(mockGotGet).toHaveBeenCalled();
expect(putUrl()).toContain("/rooms/!resolved%3Aexample.com/send/");
});
});
});