import { SystemRole } from "@capakraken/shared"; import { describe, expect, it, vi } from "vitest"; import { notificationRouter } from "../router/notification.js"; import { createCallerFactory } from "../trpc.js"; // Mock the SSE event bus — we don't test real event emission here vi.mock("../sse/event-bus.js", () => ({ emitNotificationCreated: vi.fn(), })); const createCaller = createCallerFactory(notificationRouter); // ── 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, }, }); } // ── 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("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(); }); });