65 lines
3.0 KiB
TypeScript
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);
|
|
});
|
|
});
|