import { beforeEach, describe, expect, it, vi } from "vitest"; import { PgDialect } from "drizzle-orm/pg-core"; const mocks = vi.hoisted(() => ({ rows: [] as unknown[][], filters: [] as unknown[], create: vi.fn() })); vi.mock("server-only", () => ({})); vi.mock("@/lib/notifications/repository", () => ({ createNotifications: mocks.create })); vi.mock("@/db", () => ({ getDb: () => ({ select: () => { const query = { from() { return query; }, where(filter: unknown) { mocks.filters.push(filter); return query; }, orderBy() { return query; }, limit() { return Promise.resolve(mocks.rows.shift() ?? []); }, }; return query; } }) })); import { sendGuidePublicationNotifications } from "./notifications"; const event = { aggregateId: "72df08ab-50dd-4cbd-9a69-70949d34cf9f", eventType: "guide.published", topic: "notifications:guide", payload: { publishedAt: "2026-10-08T10:00:00.000Z" }, }; beforeEach(() => { mocks.rows.length = 0; mocks.filters.length = 0; mocks.create.mockReset(); }); describe("guide publication notifications", () => { it("notifies all existing active users in batches with a retry-safe event key and guide link", async () => { mocks.rows.push([{ name: "Amber", slug: "amber" }], Array.from({ length: 100 }, (_, index) => ({ id: `user-${index.toString().padStart(3, "0")}` })), [{ id: "user-100" }], []); await sendGuidePublicationNotifications(event); expect(mocks.create).toHaveBeenCalledTimes(2); expect(mocks.create.mock.calls[0][0]).toHaveLength(100); expect(mocks.create.mock.calls[1]).toEqual([ [{ userId: "user-100", url: "/amber" }], { eventKey: `guide:${event.aggregateId}:published`, kind: "guide_published", title: "เผยแพร่ Guide ใหม่", body: "อ่าน Guide Amber ได้แล้ว" }, ]); const query = new PgDialect().sqlToQuery(mocks.filters[2] as Parameters[0]); expect(query.sql).toContain('"banned" is not true'); expect(query.sql).toContain('"created_at" <='); expect(query.sql).toContain('"id" >'); expect(query.params).toContain("user-099"); }); it("skips a guide that is private, trashed, or deleted before delivery", async () => { mocks.rows.push([]); await sendGuidePublicationNotifications(event); expect(mocks.create).not.toHaveBeenCalled(); }); it("propagates delivery failures so the outbox retries", async () => { mocks.rows.push([{ name: "Amber", slug: "amber" }], [{ id: "user" }]); mocks.create.mockRejectedValueOnce(new Error("database unavailable")); await expect(sendGuidePublicationNotifications(event)).rejects.toThrow("database unavailable"); }); it("rejects malformed events before reading users", async () => { await expect(sendGuidePublicationNotifications({ ...event, payload: {} })).rejects.toThrow("Invalid guide publication"); await expect(sendGuidePublicationNotifications({ ...event, topic: "admin" })).rejects.toThrow("Invalid guide publication"); expect(mocks.filters).toHaveLength(0); }); });