Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion src/app/api/affiliates/offers/[id]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>) {
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 }) };
}
Expand Down Expand Up @@ -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");
});
});
Comment on lines +125 to +156

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No success-path test for PATCH

The two new tests only exercise rejection (400) scenarios. There is no test that verifies a valid string title or description actually flows through to a DB update and returns 200. If the update logic were accidentally broken (e.g., updateData construction or the .update() call), both existing tests would still pass, leaving the regression undetected.

15 changes: 12 additions & 3 deletions src/app/api/affiliates/offers/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 ESLint suppression removed without removing the any usage

The // eslint-disable-next-line @typescript-eslint/no-explicit-any comment was deleted, but type AnySupabase = any still uses an explicit any. If the project ever tightens the @typescript-eslint/no-explicit-any rule to "error", this line will start failing lint without any obvious history of why the suppression existed.

import { validateOfferInput } from "@/lib/affiliates/validation";

Expand Down Expand Up @@ -100,8 +99,18 @@ export async function PATCH(
// Partial validation — only validate provided fields
const updateData: Record<string, unknown> = { 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;
Expand Down
Loading