import { SystemRole } from "@capakraken/shared"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { notificationRouter } from "../router/notification.js"; import { emitNotificationCreated, emitTaskAssigned, emitTaskCompleted, emitTaskStatusChanged, } from "../sse/event-bus.js"; import { createCallerFactory } from "../trpc.js"; const { resolveRecipientsMock } = vi.hoisted(() => ({ resolveRecipientsMock: vi.fn(), })); const { sendEmailMock } = vi.hoisted(() => ({ sendEmailMock: vi.fn(), })); // Mock the SSE event bus — we don't test real event emission here vi.mock("../sse/event-bus.js", () => ({ emitNotificationCreated: vi.fn(), emitTaskAssigned: vi.fn(), emitTaskCompleted: vi.fn(), emitTaskStatusChanged: vi.fn(), })); vi.mock("../lib/notification-targeting.js", () => ({ resolveRecipients: resolveRecipientsMock, })); vi.mock("../lib/email.js", () => ({ sendEmail: sendEmailMock, })); const createCaller = createCallerFactory(notificationRouter); beforeEach(() => { vi.clearAllMocks(); resolveRecipientsMock.mockReset(); sendEmailMock.mockReset(); }); // ── Caller factories ───────────────────────────────────────────────────────── function createProtectedCaller(db: Record) { return createCaller({ session: { user: { email: "user@example.com", name: "User", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_1", systemRole: SystemRole.USER, permissionOverrides: null, }, }); } function createManagerCaller(db: Record) { return createCaller({ session: { user: { email: "mgr@example.com", name: "Manager", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_mgr", systemRole: SystemRole.MANAGER, permissionOverrides: null, }, }); } function createAdminCaller(db: Record) { return createCaller({ session: { user: { email: "admin@example.com", name: "Admin", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_admin", systemRole: SystemRole.ADMIN, permissionOverrides: null, }, }); } // ── Sample data ────────────────────────────────────────────────────────────── function sampleNotification(overrides: Record = {}) { return { id: "notif_1", userId: "user_1", type: "VACATION_APPROVED", title: "Vacation approved", body: null, entityId: null, entityType: null, readAt: null, createdAt: new Date("2026-01-15T10:00:00Z"), ...overrides, }; } /** DB mock that resolves the session user for resolveUserId */ function withUserLookup(db: Record, userId = "user_1") { return { user: { findUnique: vi.fn().mockResolvedValue({ id: userId }), }, ...db, }; } // ─── list ──────────────────────────────────────────────────────────────────── describe("notification.list", () => { it("returns notifications for the current user", async () => { const notifications = [sampleNotification(), sampleNotification({ id: "notif_2" })]; const db = withUserLookup({ notification: { findMany: vi.fn().mockResolvedValue(notifications), }, }); const caller = createProtectedCaller(db); const result = await caller.list({ limit: 50 }); expect(result).toHaveLength(2); expect(db.notification.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ userId: "user_1" }), orderBy: { createdAt: "desc" }, take: 50, }), ); }); it("filters to unread only when unreadOnly is true", async () => { const db = withUserLookup({ notification: { findMany: vi.fn().mockResolvedValue([]), }, }); const caller = createProtectedCaller(db); await caller.list({ unreadOnly: true, limit: 10 }); expect(db.notification.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ userId: "user_1", readAt: null }), take: 10, }), ); }); }); // ─── unreadCount ───────────────────────────────────────────────────────────── describe("notification.unreadCount", () => { it("returns count of unread notifications", async () => { const db = withUserLookup({ notification: { count: vi.fn().mockResolvedValue(5), }, }); const caller = createProtectedCaller(db); const result = await caller.unreadCount(); expect(result).toBe(5); expect(db.notification.count).toHaveBeenCalledWith( expect.objectContaining({ where: { userId: "user_1", readAt: null }, }), ); }); }); // ─── markRead ──────────────────────────────────────────────────────────────── describe("notification.markRead", () => { it("marks a single notification as read when id is provided", async () => { const db = withUserLookup({ notification: { update: vi.fn().mockResolvedValue(sampleNotification({ readAt: new Date() })), updateMany: vi.fn(), }, }); const caller = createProtectedCaller(db); await caller.markRead({ id: "notif_1" }); expect(db.notification.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "notif_1", userId: "user_1" }, data: expect.objectContaining({ readAt: expect.any(Date) }), }), ); expect(db.notification.updateMany).not.toHaveBeenCalled(); }); it("marks all unread notifications as read when no id is provided", async () => { const db = withUserLookup({ notification: { update: vi.fn(), updateMany: vi.fn().mockResolvedValue({ count: 3 }), }, }); const caller = createProtectedCaller(db); await caller.markRead({}); expect(db.notification.updateMany).toHaveBeenCalledWith( expect.objectContaining({ where: { userId: "user_1", readAt: null }, data: expect.objectContaining({ readAt: expect.any(Date) }), }), ); expect(db.notification.update).not.toHaveBeenCalled(); }); }); // ─── create ────────────────────────────────────────────────────────────────── describe("notification.create", () => { it("creates a notification (manager role)", async () => { const created = sampleNotification({ userId: "target_user" }); const db = withUserLookup( { notification: { create: vi.fn().mockResolvedValue(created), findUnique: vi.fn().mockResolvedValue(created), }, }, "user_mgr", ); const caller = createManagerCaller(db); const result = await caller.create({ userId: "target_user", type: "INFO", title: "Test notification", }); expect(result.id).toBe("notif_1"); expect(db.notification.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ userId: "target_user", type: "INFO", title: "Test notification", }), }), ); expect(db.notification.findUnique).toHaveBeenCalledWith({ where: { id: "notif_1" } }); }); it("creates a notification with optional fields", async () => { const created = sampleNotification({ userId: "target_user", body: "Details here", entityId: "proj_1", entityType: "PROJECT", }); const db = withUserLookup( { notification: { create: vi.fn().mockResolvedValue(created), findUnique: vi.fn().mockResolvedValue(created), }, }, "user_mgr", ); const caller = createManagerCaller(db); await caller.create({ userId: "target_user", type: "INFO", title: "Test", body: "Details here", entityId: "proj_1", entityType: "PROJECT", }); expect(db.notification.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ body: "Details here", entityId: "proj_1", entityType: "PROJECT", }), }), ); }); it("defaults task-like managed notifications to OPEN when no taskStatus is provided", async () => { const created = sampleNotification({ userId: "target_user", category: "TASK", taskStatus: "OPEN", }); const db = withUserLookup( { notification: { create: vi.fn().mockResolvedValue(created), findUnique: vi.fn().mockResolvedValue(created), }, }, "user_mgr", ); const caller = createManagerCaller(db); const result = await caller.create({ userId: "target_user", type: "TASK_CREATED", title: "Review proposal", category: "TASK", }); expect(db.notification.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ category: "TASK", taskStatus: "OPEN", }), }), ); expect(result).toMatchObject({ id: "notif_1", category: "TASK", taskStatus: "OPEN", }); }); it("rejects creation by a regular user (FORBIDDEN)", async () => { const db = withUserLookup({ notification: { create: vi.fn(), }, }); const caller = createProtectedCaller(db); await expect( caller.create({ userId: "target", type: "INFO", title: "Nope" }), ).rejects.toThrow(); }); it("maps missing notification recipients to a not found error", async () => { const db = withUserLookup( { notification: { create: vi.fn().mockRejectedValue({ code: "P2003", message: "Foreign key constraint failed", meta: { field_name: "Notification_userId_fkey" }, }), findUnique: vi.fn(), }, }, "user_mgr", ); const caller = createManagerCaller(db); await expect(caller.create({ userId: "user_missing", type: "INFO", title: "Test notification", })).rejects.toMatchObject({ code: "NOT_FOUND", message: "Notification recipient user not found", }); expect(db.notification.findUnique).not.toHaveBeenCalled(); expect(emitNotificationCreated).not.toHaveBeenCalled(); }); }); // ─── createBroadcast ──────────────────────────────────────────────────────── describe("notification.createBroadcast", () => { const FUTURE_SCHEDULED_AT = new Date("2099-04-01T10:00:00Z"); it("rejects broadcasts when no recipients match the target", async () => { resolveRecipientsMock.mockResolvedValue([]); const create = vi.fn().mockResolvedValue({ id: "broadcast_1", title: "Ops update", createdAt: new Date("2026-03-30T10:00:00Z"), }); const update = vi.fn(); const db = { notificationBroadcast: { create, update, }, }; const caller = createManagerCaller(db); await expect(caller.createBroadcast({ title: "Ops update", targetType: "all", })).rejects.toMatchObject({ code: "BAD_REQUEST", message: "No recipients matched the broadcast target.", }); expect(create).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); expect(resolveRecipientsMock).toHaveBeenCalledWith( "all", undefined, db, "user_mgr", ); }); it("rejects immediate broadcasts when transactional persistence support is unavailable", async () => { resolveRecipientsMock.mockResolvedValue(["user_a", "user_b"]); const create = vi.fn(); const update = vi.fn(); const createNotification = vi.fn(); const db = { notificationBroadcast: { create, update, }, notification: { create: createNotification, }, }; const caller = createManagerCaller(db); await expect(caller.createBroadcast({ title: "Ops update", targetType: "all", })).rejects.toMatchObject({ code: "INTERNAL_SERVER_ERROR", message: "Immediate broadcasts require transactional persistence support.", }); expect(resolveRecipientsMock).toHaveBeenCalledWith( "all", undefined, db, "user_mgr", ); expect(create).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); expect(createNotification).not.toHaveBeenCalled(); expect(emitNotificationCreated).not.toHaveBeenCalled(); expect(emitTaskAssigned).not.toHaveBeenCalled(); }); it("rejects broadcasts with no recipients before opening a broadcast transaction", async () => { resolveRecipientsMock.mockResolvedValue([]); const txCreateBroadcast = vi.fn(); const txUpdateBroadcast = vi.fn(); const tx = { notificationBroadcast: { create: txCreateBroadcast, update: txUpdateBroadcast, }, }; const transaction = vi.fn(async (callback: (db: typeof tx) => Promise) => callback(tx)); const create = vi.fn(); const update = vi.fn(); const db = { $transaction: transaction, notificationBroadcast: { create, update, }, }; const caller = createManagerCaller(db); await expect(caller.createBroadcast({ title: "Ops update", targetType: "all", })).rejects.toMatchObject({ code: "BAD_REQUEST", message: "No recipients matched the broadcast target.", }); expect(transaction).not.toHaveBeenCalled(); expect(create).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); expect(txCreateBroadcast).not.toHaveBeenCalled(); expect(txUpdateBroadcast).not.toHaveBeenCalled(); }); it("rejects scheduled broadcasts when no recipients match the target", async () => { resolveRecipientsMock.mockResolvedValue([]); const create = vi.fn(); const update = vi.fn(); const db = { notificationBroadcast: { create, update, }, }; const caller = createManagerCaller(db); await expect(caller.createBroadcast({ title: "Ops update", targetType: "all", scheduledAt: FUTURE_SCHEDULED_AT, })).rejects.toMatchObject({ code: "BAD_REQUEST", message: "No recipients matched the broadcast target.", }); expect(create).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); expect(resolveRecipientsMock).toHaveBeenCalledWith( "all", undefined, db, "user_mgr", ); }); it("rejects scheduled broadcasts with task metadata before persisting anything", async () => { resolveRecipientsMock.mockResolvedValue(["user_a"]); const create = vi.fn(); const update = vi.fn(); const db = { notificationBroadcast: { create, update, }, }; const caller = createManagerCaller(db); await expect(caller.createBroadcast({ title: "Approval later", targetType: "all", category: "TASK", taskAction: "approve_vacation:vac_1", dueDate: new Date("2099-04-02T10:00:00Z"), scheduledAt: FUTURE_SCHEDULED_AT, })).rejects.toMatchObject({ code: "BAD_REQUEST", message: "Scheduled broadcasts with task metadata are not supported yet.", }); expect(resolveRecipientsMock).not.toHaveBeenCalled(); expect(create).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); }); it("does not partially persist an immediate broadcast when recipient fan-out fails", async () => { resolveRecipientsMock.mockResolvedValue(["user_a", "user_b"]); const txCreateBroadcast = vi.fn().mockResolvedValue({ id: "broadcast_tx_1", title: "Ops update", createdAt: new Date("2026-03-30T10:00:00Z"), }); const txUpdateBroadcast = vi.fn(); const txCreateNotification = vi.fn() .mockResolvedValueOnce({ id: "notif_a", userId: "user_a" }) .mockRejectedValueOnce(new Error("fan-out failed")); const tx = { notificationBroadcast: { create: txCreateBroadcast, update: txUpdateBroadcast, }, notification: { create: txCreateNotification, }, }; const outerCreateBroadcast = vi.fn(); const outerUpdateBroadcast = vi.fn(); const outerCreateNotification = vi.fn(); const transaction = vi.fn(async (callback: (db: typeof tx) => Promise) => callback(tx)); const db = { $transaction: transaction, notificationBroadcast: { create: outerCreateBroadcast, update: outerUpdateBroadcast, }, notification: { create: outerCreateNotification, }, }; const caller = createManagerCaller(db); await expect(caller.createBroadcast({ title: "Ops update", targetType: "all", })).rejects.toThrow("fan-out failed"); expect(transaction).toHaveBeenCalledTimes(1); expect(txCreateBroadcast).toHaveBeenCalledTimes(1); expect(txCreateNotification).toHaveBeenCalledTimes(2); expect(txUpdateBroadcast).not.toHaveBeenCalled(); expect(outerCreateBroadcast).not.toHaveBeenCalled(); expect(outerUpdateBroadcast).not.toHaveBeenCalled(); expect(outerCreateNotification).not.toHaveBeenCalled(); }); it("maps broadcast source reference loss during recipient fan-out to not found errors", async () => { resolveRecipientsMock.mockResolvedValue(["user_a", "user_b"]); const txCreateBroadcast = vi.fn().mockResolvedValue({ id: "broadcast_tx_source_missing", title: "Ops update", createdAt: new Date("2026-03-30T10:00:00Z"), }); const txUpdateBroadcast = vi.fn(); const txCreateNotification = vi.fn().mockRejectedValue( Object.assign(new Error("Foreign key constraint failed"), { code: "P2003", meta: { field_name: "Notification_sourceId_fkey" }, }), ); const tx = { notificationBroadcast: { create: txCreateBroadcast, update: txUpdateBroadcast, }, notification: { create: txCreateNotification, }, }; const outerCreateBroadcast = vi.fn(); const outerUpdateBroadcast = vi.fn(); const outerCreateNotification = vi.fn(); const transaction = vi.fn(async (callback: (db: typeof tx) => Promise) => callback(tx)); const db = { $transaction: transaction, notificationBroadcast: { create: outerCreateBroadcast, update: outerUpdateBroadcast, }, notification: { create: outerCreateNotification, }, }; const caller = createManagerCaller(db); await expect(caller.createBroadcast({ title: "Ops update", targetType: "all", })).rejects.toMatchObject({ code: "NOT_FOUND", message: "Notification broadcast not found", }); expect(transaction).toHaveBeenCalledTimes(1); expect(txCreateBroadcast).toHaveBeenCalledTimes(1); // Parallel fan-out: both recipients are attempted concurrently expect(txCreateNotification).toHaveBeenCalledTimes(2); expect(txUpdateBroadcast).not.toHaveBeenCalled(); expect(outerCreateBroadcast).not.toHaveBeenCalled(); expect(outerUpdateBroadcast).not.toHaveBeenCalled(); expect(outerCreateNotification).not.toHaveBeenCalled(); expect(emitNotificationCreated).not.toHaveBeenCalled(); expect(emitTaskAssigned).not.toHaveBeenCalled(); }); it("maps missing broadcast recipients during fan-out to not found errors", async () => { resolveRecipientsMock.mockResolvedValue(["user_a", "user_missing"]); const txCreateBroadcast = vi.fn().mockResolvedValue({ id: "broadcast_tx_missing_recipient", title: "Ops update", createdAt: new Date("2026-03-30T10:00:00Z"), }); const txUpdateBroadcast = vi.fn(); const txCreateNotification = vi.fn() .mockResolvedValueOnce({ id: "notif_a", userId: "user_a" }) .mockRejectedValueOnce( Object.assign(new Error("Foreign key constraint failed"), { code: "P2003", meta: { field_name: "Notification_userId_fkey" }, }), ); const tx = { notificationBroadcast: { create: txCreateBroadcast, update: txUpdateBroadcast, }, notification: { create: txCreateNotification, }, }; const outerCreateBroadcast = vi.fn(); const outerUpdateBroadcast = vi.fn(); const outerCreateNotification = vi.fn(); const db = { $transaction: vi.fn(async (callback: (db: typeof tx) => Promise) => callback(tx)), user: { findUnique: vi.fn(), }, notificationBroadcast: { create: outerCreateBroadcast, update: outerUpdateBroadcast, }, notification: { create: outerCreateNotification, }, }; const caller = createManagerCaller(db); await expect(caller.createBroadcast({ title: "Ops update", body: "Email everyone", channel: "both", targetType: "all", })).rejects.toMatchObject({ code: "NOT_FOUND", message: "Broadcast recipient user not found", }); await Promise.resolve(); expect(txCreateBroadcast).toHaveBeenCalledTimes(1); expect(txCreateNotification).toHaveBeenCalledTimes(2); expect(txUpdateBroadcast).not.toHaveBeenCalled(); expect(outerCreateBroadcast).not.toHaveBeenCalled(); expect(outerUpdateBroadcast).not.toHaveBeenCalled(); expect(outerCreateNotification).not.toHaveBeenCalled(); expect(db.user.findUnique).not.toHaveBeenCalled(); expect(sendEmailMock).not.toHaveBeenCalled(); expect(emitNotificationCreated).not.toHaveBeenCalled(); expect(emitTaskAssigned).not.toHaveBeenCalled(); }); it("emits recipient SSE only after an immediate broadcast commits", async () => { resolveRecipientsMock.mockResolvedValue(["user_a", "user_b"]); const txCreateBroadcast = vi.fn().mockResolvedValue({ id: "broadcast_tx_1", title: "Ops update", createdAt: new Date("2026-03-30T10:00:00Z"), }); const txCreateNotification = vi.fn() .mockResolvedValueOnce({ id: "notif_a", userId: "user_a" }) .mockResolvedValueOnce({ id: "notif_b", userId: "user_b" }); const txUpdateBroadcast = vi.fn().mockResolvedValue({ id: "broadcast_tx_1", title: "Ops update", createdAt: new Date("2026-03-30T10:00:00Z"), sentAt: new Date("2026-03-30T10:01:00Z"), recipientCount: 2, }); const tx = { notificationBroadcast: { create: txCreateBroadcast, update: txUpdateBroadcast, }, notification: { create: txCreateNotification, }, }; const transaction = vi.fn(async (callback: (db: typeof tx) => Promise) => callback(tx)); const db = { $transaction: transaction, notificationBroadcast: { create: vi.fn(), update: vi.fn(), }, notification: { create: vi.fn(), }, }; const caller = createManagerCaller(db); const result = await caller.createBroadcast({ title: "Ops update", category: "TASK", targetType: "all", taskAction: "acknowledge", }); expect(transaction).toHaveBeenCalledTimes(1); expect(txCreateBroadcast).toHaveBeenCalledTimes(1); expect(txCreateNotification).toHaveBeenCalledTimes(2); expect(txUpdateBroadcast).toHaveBeenCalledWith({ where: { id: "broadcast_tx_1" }, data: { sentAt: expect.any(Date), recipientCount: 2, }, }); expect(emitNotificationCreated).toHaveBeenNthCalledWith(1, "user_a", "notif_a"); expect(emitNotificationCreated).toHaveBeenNthCalledWith(2, "user_b", "notif_b"); expect(emitTaskAssigned).toHaveBeenNthCalledWith(1, "user_a", "notif_a"); expect(emitTaskAssigned).toHaveBeenNthCalledWith(2, "user_b", "notif_b"); expect(result).toMatchObject({ id: "broadcast_tx_1", recipientCount: 2, }); }); it("does not emit recipient SSE when a broadcast is only scheduled", async () => { resolveRecipientsMock.mockResolvedValue(["user_a", "user_b"]); const create = vi.fn().mockResolvedValue({ id: "broadcast_sched_1", title: "Scheduled ops update", createdAt: new Date("2026-03-30T10:00:00Z"), scheduledAt: FUTURE_SCHEDULED_AT, recipientCount: 2, }); const db = { notificationBroadcast: { create, update: vi.fn(), }, notification: { create: vi.fn(), }, }; const caller = createManagerCaller(db); const result = await caller.createBroadcast({ title: "Scheduled ops update", targetType: "all", scheduledAt: FUTURE_SCHEDULED_AT, }); expect(create).toHaveBeenCalledWith({ data: expect.objectContaining({ senderId: "user_mgr", title: "Scheduled ops update", targetType: "all", scheduledAt: FUTURE_SCHEDULED_AT, recipientCount: 2, }), }); expect(db.notification.create).not.toHaveBeenCalled(); expect(emitNotificationCreated).not.toHaveBeenCalled(); expect(emitTaskAssigned).not.toHaveBeenCalled(); expect(result).toMatchObject({ id: "broadcast_sched_1", scheduledAt: FUTURE_SCHEDULED_AT, recipientCount: 2, }); }); it("sends broadcast emails only after an immediate broadcast transaction commits", async () => { resolveRecipientsMock.mockResolvedValue(["user_a", "user_b"]); const txCreateBroadcast = vi.fn().mockResolvedValue({ id: "broadcast_tx_email_1", title: "Ops update", createdAt: new Date("2026-03-30T10:00:00Z"), }); const txCreateNotification = vi.fn() .mockResolvedValueOnce({ id: "notif_a", userId: "user_a" }) .mockResolvedValueOnce({ id: "notif_b", userId: "user_b" }); const txUpdateBroadcast = vi.fn().mockResolvedValue({ id: "broadcast_tx_email_1", title: "Ops update", createdAt: new Date("2026-03-30T10:00:00Z"), sentAt: new Date("2026-03-30T10:01:00Z"), recipientCount: 2, }); const tx = { notificationBroadcast: { create: txCreateBroadcast, update: txUpdateBroadcast, }, notification: { create: txCreateNotification, }, }; let transactionCommitted = false; const transaction = vi.fn(async (callback: (db: typeof tx) => Promise) => { const result = await callback(tx); expect(sendEmailMock).not.toHaveBeenCalled(); transactionCommitted = true; return result; }); const db = { $transaction: transaction, user: { findUnique: vi.fn() .mockResolvedValueOnce({ email: "user-a@example.com", name: "User A" }) .mockResolvedValueOnce({ email: "user-b@example.com", name: "User B" }), }, notificationBroadcast: { create: vi.fn(), update: vi.fn(), }, notification: { create: vi.fn(), }, }; const caller = createManagerCaller(db); const result = await caller.createBroadcast({ title: "Ops update", body: "Email everyone", channel: "both", targetType: "all", }); await vi.waitFor(() => { expect(sendEmailMock).toHaveBeenCalledTimes(2); }); expect(transactionCommitted).toBe(true); expect(sendEmailMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ to: "user-a@example.com", subject: "Ops update", text: "Email everyone", })); expect(sendEmailMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ to: "user-b@example.com", subject: "Ops update", text: "Email everyone", })); expect(result).toMatchObject({ id: "broadcast_tx_email_1", recipientCount: 2, }); }); it("does not send emails immediately for scheduled broadcasts", async () => { resolveRecipientsMock.mockResolvedValue(["user_a", "user_b"]); const create = vi.fn().mockResolvedValue({ id: "broadcast_sched_email_1", title: "Scheduled ops update", createdAt: new Date("2026-03-30T10:00:00Z"), scheduledAt: FUTURE_SCHEDULED_AT, recipientCount: 2, }); const db = { user: { findUnique: vi.fn(), }, notificationBroadcast: { create, update: vi.fn(), }, notification: { create: vi.fn(), }, }; const caller = createManagerCaller(db); const result = await caller.createBroadcast({ title: "Scheduled ops update", body: "Hold until later", channel: "email", targetType: "all", scheduledAt: FUTURE_SCHEDULED_AT, }); await Promise.resolve(); expect(db.user.findUnique).not.toHaveBeenCalled(); expect(sendEmailMock).not.toHaveBeenCalled(); expect(result).toMatchObject({ id: "broadcast_sched_email_1", scheduledAt: FUTURE_SCHEDULED_AT, recipientCount: 2, }); }); it("maps scheduled broadcast sender persistence failures to not found errors", async () => { resolveRecipientsMock.mockResolvedValue(["user_a", "user_b"]); const create = vi.fn().mockRejectedValue( Object.assign(new Error("Foreign key constraint failed"), { code: "P2003", meta: { field_name: "NotificationBroadcast_senderId_fkey" }, }), ); const db = { notificationBroadcast: { create, update: vi.fn(), }, notification: { create: vi.fn(), }, }; const caller = createManagerCaller(db); await expect(caller.createBroadcast({ title: "Scheduled ops update", targetType: "all", scheduledAt: FUTURE_SCHEDULED_AT, })).rejects.toMatchObject({ code: "NOT_FOUND", message: "Sender user not found", }); expect(create).toHaveBeenCalledTimes(1); expect(db.notificationBroadcast.update).not.toHaveBeenCalled(); expect(db.notification.create).not.toHaveBeenCalled(); expect(emitNotificationCreated).not.toHaveBeenCalled(); expect(emitTaskAssigned).not.toHaveBeenCalled(); }); it("rolls back an immediate broadcast when the final broadcast update fails", async () => { resolveRecipientsMock.mockResolvedValue(["user_a", "user_b"]); const txCreateBroadcast = vi.fn().mockResolvedValue({ id: "broadcast_tx_2", title: "Ops update", createdAt: new Date("2026-03-30T10:00:00Z"), }); const txCreateNotification = vi.fn() .mockResolvedValueOnce({ id: "notif_a", userId: "user_a" }) .mockResolvedValueOnce({ id: "notif_b", userId: "user_b" }); const txUpdateBroadcast = vi.fn().mockRejectedValue( Object.assign(new Error("Record to update not found"), { code: "P2025", meta: { modelName: "NotificationBroadcast" }, }), ); const tx = { notificationBroadcast: { create: txCreateBroadcast, update: txUpdateBroadcast, }, notification: { create: txCreateNotification, }, }; const outerCreateBroadcast = vi.fn(); const outerUpdateBroadcast = vi.fn(); const outerCreateNotification = vi.fn(); const transaction = vi.fn(async (callback: (db: typeof tx) => Promise) => callback(tx)); const db = { $transaction: transaction, notificationBroadcast: { create: outerCreateBroadcast, update: outerUpdateBroadcast, }, notification: { create: outerCreateNotification, }, }; const caller = createManagerCaller(db); await expect(caller.createBroadcast({ title: "Ops update", targetType: "all", })).rejects.toMatchObject({ code: "NOT_FOUND", message: "Notification broadcast not found", }); expect(transaction).toHaveBeenCalledTimes(1); expect(txCreateBroadcast).toHaveBeenCalledTimes(1); expect(txCreateNotification).toHaveBeenCalledTimes(2); expect(txUpdateBroadcast).toHaveBeenCalledTimes(1); expect(outerCreateBroadcast).not.toHaveBeenCalled(); expect(outerUpdateBroadcast).not.toHaveBeenCalled(); expect(outerCreateNotification).not.toHaveBeenCalled(); expect(emitNotificationCreated).not.toHaveBeenCalled(); expect(emitTaskAssigned).not.toHaveBeenCalled(); }); it("does not send broadcast emails when the immediate transaction fails", async () => { resolveRecipientsMock.mockResolvedValue(["user_a", "user_b"]); const txCreateBroadcast = vi.fn().mockResolvedValue({ id: "broadcast_tx_email_fail_1", title: "Ops update", createdAt: new Date("2026-03-30T10:00:00Z"), }); const txCreateNotification = vi.fn() .mockResolvedValueOnce({ id: "notif_a", userId: "user_a" }) .mockResolvedValueOnce({ id: "notif_b", userId: "user_b" }); const txUpdateBroadcast = vi.fn().mockRejectedValue(new Error("commit failed")); const tx = { notificationBroadcast: { create: txCreateBroadcast, update: txUpdateBroadcast, }, notification: { create: txCreateNotification, }, }; const db = { $transaction: vi.fn(async (callback: (db: typeof tx) => Promise) => callback(tx)), user: { findUnique: vi.fn(), }, notificationBroadcast: { create: vi.fn(), update: vi.fn(), }, notification: { create: vi.fn(), }, }; const caller = createManagerCaller(db); await expect(caller.createBroadcast({ title: "Ops update", body: "Email everyone", channel: "email", targetType: "all", })).rejects.toThrow("commit failed"); await Promise.resolve(); expect(db.user.findUnique).not.toHaveBeenCalled(); expect(sendEmailMock).not.toHaveBeenCalled(); }); }); // ─── task management ──────────────────────────────────────────────────────── describe("notification.listTasks", () => { it("lists task and approval notifications for the current user as owner or assignee", async () => { const tasks = [ sampleNotification({ id: "task_1", category: "TASK", taskStatus: "OPEN" }), sampleNotification({ id: "approval_1", category: "APPROVAL", taskStatus: "IN_PROGRESS", assigneeId: "user_1", }), ]; const db = withUserLookup({ notification: { findMany: vi.fn().mockResolvedValue(tasks), }, }); const caller = createProtectedCaller(db); const result = await caller.listTasks({ status: "OPEN", limit: 15 }); expect(result).toEqual(tasks); expect(db.notification.findMany).toHaveBeenCalledWith({ where: { OR: [{ userId: "user_1" }, { assigneeId: "user_1" }], category: { in: ["TASK", "APPROVAL"] }, taskStatus: "OPEN", }, orderBy: [{ priority: "desc" }, { dueDate: "asc" }, { createdAt: "desc" }], take: 15, }); }); it("can restrict tasks to those owned by the current user", async () => { const db = withUserLookup({ notification: { findMany: vi.fn().mockResolvedValue([]), }, }); const caller = createProtectedCaller(db); await caller.listTasks({ includeAssigned: false, limit: 10 }); expect(db.notification.findMany).toHaveBeenCalledWith({ where: { userId: "user_1", category: { in: ["TASK", "APPROVAL"] }, }, orderBy: [{ priority: "desc" }, { dueDate: "asc" }, { createdAt: "desc" }], take: 10, }); }); }); describe("notification.taskCounts", () => { it("normalizes grouped task counts and calculates overdue work", async () => { const groupBy = vi.fn().mockResolvedValue([ { taskStatus: "OPEN", _count: 4 }, { taskStatus: "DONE", _count: 2 }, ]); const count = vi.fn().mockResolvedValue(3); const db = withUserLookup({ notification: { groupBy, count, }, }); const caller = createProtectedCaller(db); const result = await caller.taskCounts(); expect(result).toEqual({ open: 4, inProgress: 0, done: 2, dismissed: 0, overdue: 3, }); expect(groupBy).toHaveBeenCalledWith({ by: ["taskStatus"], where: { OR: [{ userId: "user_1" }, { assigneeId: "user_1" }], category: { in: ["TASK", "APPROVAL"] }, }, _count: true, }); expect(count).toHaveBeenCalledWith({ where: { OR: [{ userId: "user_1" }, { assigneeId: "user_1" }], category: { in: ["TASK", "APPROVAL"] }, taskStatus: { in: ["OPEN", "IN_PROGRESS"] }, dueDate: { lt: expect.any(Date) }, }, }); }); }); describe("notification.getTaskDetail", () => { it("returns the selected task detail payload including sender context", async () => { const task = { id: "task_1", title: "Review allocation", body: "Please verify the plan.", type: "TASK_CREATED", priority: "HIGH", category: "TASK", taskStatus: "OPEN", taskAction: "approve_vacation:vac_1", dueDate: new Date("2026-04-10T09:00:00Z"), entityId: "vac_1", entityType: "VACATION", completedAt: null, completedBy: null, createdAt: new Date("2026-04-01T08:00:00Z"), userId: "user_1", assigneeId: "user_2", sender: { id: "user_mgr", name: "Manager", email: "mgr@example.com" }, }; const findFirst = vi.fn().mockResolvedValue(task); const db = withUserLookup({ notification: { findFirst, }, }); const caller = createProtectedCaller(db); const result = await caller.getTaskDetail({ id: "task_1" }); expect(result).toEqual(task); expect(findFirst).toHaveBeenCalledWith({ where: { id: "task_1", OR: [{ userId: "user_1" }, { assigneeId: "user_1" }], category: { in: ["TASK", "APPROVAL"] }, }, select: { id: true, title: true, body: true, type: true, priority: true, category: true, taskStatus: true, taskAction: true, dueDate: true, entityId: true, entityType: true, completedAt: true, completedBy: true, createdAt: true, userId: true, assigneeId: true, sender: { select: { id: true, name: true, email: true } }, }, }); }); it("returns NOT_FOUND when the task is not visible to the current user", async () => { const db = withUserLookup({ notification: { findFirst: vi.fn().mockResolvedValue(null), }, }); const caller = createProtectedCaller(db); await expect(caller.getTaskDetail({ id: "task_missing" })).rejects.toMatchObject({ code: "NOT_FOUND", message: "Task not found or you do not have permission", }); }); }); describe("notification.updateTaskStatus", () => { it("marks a task as done, records completion metadata, and emits completion events", async () => { const findFirst = vi.fn().mockResolvedValue({ id: "task_1", userId: "user_1", assigneeId: "user_2", }); const update = vi.fn().mockResolvedValue({ id: "task_1", taskStatus: "DONE", completedBy: "user_1", completedAt: new Date("2026-04-02T10:00:00Z"), }); const db = withUserLookup({ notification: { findFirst, update, }, }); const caller = createProtectedCaller(db); const result = await caller.updateTaskStatus({ id: "task_1", status: "DONE" }); expect(result).toMatchObject({ id: "task_1", taskStatus: "DONE", completedBy: "user_1", }); expect(findFirst).toHaveBeenCalledWith({ where: { id: "task_1", OR: [{ userId: "user_1" }, { assigneeId: "user_1" }], category: { in: ["TASK", "APPROVAL"] }, }, }); expect(update).toHaveBeenCalledWith({ where: { id: "task_1" }, data: { taskStatus: "DONE", completedAt: expect.any(Date), completedBy: "user_1", }, }); expect(emitTaskCompleted).toHaveBeenNthCalledWith(1, "user_1", "task_1"); expect(emitTaskCompleted).toHaveBeenNthCalledWith(2, "user_2", "task_1"); expect(emitTaskStatusChanged).not.toHaveBeenCalled(); }); it("emits status-change events for non-terminal task updates", async () => { const db = withUserLookup({ notification: { findFirst: vi.fn().mockResolvedValue({ id: "task_2", userId: "user_1", assigneeId: "user_3", }), update: vi.fn().mockResolvedValue({ id: "task_2", taskStatus: "IN_PROGRESS", }), }, }); const caller = createProtectedCaller(db); await caller.updateTaskStatus({ id: "task_2", status: "IN_PROGRESS" }); expect(db.notification.update).toHaveBeenCalledWith({ where: { id: "task_2" }, data: { taskStatus: "IN_PROGRESS", completedAt: null, completedBy: null, }, }); expect(emitTaskStatusChanged).toHaveBeenNthCalledWith(1, "user_1", "task_2"); expect(emitTaskStatusChanged).toHaveBeenNthCalledWith(2, "user_3", "task_2"); }); it("clears completion metadata when reopening a completed task", async () => { const db = withUserLookup({ notification: { findFirst: vi.fn().mockResolvedValue({ id: "task_3", userId: "user_1", assigneeId: "user_3", taskStatus: "DONE", completedAt: new Date("2026-04-01T10:00:00Z"), completedBy: "user_2", }), update: vi.fn().mockResolvedValue({ id: "task_3", taskStatus: "OPEN", completedAt: null, completedBy: null, }), }, }); const caller = createProtectedCaller(db); const result = await caller.updateTaskStatus({ id: "task_3", status: "OPEN" }); expect(result).toMatchObject({ id: "task_3", taskStatus: "OPEN", completedAt: null, completedBy: null, }); expect(db.notification.update).toHaveBeenCalledWith({ where: { id: "task_3" }, data: { taskStatus: "OPEN", completedAt: null, completedBy: null, }, }); expect(emitTaskStatusChanged).toHaveBeenNthCalledWith(1, "user_1", "task_3"); expect(emitTaskStatusChanged).toHaveBeenNthCalledWith(2, "user_3", "task_3"); expect(emitTaskCompleted).not.toHaveBeenCalled(); }); it("rejects task status updates for non-task notifications", async () => { const findFirst = vi.fn().mockResolvedValue(null); const update = vi.fn(); const db = withUserLookup({ notification: { findFirst, update, }, }); const caller = createProtectedCaller(db); await expect(caller.updateTaskStatus({ id: "notif_info_1", status: "DONE" })).rejects.toMatchObject( { code: "NOT_FOUND", message: "Task not found or you do not have permission", }, ); expect(findFirst).toHaveBeenCalledWith({ where: { id: "notif_info_1", OR: [{ userId: "user_1" }, { assigneeId: "user_1" }], category: { in: ["TASK", "APPROVAL"] }, }, }); expect(update).not.toHaveBeenCalled(); expect(emitTaskCompleted).not.toHaveBeenCalled(); expect(emitTaskStatusChanged).not.toHaveBeenCalled(); }); }); describe("notification.assignTask", () => { it("returns NOT_FOUND when assigning a missing task", async () => { const update = vi.fn(); const db = { notification: { findUnique: vi.fn().mockResolvedValue(null), update, }, }; const caller = createManagerCaller(db); await expect(caller.assignTask({ id: "task_missing", assigneeId: "user_4" })).rejects.toMatchObject({ code: "NOT_FOUND", message: "Task not found", }); expect(update).not.toHaveBeenCalled(); expect(emitTaskAssigned).not.toHaveBeenCalled(); }); it("maps missing task recipients to a not found error without side effects", async () => { const db = { notification: { create: vi.fn().mockRejectedValue({ code: "P2003", message: "Foreign key constraint failed", meta: { field_name: "Notification_userId_fkey" }, }), findUnique: vi.fn(), }, }; const caller = createManagerCaller(db); await expect(caller.createTask({ userId: "user_missing", title: "Review proposal", channel: "in_app", })).rejects.toMatchObject({ code: "NOT_FOUND", message: "Task recipient user not found", }); expect(db.notification.findUnique).not.toHaveBeenCalled(); expect(emitNotificationCreated).not.toHaveBeenCalled(); expect(emitTaskAssigned).not.toHaveBeenCalled(); }); it("rejects assigning non-task notifications", async () => { const update = vi.fn(); const db = { notification: { findUnique: vi.fn().mockResolvedValue({ id: "notif_9", category: "REMINDER", assigneeId: null, }), update, }, }; const caller = createManagerCaller(db); await expect(caller.assignTask({ id: "notif_9", assigneeId: "user_4" })).rejects.toMatchObject({ code: "BAD_REQUEST", message: "Only tasks and approvals can be assigned", }); expect(update).not.toHaveBeenCalled(); expect(emitTaskAssigned).not.toHaveBeenCalled(); }); it("reassigns a task and emits the assignment event for the new assignee", async () => { const findUnique = vi.fn().mockResolvedValue({ id: "task_9", category: "TASK", assigneeId: "user_2", }); const update = vi.fn().mockResolvedValue({ id: "task_9", category: "TASK", assigneeId: "user_4", }); const db = { notification: { findUnique, update, }, }; const caller = createManagerCaller(db); const result = await caller.assignTask({ id: "task_9", assigneeId: "user_4" }); expect(findUnique).toHaveBeenCalledWith({ where: { id: "task_9" } }); expect(update).toHaveBeenCalledWith({ where: { id: "task_9" }, data: { assigneeId: "user_4" }, }); expect(result).toMatchObject({ id: "task_9", assigneeId: "user_4", }); expect(emitTaskAssigned).toHaveBeenCalledWith("user_4", "task_9"); }); it("returns NOT_FOUND when assigning a task to a missing assignee", async () => { const findUnique = vi.fn().mockResolvedValue({ id: "task_9", category: "TASK", assigneeId: "user_2", }); const update = vi.fn().mockRejectedValue({ code: "P2003", message: "Foreign key constraint failed", meta: { field_name: "Notification_assigneeId_fkey" }, }); const db = { notification: { findUnique, update, }, }; const caller = createManagerCaller(db); await expect(caller.assignTask({ id: "task_9", assigneeId: "user_missing" })).rejects.toMatchObject({ code: "NOT_FOUND", message: "Assignee user not found", }); expect(findUnique).toHaveBeenCalledWith({ where: { id: "task_9" } }); expect(update).toHaveBeenCalledWith({ where: { id: "task_9" }, data: { assigneeId: "user_missing" }, }); expect(emitTaskAssigned).not.toHaveBeenCalledWith("user_missing", "task_9"); }); it("returns NOT_FOUND when the task disappears before reassignment is persisted", async () => { const findUnique = vi.fn().mockResolvedValue({ id: "task_9", category: "TASK", assigneeId: "user_2", }); const update = vi.fn().mockRejectedValue( Object.assign(new Error("Record to update not found"), { code: "P2025", }), ); const db = { notification: { findUnique, update, }, }; const caller = createManagerCaller(db); await expect(caller.assignTask({ id: "task_9", assigneeId: "user_4" })).rejects.toMatchObject({ code: "NOT_FOUND", message: "Task not found", }); expect(findUnique).toHaveBeenCalledWith({ where: { id: "task_9" } }); expect(update).toHaveBeenCalledWith({ where: { id: "task_9" }, data: { assigneeId: "user_4" }, }); expect(emitTaskAssigned).not.toHaveBeenCalled(); }); }); describe("notification.executeTaskAction", () => { it("rejects dismissed tasks before executing their domain action", async () => { const updateAssignment = vi.fn(); const db = withUserLookup({ notification: { findFirst: vi.fn().mockResolvedValue({ id: "task_1", userId: "user_1", assigneeId: null, taskAction: "confirm_assignment:assign_1", taskStatus: "DISMISSED", }), update: vi.fn(), }, assignment: { findUnique: vi.fn(), update: updateAssignment, }, }); const caller = createAdminCaller(db); await expect(caller.executeTaskAction({ id: "task_1" })).rejects.toMatchObject({ code: "PRECONDITION_FAILED", message: "This task has been dismissed", }); expect(updateAssignment).not.toHaveBeenCalled(); expect(db.notification.update).not.toHaveBeenCalled(); expect(emitTaskCompleted).not.toHaveBeenCalled(); }); it("rejects task action execution when transactional persistence support is unavailable", async () => { const updateVacation = vi.fn(); const db = withUserLookup({ notification: { findFirst: vi.fn().mockResolvedValue({ id: "task_1", userId: "user_1", assigneeId: null, taskAction: "approve_vacation:vac_1", taskStatus: "OPEN", }), update: vi.fn(), }, vacation: { findUnique: vi.fn(), update: updateVacation, }, }); const caller = createAdminCaller(db); await expect(caller.executeTaskAction({ id: "task_1" })).rejects.toMatchObject({ code: "INTERNAL_SERVER_ERROR", message: "Task action execution requires transactional persistence support.", }); expect(updateVacation).not.toHaveBeenCalled(); expect(db.notification.update).not.toHaveBeenCalled(); expect(emitTaskCompleted).not.toHaveBeenCalled(); }); }); // ─── reminders ────────────────────────────────────────────────────────────── describe("notification.createReminder", () => { it("creates a reminder for the current user and seeds nextRemindAt", async () => { const remindAt = new Date("2026-04-15T08:30:00Z"); const createdReminder = sampleNotification({ id: "rem_1", type: "REMINDER", category: "REMINDER", title: "Submit report", remindAt, nextRemindAt: remindAt, recurrence: "weekly", }); const create = vi.fn().mockResolvedValue(createdReminder); const db = withUserLookup({ notification: { create, }, }); const caller = createProtectedCaller(db); const result = await caller.createReminder({ title: "Submit report", body: "Finance review", remindAt, recurrence: "weekly", entityId: "project_1", entityType: "PROJECT", link: "/projects/project_1", }); expect(result).toEqual(createdReminder); expect(create).toHaveBeenCalledWith({ data: { userId: "user_1", type: "REMINDER", category: "REMINDER", title: "Submit report", body: "Finance review", remindAt, nextRemindAt: remindAt, recurrence: "weekly", entityId: "project_1", entityType: "PROJECT", link: "/projects/project_1", channel: "in_app", }, }); }); }); describe("notification.updateReminder", () => { it("updates an owned reminder and keeps nextRemindAt in sync with remindAt", async () => { const remindAt = new Date("2026-05-01T07:00:00Z"); const findFirst = vi.fn().mockResolvedValue({ id: "rem_1", userId: "user_1", category: "REMINDER", }); const update = vi.fn().mockResolvedValue({ id: "rem_1", title: "Updated reminder", remindAt, nextRemindAt: remindAt, recurrence: null, }); const db = withUserLookup({ notification: { findFirst, update, }, }); const caller = createProtectedCaller(db); const result = await caller.updateReminder({ id: "rem_1", title: "Updated reminder", remindAt, recurrence: null, }); expect(result).toMatchObject({ id: "rem_1", title: "Updated reminder", recurrence: null, }); expect(findFirst).toHaveBeenCalledWith({ where: { id: "rem_1", userId: "user_1", category: "REMINDER" }, }); expect(update).toHaveBeenCalledWith({ where: { id: "rem_1" }, data: { title: "Updated reminder", remindAt, nextRemindAt: remindAt, recurrence: null, }, }); }); it("returns NOT_FOUND when the reminder is missing or belongs to another user", async () => { const update = vi.fn(); const db = withUserLookup({ notification: { findFirst: vi.fn().mockResolvedValue(null), update, }, }); const caller = createProtectedCaller(db); await expect(caller.updateReminder({ id: "rem_missing", title: "Updated reminder", })).rejects.toMatchObject({ code: "NOT_FOUND", message: "Reminder not found or you do not have permission", }); expect(update).not.toHaveBeenCalled(); }); }); describe("notification.deleteReminder", () => { it("deletes an owned reminder", async () => { const findFirst = vi.fn().mockResolvedValue({ id: "rem_1", userId: "user_1", category: "REMINDER", }); const deleteFn = vi.fn().mockResolvedValue({ id: "rem_1" }); const db = withUserLookup({ notification: { findFirst, delete: deleteFn, }, }); const caller = createProtectedCaller(db); await caller.deleteReminder({ id: "rem_1" }); expect(findFirst).toHaveBeenCalledWith({ where: { id: "rem_1", userId: "user_1", category: "REMINDER" }, }); expect(deleteFn).toHaveBeenCalledWith({ where: { id: "rem_1" } }); }); it("returns NOT_FOUND when the reminder is missing or belongs to another user", async () => { const deleteFn = vi.fn(); const db = withUserLookup({ notification: { findFirst: vi.fn().mockResolvedValue(null), delete: deleteFn, }, }); const caller = createProtectedCaller(db); await expect(caller.deleteReminder({ id: "rem_missing" })).rejects.toMatchObject({ code: "NOT_FOUND", message: "Reminder not found or you do not have permission", }); expect(deleteFn).not.toHaveBeenCalled(); }); }); describe("notification.listReminders", () => { it("lists reminders for the current user ordered by next reminder date", async () => { const reminders = [ { id: "rem_1", userId: "user_1", category: "REMINDER", nextRemindAt: new Date("2026-05-01T07:00:00Z"), }, ]; const db = withUserLookup({ notification: { findMany: vi.fn().mockResolvedValue(reminders), }, }); const caller = createProtectedCaller(db); const result = await caller.listReminders({ limit: 10 }); expect(result).toEqual(reminders); expect(db.notification.findMany).toHaveBeenCalledWith({ where: { userId: "user_1", category: "REMINDER" }, orderBy: { nextRemindAt: "asc" }, take: 10, }); }); }); // ─── broadcasts and delete ───────────────────────────────────────────────── describe("notification.getBroadcastById", () => { it("returns the broadcast including sender context", async () => { const findUnique = vi.fn().mockResolvedValue({ id: "broadcast_1", title: "Office update", sender: { id: "user_mgr", name: "Manager", email: "mgr@example.com" }, }); const db = withUserLookup({ notificationBroadcast: { findUnique, }, }, "user_mgr"); const caller = createManagerCaller(db); const result = await caller.getBroadcastById({ id: "broadcast_1" }); expect(result).toEqual({ id: "broadcast_1", title: "Office update", sender: { id: "user_mgr", name: "Manager", email: "mgr@example.com" }, }); expect(findUnique).toHaveBeenCalledWith({ where: { id: "broadcast_1" }, include: { sender: { select: { id: true, name: true, email: true } }, }, }); }); }); describe("notification.delete", () => { it("deletes a regular notification owned by the current user", async () => { const findFirst = vi.fn().mockResolvedValue({ id: "notif_1", userId: "user_1", category: "NOTIFICATION", senderId: "user_mgr", }); const deleteFn = vi.fn().mockResolvedValue({ id: "notif_1" }); const db = withUserLookup({ notification: { findFirst, delete: deleteFn, }, }); const caller = createProtectedCaller(db); await caller.delete({ id: "notif_1" }); expect(findFirst).toHaveBeenCalledWith({ where: { id: "notif_1", userId: "user_1" }, }); expect(deleteFn).toHaveBeenCalledWith({ where: { id: "notif_1" } }); }); it("forbids deleting a task created by another user", async () => { const deleteFn = vi.fn(); const db = withUserLookup({ notification: { findFirst: vi.fn().mockResolvedValue({ id: "task_1", userId: "user_1", category: "TASK", senderId: "user_mgr", }), delete: deleteFn, }, }); const caller = createProtectedCaller(db); await expect(caller.delete({ id: "task_1" })).rejects.toMatchObject({ code: "FORBIDDEN", message: "Cannot delete tasks created by others", }); expect(deleteFn).not.toHaveBeenCalled(); }); });