test(01-04): implement 7 media upload unit tests covering security and validation

- Test 1: getPresignedUrl generates URL with correct contentType and avatars/-scoped key
- Test 2: getPresignedUrl rejects non-owner with FORBIDDEN (T-4-01)
- Test 3: getPresignedUrl rejects image/gif (Zod enum validation, T-4-02)
- Test 4: getPresignedUrl rejects 11MB file (T-4-06 fileSize limit)
- Test 5: confirmUpload writes avatarKey to DB with correct update call
- Test 6: confirmUpload rejects path traversal key (T-4-05)
- Bonus: confirmUpload rejects non-owner with FORBIDDEN (T-4-01)
- Mock getPresignedUploadUrl via vi.mock — no real R2 calls
This commit is contained in:
admin committed 2026-06-06 11:11:40 +02:00
1 parent fb8bb5c8fe
commit 62b7cdaf1e
1 file changed
+219 -24
+219 -24
View File
@@ -1,36 +1,231 @@
/**
* Wave 0 Test Scaffold — Media Upload
* Media Upload Unit Tests — Plan 04
*
* Covers: Presigned R2 URL generation with correct ContentType
* Covers: Presigned R2 URL generation and confirmation pipeline
*
* These tests verify that:
* 1. getPresignedUploadUrl generates a URL using the provided key and contentType
* 2. The contentType in the presigned URL matches what is sent to the S3 client
* 3. The URL is never exposed as a raw R2 key (only served as signed URL)
* Test 1 — getPresignedUrl generates URL with correct contentType
* Test 2 — getPresignedUrl rejects non-owner (FORBIDDEN)
* Test 3 — getPresignedUrl rejects unsupported contentType (Zod BAD_REQUEST)
* Test 4 — getPresignedUrl rejects oversized file (Zod BAD_REQUEST)
* Test 5 — confirmUpload writes avatarKey to DB
* Test 6 — confirmUpload rejects arbitrary key path (Zod BAD_REQUEST)
*
* Implementation will live in src/lib/r2.ts (Plan 01, Task 2).
* Security (T-4-01, T-4-02, T-4-03, T-4-05, T-4-06):
* - Ownership assertions tested (T-4-01)
* - ContentType restriction tested (T-4-02)
* - No real R2 calls — getPresignedUploadUrl is mocked (T-4-03)
* - key.startsWith('avatars/') validation tested (T-4-05)
* - fileSize <= 10MB validation tested (T-4-06)
*
* All Prisma calls are mocked — no real DB connections.
*/
import { describe, it } from "vitest";
import { describe, it, expect, beforeEach, vi } from "vitest";
import { TRPCError } from "@trpc/server";
import { mediaRouter } from "@/trpc/routers/media";
import { createMockPrisma } from "@/__tests__/helpers/prisma-mock";
describe("Presigned R2 upload URL", () => {
it.todo(
"getPresignedUploadUrl returns a string URL signed for the given key and contentType"
);
// Mock server-only — the import guard throws in non-server environments
vi.mock("server-only", () => ({}));
it.todo(
"getPresignedUploadUrl encodes the correct ContentType in the presigned URL signature — mismatch causes R2 403"
);
// Mock getPresignedUploadUrl so no real R2 calls are made (T-4-03)
vi.mock("@/lib/r2", () => ({
getPresignedUploadUrl: vi
.fn()
.mockResolvedValue("https://fake-r2.example.com/presigned-url"),
}));
it.todo(
"getPresignedUploadUrl sets expiresIn to 300 seconds (5 minutes)"
);
// Mock Clerk server auth
vi.mock("@clerk/nextjs/server", () => ({
auth: vi.fn().mockResolvedValue({ userId: "owner-1" }),
}));
it.todo(
"getPresignedUploadUrl only accepts image/jpeg, image/png, image/webp — rejects other content types"
);
// Mock the prisma singleton used by trpc/init
vi.mock("@/lib/prisma", () => ({
prisma: {},
}));
it.todo(
"R2 credentials (R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY) are NOT exposed in the presigned URL or client bundle"
);
import { getPresignedUploadUrl } from "@/lib/r2";
const OWNER_ID = "owner-1";
const ATTACKER_ID = "attacker-999";
const PET_ID = "pet-1";
const AVATAR_KEY = "avatars/owner-1/pet-1/some-uuid.jpg";
const MOCK_PET = {
id: PET_ID,
name: "Buddy",
ownerId: OWNER_ID,
speciesId: "species-dog",
breedId: null,
bio: null,
adoptionStory: null,
avatarKey: null,
createdAt: new Date(),
updatedAt: new Date(),
};
/**
* Create a tRPC caller with a mock context.
* Bypasses HTTP entirely — calls procedure logic directly.
*/
function createTestCaller(
userId: string,
mockPrisma: ReturnType<typeof createMockPrisma>
) {
type CallerCtx = {
userId: string | null;
prisma: import("@prisma/client").PrismaClient;
};
const ctx: CallerCtx = {
userId,
prisma: mockPrisma as unknown as import("@prisma/client").PrismaClient,
};
return mediaRouter.createCaller(ctx);
}
describe("media.getPresignedUrl", () => {
let mockPrisma: ReturnType<typeof createMockPrisma>;
beforeEach(() => {
mockPrisma = createMockPrisma();
vi.clearAllMocks();
});
it("Test 1 — generates presigned URL with correct contentType and scoped key", async () => {
mockPrisma.pet.findUnique.mockResolvedValue({ ...MOCK_PET, ownerId: OWNER_ID });
const caller = createTestCaller(OWNER_ID, mockPrisma);
const result = await caller.getPresignedUrl({
petId: PET_ID,
contentType: "image/jpeg",
fileSize: 1024,
});
// Assert getPresignedUploadUrl was called with correct arguments
expect(getPresignedUploadUrl).toHaveBeenCalledOnce();
const [calledKey, calledContentType] = (
getPresignedUploadUrl as ReturnType<typeof vi.fn>
).mock.calls[0];
expect(calledKey).toMatch(new RegExp(`^avatars/${OWNER_ID}/${PET_ID}/`));
expect(calledContentType).toBe("image/jpeg");
// Assert returned URL matches mock
expect(result.presignedUrl).toBe("https://fake-r2.example.com/presigned-url");
// Assert key is scoped under avatars/
expect(result.key).toMatch(/^avatars\//);
});
it("Test 2 — rejects non-owner with FORBIDDEN (T-4-01)", async () => {
// Pet belongs to OWNER_ID, but attacker is calling
mockPrisma.pet.findUnique.mockResolvedValue({ ...MOCK_PET, ownerId: OWNER_ID });
const caller = createTestCaller(ATTACKER_ID, mockPrisma);
await expect(
caller.getPresignedUrl({
petId: PET_ID,
contentType: "image/jpeg",
fileSize: 1024,
})
).rejects.toMatchObject({
code: "FORBIDDEN",
});
// No presigned URL should be generated for non-owner
expect(getPresignedUploadUrl).not.toHaveBeenCalled();
});
it("Test 3 — rejects unsupported contentType image/gif (Zod BAD_REQUEST, T-4-02)", async () => {
const caller = createTestCaller(OWNER_ID, mockPrisma);
await expect(
caller.getPresignedUrl({
petId: PET_ID,
contentType: "image/gif" as "image/jpeg",
fileSize: 1024,
})
).rejects.toThrow(TRPCError);
// No DB lookup or presigned URL should be generated for invalid type
expect(getPresignedUploadUrl).not.toHaveBeenCalled();
});
it("Test 4 — rejects oversized file (11MB > 10MB limit, T-4-06)", async () => {
const caller = createTestCaller(OWNER_ID, mockPrisma);
await expect(
caller.getPresignedUrl({
petId: PET_ID,
contentType: "image/jpeg",
fileSize: 11 * 1024 * 1024, // 11 MB — exceeds the 10 MB limit
})
).rejects.toThrow(TRPCError);
expect(getPresignedUploadUrl).not.toHaveBeenCalled();
});
});
describe("media.confirmUpload", () => {
let mockPrisma: ReturnType<typeof createMockPrisma>;
beforeEach(() => {
mockPrisma = createMockPrisma();
vi.clearAllMocks();
});
it("Test 5 — writes avatarKey to DB for pet owner", async () => {
mockPrisma.pet.findUnique.mockResolvedValue({ ...MOCK_PET, ownerId: OWNER_ID });
mockPrisma.pet.update.mockResolvedValue({
...MOCK_PET,
avatarKey: AVATAR_KEY,
});
const caller = createTestCaller(OWNER_ID, mockPrisma);
const result = await caller.confirmUpload({
petId: PET_ID,
key: AVATAR_KEY,
});
// Assert DB update was called with the correct avatarKey
expect(mockPrisma.pet.update).toHaveBeenCalledWith({
where: { id: PET_ID },
data: { avatarKey: AVATAR_KEY },
});
// Assert return value contains the avatarKey
expect(result).toMatchObject({ avatarKey: AVATAR_KEY });
});
it("Test 6 — rejects arbitrary key path not starting with 'avatars/' (T-4-05)", async () => {
const caller = createTestCaller(OWNER_ID, mockPrisma);
await expect(
caller.confirmUpload({
petId: PET_ID,
key: "malicious/../../etc/passwd",
})
).rejects.toThrow(TRPCError);
// No DB write should occur for path traversal attempts
expect(mockPrisma.pet.update).not.toHaveBeenCalled();
});
it("Bonus — rejects non-owner confirmUpload with FORBIDDEN (T-4-01)", async () => {
mockPrisma.pet.findUnique.mockResolvedValue({ ...MOCK_PET, ownerId: OWNER_ID });
const caller = createTestCaller(ATTACKER_ID, mockPrisma);
await expect(
caller.confirmUpload({
petId: PET_ID,
key: AVATAR_KEY,
})
).rejects.toMatchObject({
code: "FORBIDDEN",
});
expect(mockPrisma.pet.update).not.toHaveBeenCalled();
});
});