107 lines
6.0 KiB
TypeScript
107 lines
6.0 KiB
TypeScript
import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const state = vi.hoisted(() => ({
|
|
select: vi.fn(), rows: [] as unknown[][], notifications: vi.fn(), send: vi.fn(),
|
|
authorize: vi.fn(), publish: vi.fn(), after: vi.fn(),
|
|
}));
|
|
vi.mock("server-only", () => ({}));
|
|
vi.mock("@/db", () => ({ getDb: () => ({ select: state.select }) }));
|
|
vi.mock("web-push", () => ({ default: { sendNotification: state.send, setVapidDetails: vi.fn() } }));
|
|
vi.mock("@/lib/commission/push", () => ({ pushPublicKey: () => "public-key", validPushEndpoint: () => true }));
|
|
vi.mock("@/lib/notifications/repository", () => ({ createNotifications: state.notifications }));
|
|
vi.mock("./repository", () => ({
|
|
authorizeComment: state.authorize, requireCommentViewer: async () => ({ id: "author", admin: false }), commentId: (id: string) => id,
|
|
}));
|
|
vi.mock("./publish", () => ({ publishComment: state.publish }));
|
|
vi.mock("next/server", () => ({ after: state.after }));
|
|
vi.mock("@/lib/security/rate-limit", () => ({ limitRequest: async () => {} }));
|
|
|
|
import { sendCommentPush } from "./push";
|
|
import { PATCH } from "@/app/api/comments/[id]/route";
|
|
|
|
const id = "00000000-0000-4000-8000-000000000001";
|
|
const subscription = { endpoint: "https://fcm.googleapis.com/fcm/send/test", p256dh: "key", auth: "auth" };
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.stubEnv("BETTER_AUTH_URL", "https://guide.example.test");
|
|
state.rows = [];
|
|
state.select.mockImplementation(() => {
|
|
const rows = state.rows.shift() ?? [];
|
|
const query = { from: () => query, where: () => query, limit: () => Promise.resolve(rows), then: (resolve: (rows: unknown[]) => unknown) => Promise.resolve(rows).then(resolve) };
|
|
return query;
|
|
});
|
|
state.authorize.mockResolvedValue({ destination: { target: "guide:test", href: "/test/comment", name: "Test Guide", writable: true } });
|
|
state.publish.mockResolvedValue({ id, version: 2 });
|
|
state.notifications.mockResolvedValue(undefined);
|
|
state.send.mockResolvedValue(undefined);
|
|
});
|
|
afterEach(() => vi.unstubAllEnvs());
|
|
|
|
describe("comment notifications", () => {
|
|
it.each([2, 3])("ignores an explicit edit revision %s before querying or sending", async (version) => {
|
|
await sendCommentPush(id, version);
|
|
expect(state.select).not.toHaveBeenCalled();
|
|
expect(state.notifications).not.toHaveBeenCalled();
|
|
expect(state.send).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([null, "parent-id"])("ignores an edited comment or reply when no revision is supplied (parent %s)", async (replyToId) => {
|
|
state.rows = [[{ id, version: 2, hidden: false, deletedAt: null, replyToId }]];
|
|
await sendCommentPush(id);
|
|
expect(state.select).toHaveBeenCalledTimes(1);
|
|
expect(state.authorize).not.toHaveBeenCalled();
|
|
expect(state.notifications).not.toHaveBeenCalled();
|
|
expect(state.send).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([false, true])("still sends new-comment notifications with the author's profile picture (reply: %s)", async (reply) => {
|
|
const recipient = { id: reply ? "reader" : "admin", admin: !reply };
|
|
state.rows = [
|
|
[{ id, version: 1, hidden: false, deletedAt: null, authorId: "author", replyToId: reply ? "parent-id" : null, rootId: reply ? "root-id" : null }],
|
|
...(reply ? [[{ authorId: recipient.id, hidden: false, deletedAt: null }], [{ id: recipient.id, role: "user", banned: false }]] : [[{ id: recipient.id }]]),
|
|
[{ name: "Author", image: "https://profile.example/author.png" }], [{ text: "Original comment" }], [subscription],
|
|
];
|
|
await sendCommentPush(id, 1);
|
|
expect(state.notifications).toHaveBeenCalledWith([expect.objectContaining({ userId: recipient.id, adminOnly: !reply })], expect.objectContaining({
|
|
kind: reply ? "comment_reply" : "comment_new", eventKey: `comment:${id}`, body: "Original comment",
|
|
}));
|
|
expect(state.send).toHaveBeenCalledTimes(1);
|
|
expect(JSON.parse(state.send.mock.calls[0][1])).toMatchObject({ body: "Original comment", icon: "https://profile.example/author.png" });
|
|
expect(state.notifications.mock.calls[0][1].title).toBe(reply ? "Author ตอบกลับความคิดเห็นของคุณ" : "Author แสดงความคิดเห็นใหม่ในไกด์ Test Guide");
|
|
});
|
|
|
|
it("still delivers a delayed initial notification when the comment has since been edited", async () => {
|
|
state.rows = [
|
|
[{ id, version: 2, hidden: false, deletedAt: null, authorId: "author", replyToId: null }],
|
|
[{ id: "admin" }], [{ name: "Author" }], [{ text: "Original revision" }], [subscription],
|
|
];
|
|
await sendCommentPush(id, 1);
|
|
expect(state.notifications).toHaveBeenCalledWith(expect.any(Array), expect.objectContaining({ kind: "comment_new", body: "Original revision" }));
|
|
expect(state.send).toHaveBeenCalledTimes(1);
|
|
expect(JSON.parse(state.send.mock.calls[0][1]).icon).toBe("/icon/nav/Comment.webp");
|
|
});
|
|
|
|
it("keeps Stygian notification titles separate from guide titles", async () => {
|
|
state.authorize.mockResolvedValue({ destination: { target: "stygian:123", href: "/stygian/comment", name: "Test round", writable: true } });
|
|
state.rows = [
|
|
[{ id, version: 1, hidden: false, deletedAt: null, authorId: "author", replyToId: null }],
|
|
[{ id: "admin" }], [{ name: "Author" }], [{ text: "Original comment" }], [subscription],
|
|
];
|
|
await sendCommentPush(id, 1);
|
|
expect(state.notifications.mock.calls[0][1].title).toBe("Author แสดงความคิดเห็นใหม่ใน Test round");
|
|
});
|
|
|
|
it("saves edits through PATCH without scheduling a notification", async () => {
|
|
const response = await PATCH(new Request(`https://guide.example.test/api/comments/${id}`, {
|
|
method: "PATCH", headers: { Origin: "https://guide.example.test" },
|
|
}), { params: Promise.resolve({ id }) });
|
|
expect(response.status).toBe(200);
|
|
expect(await response.json()).toEqual({ id, version: 2 });
|
|
expect(state.publish).toHaveBeenCalled();
|
|
expect(state.after).not.toHaveBeenCalled();
|
|
expect(state.notifications).not.toHaveBeenCalled();
|
|
expect(state.send).not.toHaveBeenCalled();
|
|
});
|
|
});
|