/** * The operator's instance-wide channel switches, against a real settings row. * * The unit suite stubs `prisma.appSettings.findUnique`; this one writes the * actual `app_settings` singleton, dispatches through the real cascade, and * reads the `push_attempts` ledger back out of Postgres. It is the layer that * proves the column the admin route writes is the column the dispatcher * reads — a unit mock would agree with itself even if those two drifted apart. */ import { beforeEach, describe, expect, it, vi } from "vitest"; process.env.ENCRYPTION_KEY ??= "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; const ntfySendMock = vi.fn(); vi.mock("@/lib/notifications/senders/ntfy", () => ({ sendViaNtfy: (...args: unknown[]) => ntfySendMock(...args), })); vi.mock("@/lib/db-compat", () => ({ ensureDbCompatibility: vi.fn().mockResolvedValue(undefined), })); import { getPrismaClient, truncateAllTables } from "./setup"; const TEST_USER_ID = "user-global-channel-switch"; async function seedNtfyChannel(): Promise { const { encrypt } = await import("@/lib/crypto"); await getPrismaClient().notificationChannel.create({ data: { userId: TEST_USER_ID, type: "NTFY", enabled: true, config: encrypt( JSON.stringify({ serverUrl: "https://ntfy.example", topic: "health" }), ), }, }); } async function setNtfyGlobal(ntfyGlobal: boolean): Promise { await getPrismaClient().appSettings.upsert({ where: { id: "singleton" }, create: { id: "singleton", ntfyGlobal }, update: { ntfyGlobal }, }); } async function dispatch() { const { dispatchNotification } = await import("@/lib/notifications/dispatcher"); return dispatchNotification({ userId: TEST_USER_ID, eventType: "MEASUREMENT_REMINDER", title: "Time to weigh in", message: "Your morning reading is due", }); } /** The ledger write is fire-and-forget; let its microtask land. */ async function settleLedger(): Promise { await new Promise((resolve) => setTimeout(resolve, 50)); } beforeEach(async () => { vi.clearAllMocks(); await truncateAllTables(getPrismaClient()); await getPrismaClient().user.create({ data: { id: TEST_USER_ID, username: "global-channel-switch", email: "global-channel-switch@example.test", }, }); }); describe("instance-wide notification channel switch", () => { it("delivers nothing and records the skip when the operator switched ntfy off", async () => { await seedNtfyChannel(); await setNtfyGlobal(false); const outcome = await dispatch(); await settleLedger(); expect(ntfySendMock).not.toHaveBeenCalled(); expect(outcome).toEqual({ dispatched: false, channelsAttempted: 0, channelsSucceeded: 0, }); const attempts = await getPrismaClient().pushAttempt.findMany({ where: { userId: TEST_USER_ID }, }); expect(attempts).toHaveLength(1); expect(attempts[0]).toMatchObject({ channel: "NTFY", eventType: "MEASUREMENT_REMINDER", result: "skipped", reason: "globally_disabled", }); // The switch is the operator's, not the user's: the account's own // channel row is left enabled so flipping the switch back restores // delivery without every user having to re-enable anything. const channel = await getPrismaClient().notificationChannel.findFirst({ where: { userId: TEST_USER_ID, type: "NTFY" }, }); expect(channel?.enabled).toBe(true); }); it("delivers on the same channel once the switch is back on", async () => { await seedNtfyChannel(); await setNtfyGlobal(true); ntfySendMock.mockResolvedValueOnce({ ok: true }); const outcome = await dispatch(); expect(ntfySendMock).toHaveBeenCalledTimes(1); expect(outcome).toEqual({ dispatched: true, channelsAttempted: 1, channelsSucceeded: 1, }); }); it("delivers when no settings row exists at all", async () => { await seedNtfyChannel(); ntfySendMock.mockResolvedValueOnce({ ok: true }); const outcome = await dispatch(); expect(ntfySendMock).toHaveBeenCalledTimes(1); expect(outcome.dispatched).toBe(true); }); });