147 lines
4.7 KiB
TypeScript
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();
|
|
});
|
|
});
|