Files
petfeed/src/__tests__/search.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

128 lines
4.5 KiB
TypeScript

/**
* Search Unit Tests — Phase 3 Plans 03-05 + 03-06
*
* Covers: SRCH-01 (hashtag search), SRCH-02 (pet name/breed search)
*
* Tests:
* - pets: matches by name ILIKE
* - pets: matches by breed name
* - byHashtag: returns posts for known tag
* - byHashtag: returns empty for unknown tag
*/
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 createSearchCaller(overrides?: {
prisma?: ReturnType<typeof createMockPrisma>;
}) {
const { searchRouter } = await import("@/trpc/routers/search");
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({ search: searchRouter });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const caller = testRouter.createCaller({ userId: "owner-1", prisma: mockPrisma as any });
return { caller, mockPrisma };
}
// ─── search.pets ─────────────────────────────────────────────────────────────
describe("search.pets", () => {
it("SRCH-02 — matches pets by name", async () => {
const { caller, mockPrisma } = await createSearchCaller();
const mockPets = [
{ id: "pet-1", name: "Buddy", species: { name: "Dog" }, breed: null, _count: { followers: 3 } },
];
mockPrisma.pet.findMany.mockResolvedValue(mockPets);
const result = await caller.search.pets({ query: "Buddy" });
expect(mockPrisma.pet.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
OR: expect.arrayContaining([
expect.objectContaining({ name: expect.objectContaining({ contains: "Buddy" }) }),
]),
}),
})
);
expect(result.pets).toHaveLength(1);
expect(result.pets[0].name).toBe("Buddy");
});
it("SRCH-02 — matches pets by breed name", async () => {
const { caller, mockPrisma } = await createSearchCaller();
const mockPets = [
{ id: "pet-2", name: "Max", species: { name: "Dog" }, breed: { name: "Golden Retriever" }, _count: { followers: 5 } },
];
mockPrisma.pet.findMany.mockResolvedValue(mockPets);
const result = await caller.search.pets({ query: "Golden" });
expect(result.pets).toHaveLength(1);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((result.pets[0] as any).breed?.name).toBe("Golden Retriever");
});
});
// ─── search.byHashtag ────────────────────────────────────────────────────────
describe("search.byHashtag", () => {
it("SRCH-01 — returns posts for a known hashtag", async () => {
const { caller, mockPrisma } = await createSearchCaller();
mockPrisma.hashtag.findUnique.mockResolvedValue({ id: "ht-1", tag: "goldenretriever" });
const mockPost = {
id: "post-1",
petId: "pet-1",
type: "PHOTO",
caption: "#goldenretriever love",
createdAt: new Date(),
pet: { id: "pet-1", name: "Buddy", avatarKey: null },
images: [{ storageKey: "posts/img1.jpg", position: 0 }],
};
mockPrisma.postHashtag.findMany.mockResolvedValue([{ post: mockPost }]);
const result = await caller.search.byHashtag({ tag: "goldenretriever" });
expect(result.posts).toHaveLength(1);
expect(result.nextCursor).toBeNull();
});
it("SRCH-01 — returns empty array for unknown hashtag", async () => {
const { caller, mockPrisma } = await createSearchCaller();
mockPrisma.hashtag.findUnique.mockResolvedValue(null);
const result = await caller.search.byHashtag({ tag: "nonexistenttag" });
expect(result.posts).toHaveLength(0);
expect(result.nextCursor).toBeNull();
});
});