Bundles the 2026-07-12 code-audit session (Clusters A/B/C/D partial/E/F): denormalized Post/Advertisement reaction/comment/repost counters synced transactionally instead of live _count queries; real cursor-based pagination for followers/following/blocks lists; assertPetOwnership + formatRelativeTime centralized; dead r2.ts + AWS SDK deps removed; missing DB indexes added; account-deletion flow, mention notifications, pull-to-refresh feed, and mobile UI/i18n fixes from the surrounding sessions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
143 lines
5.0 KiB
TypeScript
143 lines
5.0 KiB
TypeScript
/**
|
|
* Comments Unit Tests — Phase 3 Plan 03-03
|
|
*
|
|
* Covers: ENGM-02 (comment on post), ENGM-03 (delete own comment)
|
|
*
|
|
* Tests:
|
|
* - create returns comment with body
|
|
* - delete own comment succeeds
|
|
* - delete other's comment throws FORBIDDEN
|
|
*/
|
|
|
|
import { describe, it, expect, vi } from "vitest";
|
|
import { TRPCError } from "@trpc/server";
|
|
|
|
vi.mock("server-only", () => ({}));
|
|
vi.mock("@clerk/nextjs/server", () => ({
|
|
auth: vi.fn().mockResolvedValue({ userId: "owner-1" }),
|
|
}));
|
|
vi.mock("@/lib/redis", () => ({ redis: {} }));
|
|
vi.mock("@/lib/feed-helpers", () => ({
|
|
fanOutPost: vi.fn().mockResolvedValue(undefined),
|
|
backfillOnFollow: vi.fn().mockResolvedValue(undefined),
|
|
feedKey: vi.fn((id: string) => `feed:${id}`),
|
|
getFeedPage: vi.fn(),
|
|
trimFeed: vi.fn(),
|
|
PAGE_SIZE: 20,
|
|
FEED_RETENTION_MS: 90 * 24 * 60 * 60 * 1000,
|
|
}));
|
|
vi.mock("@/lib/prisma", () => ({ prisma: {} }));
|
|
|
|
import { createMockPrisma } from "@/__tests__/helpers/prisma-mock";
|
|
|
|
async function createCommentsCaller(overrides?: {
|
|
userId?: string | null;
|
|
prisma?: ReturnType<typeof createMockPrisma>;
|
|
}) {
|
|
const { commentsRouter } = await import("@/trpc/routers/comments");
|
|
const { initTRPC } = await import("@trpc/server");
|
|
|
|
const mockPrisma = overrides?.prisma ?? createMockPrisma();
|
|
const userId = overrides?.userId !== undefined ? overrides.userId : "owner-1";
|
|
// $transaction receives an array of already-invoked mock calls (promises or
|
|
// plain values) — Promise.all mirrors that shape closely enough for tests.
|
|
mockPrisma.$transaction.mockImplementation((ops: unknown[]) => Promise.all(ops));
|
|
|
|
const t = initTRPC
|
|
.context<{ userId: string | null; prisma: typeof mockPrisma }>()
|
|
.create();
|
|
|
|
const testRouter = t.router({ comments: commentsRouter });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const caller = testRouter.createCaller({ userId, prisma: mockPrisma as any });
|
|
return { caller, mockPrisma };
|
|
}
|
|
|
|
// ─── comments.create ─────────────────────────────────────────────────────────
|
|
|
|
describe("comments.create", () => {
|
|
it("ENGM-02 — create returns comment with correct body", async () => {
|
|
const { caller, mockPrisma } = await createCommentsCaller();
|
|
|
|
const mockPet = { id: "pet-1", ownerId: "owner-1", name: "Buddy" };
|
|
mockPrisma.pet.findUnique.mockResolvedValue(mockPet);
|
|
mockPrisma.comment.create.mockResolvedValue({
|
|
id: "cmt-1",
|
|
postId: "post-1",
|
|
petId: "pet-1",
|
|
body: "Cute pup!",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
pet: { id: "pet-1", name: "Buddy", avatarKey: null },
|
|
});
|
|
mockPrisma.post.update.mockResolvedValue({ petId: "pet-1" });
|
|
|
|
const result = await caller.comments.create({
|
|
postId: "post-1",
|
|
petId: "pet-1",
|
|
body: "Cute pup!",
|
|
});
|
|
|
|
expect(mockPrisma.comment.create).toHaveBeenCalled();
|
|
expect(mockPrisma.post.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: { id: "post-1" },
|
|
data: { commentCount: { increment: 1 } },
|
|
})
|
|
);
|
|
expect(result).toMatchObject({ body: "Cute pup!", petId: "pet-1" });
|
|
});
|
|
});
|
|
|
|
// ─── comments.delete ─────────────────────────────────────────────────────────
|
|
|
|
describe("comments.delete", () => {
|
|
it("ENGM-03 — delete own comment succeeds", async () => {
|
|
const { caller, mockPrisma } = await createCommentsCaller();
|
|
|
|
mockPrisma.pet.findUnique.mockResolvedValue({ id: "pet-1", ownerId: "owner-1", name: "Buddy" });
|
|
mockPrisma.comment.findUnique.mockResolvedValue({
|
|
id: "cmt-1",
|
|
postId: "post-1",
|
|
petId: "pet-1",
|
|
body: "Cute pup!",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
mockPrisma.comment.delete.mockResolvedValue({ id: "cmt-1" });
|
|
mockPrisma.post.update.mockResolvedValue({ id: "post-1" });
|
|
|
|
await expect(
|
|
caller.comments.delete({ commentId: "cmt-1", petId: "pet-1" })
|
|
).resolves.not.toThrow();
|
|
|
|
expect(mockPrisma.comment.delete).toHaveBeenCalledWith(
|
|
expect.objectContaining({ where: { id: "cmt-1" } })
|
|
);
|
|
expect(mockPrisma.post.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: { id: "post-1" },
|
|
data: { commentCount: { decrement: 1 } },
|
|
})
|
|
);
|
|
});
|
|
|
|
it("ENGM-03 — delete other's comment throws FORBIDDEN", async () => {
|
|
const { caller, mockPrisma } = await createCommentsCaller();
|
|
|
|
mockPrisma.pet.findUnique.mockResolvedValue({ id: "pet-1", ownerId: "owner-1", name: "Buddy" });
|
|
mockPrisma.comment.findUnique.mockResolvedValue({
|
|
id: "cmt-99",
|
|
postId: "post-1",
|
|
petId: "pet-2", // different pet
|
|
body: "Someone else's comment",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
await expect(
|
|
caller.comments.delete({ commentId: "cmt-99", petId: "pet-1" })
|
|
).rejects.toThrow(TRPCError);
|
|
});
|
|
});
|