diff --git a/src/app/api/affiliates/offers/[id]/route.test.ts b/src/app/api/affiliates/offers/[id]/route.test.ts index ecc76a6a..1be6f75e 100644 --- a/src/app/api/affiliates/offers/[id]/route.test.ts +++ b/src/app/api/affiliates/offers/[id]/route.test.ts @@ -19,12 +19,20 @@ vi.mock("@/lib/affiliates/validation", () => ({ validateOfferInput: vi.fn(), })); -import { GET } from "./route"; +import { GET, PATCH } from "./route"; function makeRequest(id: string) { return new NextRequest(`http://localhost/api/affiliates/offers/${id}`); } +function makePatchRequest(id: string, body: Record) { + return new NextRequest(`http://localhost/api/affiliates/offers/${id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); +} + function makeParams(id: string) { return { params: Promise.resolve({ id }) }; } @@ -113,3 +121,36 @@ describe("GET /api/affiliates/offers/[id]", () => { expect(res.status).toBe(404); }); }); + +describe("PATCH /api/affiliates/offers/[id]", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockGetAuthContext.mockResolvedValue({ user: { id: "seller1" } }); + }); + + it("rejects non-string title updates", async () => { + mockFrom.mockReturnValue(chainable({ id: "offer1", seller_id: "seller1" })); + + const res = await PATCH( + makePatchRequest("offer1", { title: 123 }), + makeParams("offer1") + ); + const body = await res.json(); + + expect(res.status).toBe(400); + expect(body.error).toBe("title must be a string"); + }); + + it("rejects non-string description updates", async () => { + mockFrom.mockReturnValue(chainable({ id: "offer1", seller_id: "seller1" })); + + const res = await PATCH( + makePatchRequest("offer1", { description: { text: "not a string" } }), + makeParams("offer1") + ); + const body = await res.json(); + + expect(res.status).toBe(400); + expect(body.error).toBe("description must be a string"); + }); +}); diff --git a/src/app/api/affiliates/offers/[id]/route.ts b/src/app/api/affiliates/offers/[id]/route.ts index eec125d1..b00954b8 100644 --- a/src/app/api/affiliates/offers/[id]/route.ts +++ b/src/app/api/affiliates/offers/[id]/route.ts @@ -2,7 +2,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getAuthContext } from "@/lib/auth/get-user"; import { createServiceClient } from "@/lib/supabase/service"; -// eslint-disable-next-line @typescript-eslint/no-explicit-any type AnySupabase = any; import { validateOfferInput } from "@/lib/affiliates/validation"; @@ -100,8 +99,18 @@ export async function PATCH( // Partial validation — only validate provided fields const updateData: Record = { updated_at: new Date().toISOString() }; - if (body.title !== undefined) updateData.title = body.title.trim(); - if (body.description !== undefined) updateData.description = body.description.trim(); + if (body.title !== undefined) { + if (typeof body.title !== "string") { + return NextResponse.json({ error: "title must be a string" }, { status: 400 }); + } + updateData.title = body.title.trim(); + } + if (body.description !== undefined) { + if (typeof body.description !== "string") { + return NextResponse.json({ error: "description must be a string" }, { status: 400 }); + } + updateData.description = body.description.trim(); + } if (body.product_url !== undefined) updateData.product_url = body.product_url; if (body.product_type !== undefined) updateData.product_type = body.product_type; if (body.price_sats !== undefined) updateData.price_sats = body.price_sats;