- 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>
107 lines
3.5 KiB
TypeScript
107 lines
3.5 KiB
TypeScript
/**
|
|
* Explore Unit Tests — Phase 3 Plan 03-05
|
|
*
|
|
* Covers: EXPL-01 (explore page — browse pets by species/breed)
|
|
*
|
|
* Tests:
|
|
* - listPets returns all pets when no filter
|
|
* - listPets filters by speciesId
|
|
* - cursor pagination: returns nextCursor when more results exist
|
|
*/
|
|
|
|
import { describe, it, expect, vi } from "vitest";
|
|
|
|
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 createExploreCaller(overrides?: {
|
|
prisma?: ReturnType<typeof createMockPrisma>;
|
|
}) {
|
|
const { exploreRouter } = await import("@/trpc/routers/explore");
|
|
const { initTRPC } = await import("@trpc/server");
|
|
|
|
const mockPrisma = overrides?.prisma ?? createMockPrisma();
|
|
|
|
const t = initTRPC
|
|
.context<{ userId: string | null; prisma: typeof mockPrisma }>()
|
|
.create();
|
|
|
|
const testRouter = t.router({ explore: exploreRouter });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const caller = testRouter.createCaller({ userId: "owner-1", prisma: mockPrisma as any });
|
|
return { caller, mockPrisma };
|
|
}
|
|
|
|
const makePet = (id: string, speciesId: string) => ({
|
|
id,
|
|
name: `Pet ${id}`,
|
|
speciesId,
|
|
breedId: null,
|
|
avatarKey: null,
|
|
ownerId: "owner-1",
|
|
createdAt: new Date(),
|
|
species: { id: speciesId, name: "Dog" },
|
|
breed: null,
|
|
_count: { followers: 10 },
|
|
});
|
|
|
|
// ─── explore.listPets ────────────────────────────────────────────────────────
|
|
|
|
describe("explore.listPets", () => {
|
|
it("EXPL-01 — returns all pets when no filter applied", async () => {
|
|
const { caller, mockPrisma } = await createExploreCaller();
|
|
|
|
const pets = [makePet("pet-1", "species-dog"), makePet("pet-2", "species-cat")];
|
|
mockPrisma.pet.findMany.mockResolvedValue(pets);
|
|
|
|
const result = await caller.explore.listPets({ limit: 20 });
|
|
|
|
expect(result.pets).toHaveLength(2);
|
|
expect(result.nextCursor).toBeNull();
|
|
});
|
|
|
|
it("EXPL-01 — filters by speciesId when provided", async () => {
|
|
const { caller, mockPrisma } = await createExploreCaller();
|
|
|
|
const pets = [makePet("pet-1", "species-dog")];
|
|
mockPrisma.pet.findMany.mockResolvedValue(pets);
|
|
|
|
const result = await caller.explore.listPets({ speciesId: "species-dog", limit: 20 });
|
|
|
|
expect(mockPrisma.pet.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({ speciesId: "species-dog" }),
|
|
})
|
|
);
|
|
expect(result.pets).toHaveLength(1);
|
|
});
|
|
|
|
it("EXPL-01 — cursor pagination: returns nextCursor when more results exist", async () => {
|
|
const { caller, mockPrisma } = await createExploreCaller();
|
|
|
|
// Return limit+1 pets to trigger nextCursor
|
|
const pets = Array.from({ length: 21 }, (_, i) => makePet(`pet-${i}`, "species-dog"));
|
|
mockPrisma.pet.findMany.mockResolvedValue(pets);
|
|
|
|
const result = await caller.explore.listPets({ limit: 20 });
|
|
|
|
expect(result.pets).toHaveLength(20);
|
|
expect(result.nextCursor).not.toBeNull();
|
|
});
|
|
});
|