Files
buzz-sheet/lib/guides/notifications.test.ts
T
gunshiz 72e9f525cc
CI / Verify (push) Successful in 1m42s
CI / Build immutable images and deploy (push) Successful in 3m3s
feat(notifications) : notify guide publications and admin hearts; simplify Stygian navigation
2026-10-08 04:04:02 +07:00

65 lines
3.0 KiB
TypeScript

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<PgDialect["sqlToQuery"]>[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);
});
});