mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
feat: unify alerts into one plugin, channels, rules and inbox
This commit is contained in:
1 parent
fbe736bc20
commit
7c02daccf4
129 files changed
+20957
-2687
No files matched your search
@@ -1,93 +0,0 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { TestSqliteDatabase } from "./test-support.js";
|
||||
import { DismissedAlertRepository } from "../../../database/repositories/dismissed-alert-repository.js";
|
||||
|
||||
describe("DismissedAlertRepository", () => {
|
||||
let adapter: TestSqliteDatabase | null = null;
|
||||
|
||||
afterEach(async () => {
|
||||
if (adapter) {
|
||||
await adapter.close();
|
||||
adapter = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function createRepository(
|
||||
onWrite?: () => void | Promise<void>,
|
||||
): Promise<DismissedAlertRepository> {
|
||||
adapter = new TestSqliteDatabase();
|
||||
const context = await adapter.connect();
|
||||
await adapter.exec(`
|
||||
INSERT INTO users (id, username, password_hash)
|
||||
VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash');
|
||||
`);
|
||||
|
||||
return new DismissedAlertRepository(context, onWrite);
|
||||
}
|
||||
|
||||
it("creates, lists, and finds dismissed alerts by user", async () => {
|
||||
const repo = await createRepository();
|
||||
|
||||
await repo.create("user-1", "alert-1");
|
||||
await repo.create("user-1", "alert-2");
|
||||
await repo.create("user-2", "alert-3");
|
||||
|
||||
expect(await repo.listAlertIdsByUserId("user-1")).toEqual([
|
||||
"alert-1",
|
||||
"alert-2",
|
||||
]);
|
||||
expect((await repo.findForUser("user-1", "alert-1"))?.alertId).toBe(
|
||||
"alert-1",
|
||||
);
|
||||
expect(await repo.findForUser("user-1", "alert-3")).toBeNull();
|
||||
expect(
|
||||
(await repo.listByUserId("user-1")).map((row) => row.alertId),
|
||||
).toEqual(["alert-1", "alert-2"]);
|
||||
});
|
||||
|
||||
it("creates import alerts without duplicating user alert pairs", async () => {
|
||||
let writeCount = 0;
|
||||
const repo = await createRepository(() => {
|
||||
writeCount += 1;
|
||||
});
|
||||
|
||||
await expect(
|
||||
repo.createForImport("user-1", "alert-1", "2026-01-01T00:00:00.000Z"),
|
||||
).resolves.toBe(true);
|
||||
await expect(
|
||||
repo.createForImport("user-1", "alert-1", "2026-01-02T00:00:00.000Z"),
|
||||
).resolves.toBe(false);
|
||||
|
||||
const alerts = await repo.listByUserId("user-1");
|
||||
expect(alerts).toHaveLength(1);
|
||||
expect(alerts[0]).toMatchObject({
|
||||
alertId: "alert-1",
|
||||
dismissedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
expect(writeCount).toBe(1);
|
||||
});
|
||||
|
||||
it("deletes dismissed alerts and only triggers writes for changed rows", async () => {
|
||||
let writeCount = 0;
|
||||
const repo = await createRepository(() => {
|
||||
writeCount += 1;
|
||||
});
|
||||
|
||||
await repo.create("user-1", "alert-1");
|
||||
await repo.create("user-1", "alert-2");
|
||||
await repo.create("user-2", "alert-3");
|
||||
expect(writeCount).toBe(3);
|
||||
|
||||
expect(await repo.deleteForUser("user-1", "missing")).toBe(false);
|
||||
expect(writeCount).toBe(3);
|
||||
|
||||
expect(await repo.deleteForUser("user-1", "alert-1")).toBe(true);
|
||||
expect(writeCount).toBe(4);
|
||||
expect(await repo.listAlertIdsByUserId("user-1")).toEqual(["alert-2"]);
|
||||
|
||||
expect(await repo.deleteByUserId("user-1")).toBe(1);
|
||||
expect(writeCount).toBe(5);
|
||||
expect(await repo.deleteByUserId("user-1")).toBe(0);
|
||||
expect(writeCount).toBe(5);
|
||||
});
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
/**
|
||||
* POST /notification-channels/:id/test sends through deliverNotification,
|
||||
* the same path an automation's notify step takes.
|
||||
*/
|
||||
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const h = vi.hoisted(() => ({
|
||||
deliver: vi.fn(),
|
||||
channel: null as null | { id: number; type: string; config: string },
|
||||
}));
|
||||
|
||||
vi.mock("../../../utils/auth-manager.js", () => ({
|
||||
AuthManager: {
|
||||
getInstance: () => ({
|
||||
createAuthMiddleware:
|
||||
() => (req: { userId?: string }, _res: unknown, next: () => void) => {
|
||||
req.userId = "user-1";
|
||||
next();
|
||||
},
|
||||
}),
|
||||
},
|
||||
}));
|
||||
vi.mock("../../../database/repositories/factory.js", () => ({
|
||||
createCurrentNotificationChannelRepository: () => ({
|
||||
findNotificationChannelForUser: async () => h.channel,
|
||||
}),
|
||||
}));
|
||||
vi.mock("../../../utils/notification-sender.js", () => ({
|
||||
deliverNotification: h.deliver,
|
||||
}));
|
||||
|
||||
const { default: router } =
|
||||
await import("../../../database/routes/notification-channels-routes.js");
|
||||
const app = express().use(express.json()).use(router);
|
||||
|
||||
beforeEach(() => {
|
||||
h.deliver.mockReset();
|
||||
h.channel = { id: 3, type: "ntfy", config: '{"url":"https://ntfy.sh"}' };
|
||||
});
|
||||
|
||||
describe("POST /notification-channels/:id/test", () => {
|
||||
it("delivers a test notification to the channel", async () => {
|
||||
h.deliver.mockResolvedValue(undefined);
|
||||
const response = await request(app).post("/notification-channels/3/test");
|
||||
expect(response.body).toEqual({ success: true });
|
||||
expect(h.deliver).toHaveBeenCalledWith(
|
||||
h.channel,
|
||||
expect.objectContaining({ severity: "info" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports the delivery error", async () => {
|
||||
h.deliver.mockRejectedValue(new Error("ntfy channel is missing a topic"));
|
||||
const response = await request(app).post("/notification-channels/3/test");
|
||||
expect(response.body).toEqual({
|
||||
success: false,
|
||||
error: "ntfy channel is missing a topic",
|
||||
});
|
||||
});
|
||||
|
||||
it("answers 404 for a channel the user does not own", async () => {
|
||||
h.channel = null;
|
||||
const response = await request(app).post("/notification-channels/9/test");
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
@@ -29,7 +29,6 @@ vi.mock("../../../database/repositories/factory.js", () => ({
|
||||
createCurrentSettingsRepository: () => ({}),
|
||||
createCurrentSshCredentialUsageRepository: deletingRepo("usage"),
|
||||
createCurrentRecentActivityRepository: deletingRepo("activity"),
|
||||
createCurrentDismissedAlertRepository: deletingRepo("alerts"),
|
||||
createCurrentHostRepository: deletingRepo("hosts"),
|
||||
createCurrentCredentialRepository: deletingRepo("credentials"),
|
||||
}));
|
||||
@@ -126,7 +125,6 @@ describe("resetUserPassword", () => {
|
||||
expect(calls.deletedFor).toEqual([
|
||||
"usage:user-1",
|
||||
"activity:user-1",
|
||||
"alerts:user-1",
|
||||
"hosts:user-1",
|
||||
"credentials:user-1",
|
||||
]);
|
||||
|
||||
Reference in new issue
Block a user