Files
petfeed/src/__tests__/reposts.test.ts
T
adminandClaude Sonnet 4.6 73666a3916 feat(03-01): Phase 3 schema foundation — Reaction/Comment/Repost/Hashtag models + router stubs RED
- Prisma: add Reaction, Comment, Repost, Hashtag, PostHashtag models; PostType.REPOST enum
- Back-relations on Post and Pet for all Phase 3 models (named to avoid ambiguous-relation errors)
- 5 tRPC router stubs: reactions, comments, reposts, explore, search — all registered in _app.ts
- Extend prisma-mock.ts with Phase 3 model mocks
- 5 test scaffold files: 14 failing RED + 3 FORBIDDEN passing; 53 Phase 2 tests untouched
- Phase 3 planning: CONTEXT, RESEARCH, UI-SPEC, VALIDATION, 6 PLANs, ROADMAP + STATE updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 11:31:48 +02:00

118 lines
4.2 KiB
TypeScript

/**
* Reposts Unit Tests — Phase 3 Plan 03-04
*
* Covers: ENGM-04 (paw-back / repost)
*
* Tests:
* - create repost creates Post(type=REPOST) + Repost + fanOut
* - repost-of-repost is FORBIDDEN
* - second repost is idempotent (returns existing postId)
*/
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: {} }));
const mockFanOutPost = vi.fn().mockResolvedValue(undefined);
vi.mock("@/lib/feed-helpers", () => ({
fanOutPost: mockFanOutPost,
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 createRepostsCaller(overrides?: {
userId?: string | null;
prisma?: ReturnType<typeof createMockPrisma>;
}) {
const { repostsRouter } = await import("@/trpc/routers/reposts");
const { initTRPC } = await import("@trpc/server");
const mockPrisma = overrides?.prisma ?? createMockPrisma();
const userId = overrides?.userId !== undefined ? overrides.userId : "owner-1";
const t = initTRPC
.context<{ userId: string | null; prisma: typeof mockPrisma }>()
.create();
const testRouter = t.router({ reposts: repostsRouter });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const caller = testRouter.createCaller({ userId, prisma: mockPrisma as any });
return { caller, mockPrisma };
}
// ─── reposts.create ──────────────────────────────────────────────────────────
describe("reposts.create", () => {
it("ENGM-04 — create repost creates Post(type=REPOST) and calls fanOutPost", async () => {
const { caller, mockPrisma } = await createRepostsCaller();
mockPrisma.pet.findUnique.mockResolvedValue({ id: "pet-1", ownerId: "owner-1", name: "Buddy" });
mockPrisma.post.findUnique.mockResolvedValue({ id: "orig-1", type: "PHOTO", petId: "pet-2" });
mockPrisma.repost.findUnique.mockResolvedValue(null); // no existing repost
const createdAt = new Date();
mockPrisma.post.create.mockResolvedValue({
id: "repost-1",
petId: "pet-1",
type: "REPOST",
createdAt,
});
const result = await caller.reposts.create({
originalPostId: "orig-1",
reposterPetId: "pet-1",
});
expect(mockPrisma.post.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ type: "REPOST", petId: "pet-1" }),
})
);
expect(mockFanOutPost).toHaveBeenCalled();
expect(result).toMatchObject({ postId: "repost-1" });
});
it("ENGM-04 — repost-of-repost is FORBIDDEN", async () => {
const { caller, mockPrisma } = await createRepostsCaller();
mockPrisma.pet.findUnique.mockResolvedValue({ id: "pet-1", ownerId: "owner-1", name: "Buddy" });
mockPrisma.post.findUnique.mockResolvedValue({ id: "orig-1", type: "REPOST", petId: "pet-2" });
await expect(
caller.reposts.create({ originalPostId: "orig-1", reposterPetId: "pet-1" })
).rejects.toThrow(TRPCError);
});
it("ENGM-04 — second repost is idempotent: returns existing postId without duplicate create", async () => {
const { caller, mockPrisma } = await createRepostsCaller();
mockPrisma.pet.findUnique.mockResolvedValue({ id: "pet-1", ownerId: "owner-1", name: "Buddy" });
mockPrisma.post.findUnique.mockResolvedValue({ id: "orig-1", type: "PHOTO", petId: "pet-2" });
mockPrisma.repost.findUnique.mockResolvedValue({
postId: "existing-repost-1",
originalPostId: "orig-1",
reposterPetId: "pet-1",
createdAt: new Date(),
});
const result = await caller.reposts.create({
originalPostId: "orig-1",
reposterPetId: "pet-1",
});
expect(mockPrisma.post.create).not.toHaveBeenCalled();
expect(result).toMatchObject({ postId: "existing-repost-1" });
});
});