Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/notifications/services/.server/NotificationService.test.ts
T

147 lines
4.7 KiB
TypeScript

// @vitest-environment node
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@toolctrl/queue", () => ({
buildNotificationJob: vi.fn((payload) => ({ name: "notification", payload })),
enqueueJob: vi.fn(),
queueNames: { notifications: "notifications" },
resolveRedisConnectionOptions: vi.fn(() => undefined),
}));
vi.mock("@toolctrl/queue/runtime", () => ({
createQueue: vi.fn(() => ({ close: vi.fn(async () => undefined) })),
}));
vi.mock("@toolctrl/db/notifications.server", () => ({
createNotificationDelivery: vi.fn(async (input) => ({ id: "delivery_1", ...input })),
redactNotificationDiagnosticText: vi.fn((value: string) => value),
}));
vi.mock("~/utils/db/permissions/roles.db.server", () => ({
getRolesByName: vi.fn(async () => []),
}));
vi.mock("~/utils/db/permissions/userRoles.db.server", () => ({
getUsersRolesInTenant: vi.fn(async () => []),
}));
vi.mock("~/utils/db/users.db.server", () => ({
getAdminUsersInRoles: vi.fn(async () => []),
}));
import { createNotificationDelivery } from "@toolctrl/db/notifications.server";
import { enqueueJob, resolveRedisConnectionOptions } from "@toolctrl/queue";
import NotificationService, { mapNotificationChannelToCategory } from "./NotificationService";
const user = {
id: "user_1",
email: "user@example.test",
firstName: "Ada",
lastName: "Lovelace",
phone: null,
defaultTenantId: "tenant_1",
};
describe("NotificationService", () => {
beforeEach(() => {
vi.clearAllMocks();
delete process.env.NOTIFICATIONS_NOVU_API_KEY;
vi.unstubAllGlobals();
});
it("records skipped deliveries when the queue is not configured", async () => {
await NotificationService.send({
channel: "admin-users",
tenantId: "tenant_1",
to: user,
notification: { message: "Profile changed" },
});
expect(createNotificationDelivery).toHaveBeenCalledWith({
tenantId: "tenant_1",
userId: "user_1",
channel: "in-app",
category: "team",
messageKey: "admin-users",
status: "SKIPPED",
lastError: "Notification queue is not configured.",
payloadMetadata: {
source: "legacy-notification-service",
reason: "queue-not-configured",
},
});
});
it("records skipped deliveries when a configured queue fails", async () => {
vi.mocked(resolveRedisConnectionOptions).mockReturnValue({ host: "127.0.0.1", port: 6379 } as any);
vi.mocked(enqueueJob).mockRejectedValue(new Error("Redis connection failed"));
await NotificationService.send({
channel: "admin-users",
tenantId: "tenant_1",
to: user,
notification: { message: "Profile changed" },
});
expect(createNotificationDelivery).toHaveBeenCalledWith({
tenantId: "tenant_1",
userId: "user_1",
channel: "in-app",
category: "team",
messageKey: "admin-users",
status: "SKIPPED",
lastError: "Redis connection failed",
payloadMetadata: {
source: "legacy-notification-service",
reason: "queue-enqueue-failed",
},
});
});
it("maps subscription notifications to billing preferences", () => {
expect(mapNotificationChannelToCategory("admin-subscriptions")).toBe("billing");
});
it("returns null for Novu provider error responses", async () => {
process.env.NOTIFICATIONS_NOVU_API_KEY = "novu-secret";
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ message: "unauthorized" }), { status: 401 }));
vi.stubGlobal("fetch", fetchMock);
await expect(NotificationService.getSubscribers({})).resolves.toBeNull();
expect(fetchMock).toHaveBeenCalledWith(
"https://api.novu.co/v1/subscribers?page=0",
expect.objectContaining({
headers: expect.objectContaining({ Authorization: "ApiKey novu-secret" }),
})
);
});
it("passes tenantId through to enqueueJob so the priority resolver can run", async () => {
vi.mocked(resolveRedisConnectionOptions).mockReturnValue({ host: "127.0.0.1", port: 6379 } as any);
vi.mocked(enqueueJob).mockResolvedValue({ id: "job_priority_1" } as any);
await NotificationService.send({
channel: "admin-users",
tenantId: "tenant_priority_1",
to: user,
notification: { message: "Profile changed" },
});
const lastCall = vi.mocked(enqueueJob).mock.calls.at(-1);
expect(lastCall?.[2]).toMatchObject({ tenantId: "tenant_priority_1" });
});
it("returns null for Novu provider network failures", async () => {
process.env.NOTIFICATIONS_NOVU_API_KEY = "novu-secret";
vi.stubGlobal(
"fetch",
vi.fn(async () => {
throw new Error("network down");
})
);
await expect(NotificationService.getIntegrations()).resolves.toBeNull();
});
});