test(02-04): add failing tests for posts router RED
- PHOTO-01: create single photo inserts Post + PostImage + fans out - PHOTO-02: carousel limit rejects >10 images (Zod already validates) - PHOTO-03: updateCaption forbidden for non-owner → FORBIDDEN - PHOTO-04: delete calls prisma.post.delete (PostImages cascade via schema) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
bdd676f19b
commit
5e71010e50
1 file changed
+194
-9
+194
-9
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Posts Unit Tests — Phase 2 Plan 02-01 (scaffold)
|
||||
* Posts Unit Tests — Phase 2 Plan 02-04
|
||||
*
|
||||
* Covers: PHOTO-01, PHOTO-02, PHOTO-03, PHOTO-04
|
||||
*
|
||||
@@ -8,15 +8,48 @@
|
||||
* Test: updateCaption forbidden — non-owner cannot edit (PHOTO-03)
|
||||
* Test: delete cascades PostImages (PHOTO-04)
|
||||
*
|
||||
* All tests are it.todo stubs until Wave 1 implementation.
|
||||
* No real DB or Redis calls — all mocked.
|
||||
* Redis mocked via vi.mock('@/lib/redis') — no real Upstash calls.
|
||||
* fanOutPost mocked via vi.mock('@/lib/feed-helpers').
|
||||
*/
|
||||
|
||||
import { describe, it, vi } from "vitest";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
// Mock server-only guard
|
||||
vi.mock("server-only", () => ({}));
|
||||
|
||||
// Mock Redis — no real Upstash calls
|
||||
vi.mock("@/lib/redis", () => ({
|
||||
redis: {
|
||||
zrange: vi.fn(),
|
||||
zadd: vi.fn(),
|
||||
pipeline: vi.fn(() => ({
|
||||
zadd: vi.fn(),
|
||||
exec: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock fanOutPost — we verify it is called with correct args
|
||||
vi.mock("@/lib/feed-helpers", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("@/lib/feed-helpers")>();
|
||||
return {
|
||||
...actual,
|
||||
fanOutPost: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock Supabase storage — no real storage calls
|
||||
vi.mock("@/lib/supabase-storage", () => ({
|
||||
createSignedUploadUrl: vi.fn().mockResolvedValue({
|
||||
signedUrl: "https://storage.example.com/signed",
|
||||
path: "posts/owner-1/pet-1/uuid.jpg",
|
||||
}),
|
||||
ALLOWED_CONTENT_TYPES: ["image/jpeg", "image/png", "image/webp"],
|
||||
MAX_FILE_SIZE: 10 * 1024 * 1024,
|
||||
}));
|
||||
|
||||
// Mock Clerk server auth
|
||||
vi.mock("@clerk/nextjs/server", () => ({
|
||||
auth: vi.fn().mockResolvedValue({ userId: "owner-1" }),
|
||||
@@ -27,16 +60,168 @@ vi.mock("@/lib/prisma", () => ({
|
||||
prisma: {},
|
||||
}));
|
||||
|
||||
describe("posts.create", () => {
|
||||
it.todo("PHOTO-01 — create single photo inserts Post + PostImage with correct petId");
|
||||
import { postsRouter } from "@/trpc/routers/posts";
|
||||
import { fanOutPost } from "@/lib/feed-helpers";
|
||||
import { createMockPrisma } from "@/__tests__/helpers/prisma-mock";
|
||||
|
||||
it.todo("PHOTO-02 — carousel limit rejects >10 images with BAD_REQUEST");
|
||||
const mockFanOutPost = vi.mocked(fanOutPost);
|
||||
|
||||
// Helper to create a tRPC caller with mocked context
|
||||
function createCaller(overrides?: {
|
||||
userId?: string;
|
||||
prisma?: ReturnType<typeof createMockPrisma>;
|
||||
}) {
|
||||
const mockPrisma = overrides?.prisma ?? createMockPrisma();
|
||||
const ctx = {
|
||||
userId: overrides?.userId ?? "owner-1",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
prisma: mockPrisma as any,
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return { caller: postsRouter.createCaller(ctx as any), mockPrisma };
|
||||
}
|
||||
|
||||
describe("posts.create", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("PHOTO-01 — create single photo inserts Post + PostImage with correct petId", async () => {
|
||||
const { caller, mockPrisma } = createCaller();
|
||||
|
||||
// Pet belongs to owner-1
|
||||
mockPrisma.pet.findUnique.mockResolvedValue({
|
||||
id: "pet-1",
|
||||
ownerId: "owner-1",
|
||||
});
|
||||
|
||||
const mockPost = {
|
||||
id: "post-1",
|
||||
petId: "pet-1",
|
||||
type: "PHOTO",
|
||||
caption: "Hello world",
|
||||
createdAt: new Date("2026-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2026-01-01T00:00:00Z"),
|
||||
images: [
|
||||
{
|
||||
id: "img-1",
|
||||
postId: "post-1",
|
||||
storageKey: "posts/owner-1/pet-1/uuid.jpg",
|
||||
position: 0,
|
||||
width: null,
|
||||
height: null,
|
||||
},
|
||||
],
|
||||
pet: { id: "pet-1", name: "Buddy", avatarKey: null },
|
||||
milestone: null,
|
||||
};
|
||||
|
||||
mockPrisma.post.create.mockResolvedValue(mockPost);
|
||||
|
||||
const result = await caller.create({
|
||||
petId: "pet-1",
|
||||
caption: "Hello world",
|
||||
imageKeys: ["posts/owner-1/pet-1/uuid.jpg"],
|
||||
});
|
||||
|
||||
// Post was created
|
||||
expect(mockPrisma.post.create).toHaveBeenCalledOnce();
|
||||
// Created with correct petId
|
||||
expect(mockPrisma.post.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
petId: "pet-1",
|
||||
type: "PHOTO",
|
||||
caption: "Hello world",
|
||||
}),
|
||||
})
|
||||
);
|
||||
// Fan-out was called (PHOTO-01 — post appears in followers' feeds)
|
||||
expect(mockFanOutPost).toHaveBeenCalledOnce();
|
||||
expect(mockFanOutPost).toHaveBeenCalledWith(
|
||||
expect.anything(), // redis
|
||||
"post-1",
|
||||
"pet-1",
|
||||
expect.any(Date),
|
||||
expect.anything() // prisma
|
||||
);
|
||||
// Result matches created post
|
||||
expect(result.id).toBe("post-1");
|
||||
});
|
||||
|
||||
it("PHOTO-02 — carousel limit rejects >10 images with BAD_REQUEST", async () => {
|
||||
const { caller } = createCaller();
|
||||
|
||||
// 11 valid keys should be rejected by Zod before hitting the mutation handler
|
||||
await expect(
|
||||
caller.create({
|
||||
petId: "pet-1",
|
||||
caption: null,
|
||||
imageKeys: Array.from(
|
||||
{ length: 11 },
|
||||
(_, i) => `posts/owner-1/pet-1/img-${i}.jpg`
|
||||
),
|
||||
})
|
||||
).rejects.toThrow(TRPCError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("posts.updateCaption", () => {
|
||||
it.todo("PHOTO-03 — updateCaption forbidden for non-owner returns FORBIDDEN");
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("PHOTO-03 — updateCaption forbidden for non-owner returns FORBIDDEN", async () => {
|
||||
// caller is owner-1, but the post belongs to owner-2's pet
|
||||
const { caller, mockPrisma } = createCaller({ userId: "owner-1" });
|
||||
|
||||
// Post belongs to pet-2 which is owned by owner-2
|
||||
mockPrisma.post.findUnique.mockResolvedValue({
|
||||
id: "post-1",
|
||||
petId: "pet-2",
|
||||
pet: { id: "pet-2", ownerId: "owner-2" },
|
||||
});
|
||||
|
||||
await expect(
|
||||
caller.updateCaption({ postId: "post-1", caption: "Changed caption" })
|
||||
).rejects.toThrow(TRPCError);
|
||||
|
||||
// Verify it was FORBIDDEN specifically
|
||||
try {
|
||||
await caller.updateCaption({ postId: "post-1", caption: "Changed" });
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(TRPCError);
|
||||
expect((err as TRPCError).code).toBe("FORBIDDEN");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("posts.delete", () => {
|
||||
it.todo("PHOTO-04 — delete cascades PostImages and removes post from feed");
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("PHOTO-04 — delete cascades PostImages and removes post from feed", async () => {
|
||||
const { caller, mockPrisma } = createCaller();
|
||||
|
||||
// Post belongs to pet-1 owned by owner-1
|
||||
mockPrisma.post.findUnique.mockResolvedValue({
|
||||
id: "post-1",
|
||||
petId: "pet-1",
|
||||
pet: { id: "pet-1", ownerId: "owner-1" },
|
||||
});
|
||||
|
||||
mockPrisma.post.delete.mockResolvedValue({
|
||||
id: "post-1",
|
||||
petId: "pet-1",
|
||||
});
|
||||
|
||||
await caller.delete({ postId: "post-1" });
|
||||
|
||||
// Prisma delete was called — PostImages cascade via schema onDelete:Cascade
|
||||
expect(mockPrisma.post.delete).toHaveBeenCalledOnce();
|
||||
expect(mockPrisma.post.delete).toHaveBeenCalledWith({
|
||||
where: { id: "post-1" },
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user