Skip to content

Validate affiliate edit product URLs - #138

Closed
absalonCRC wants to merge 1 commit into
profullstack:masterfrom
absalonCRC:fix-affiliate-edit-product-url-validation
Closed

Validate affiliate edit product URLs#138
absalonCRC wants to merge 1 commit into
profullstack:masterfrom
absalonCRC:fix-affiliate-edit-product-url-validation

Conversation

@absalonCRC

@absalonCRC absalonCRC commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • validate product_url updates in PATCH /api/affiliates/offers/[id] instead of writing the raw request value
  • trim valid http(s) URLs before saving and allow explicit clearing with null or blank strings
  • add regression coverage for rejecting javascript: URLs and preserving trimmed valid URLs

Fixes #137

Validation

  • pnpm test:run 'src/app/api/affiliates/offers/[id]/route.test.ts' src/lib/affiliates/validation.test.ts
  • pnpm exec eslint 'src/app/api/affiliates/offers/[id]/route.ts' 'src/app/api/affiliates/offers/[id]/route.test.ts' src/lib/affiliates/validation.ts src/lib/affiliates/validation.test.ts
  • pnpm type-check
  • git diff --check

Bounty / payment

Submitted for the active uGig affiliate-program testing task. SOL receive address: 27sdMYXofqoM9qR13bZhccRNYeEgYn5EoHXTSJn4QWKP.

Payment fallback: PayPal cultofrozen@gmail.com

@greptile-apps

greptile-apps Bot commented May 23, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds URL validation to the PATCH /api/affiliates/offers/[id] endpoint, preventing raw (potentially malicious) values like javascript: URLs from being persisted as product_url. The fix reuses the existing isValidUrl helper from @/lib/affiliates/validation and adds explicit handling for null and blank-string clearing.

  • route.ts: Replaces the bare updateData.product_url = body.product_url assignment with a validation branch that type-checks the value, trims whitespace, rejects non-http(s) schemes with a 400, and maps null/blank to a DB null.
  • route.test.ts: Removes the now-unused validateOfferInput mock and adds a PATCH test suite covering javascript: rejection and whitespace-trimming; clearing via null or blank string is not yet tested.

Confidence Score: 4/5

Safe to merge — the core fix is correct and well-scoped; only test coverage for the clearing paths is missing.

The validation logic itself is solid: all five input cases (null, non-string, blank, invalid scheme, valid URL) are handled correctly in the handler. The gap is that the null and blank-string clearing behaviors documented in the PR description are not exercised by any test, so a future regression there would go undetected.

route.test.ts — the new PATCH describe block is missing tests for the null and whitespace-only clearing branches.

Important Files Changed

Filename Overview
src/app/api/affiliates/offers/[id]/route.ts Adds proper product_url validation to the PATCH handler using isValidUrl; handles null, blank, non-string, and invalid-scheme inputs correctly. Dropped an eslint-disable comment for the AnySupabase type alias — lint passes per the author's validation run.
src/app/api/affiliates/offers/[id]/route.test.ts Adds PATCH test suite covering javascript: rejection and whitespace-trimming; missing coverage for the null and blank-string clearing paths documented in the PR description.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[PATCH /api/affiliates/offers/:id] --> B{Authenticated?}
    B -- No --> C[401 Unauthorized]
    B -- Yes --> D{Offer exists & owned?}
    D -- No --> E[404 Not found]
    D -- Yes --> F{product_url in body?}
    F -- No --> K[Skip field]
    F -- Yes --> G{value === null?}
    G -- Yes --> H[Set product_url = null]
    G -- No --> I{typeof string?}
    I -- No --> J[400 must be a string]
    I -- Yes --> L{trimmed length === 0?}
    L -- Yes --> H
    L -- No --> M{isValidUrl?}
    M -- No --> N[400 must use http/https]
    M -- Yes --> O[Set product_url = trimmed value]
    H & K & O --> P[Run DB update]
    P --> Q[200 OK with offer]
Loading

Reviews (1): Last reviewed commit: "Validate affiliate edit product URLs" | Re-trigger Greptile

Comment on lines +113 to +179
describe("PATCH /api/affiliates/offers/[id]", () => {
beforeEach(() => {
vi.clearAllMocks();
mockGetAuthContext.mockResolvedValue({
user: { id: "seller1", authMethod: "session" },
});
});

it("rejects non-http product_url updates", async () => {
mockFrom.mockImplementation((table: string) => {
if (table === "affiliate_offers") {
return chainable({ id: "offer-1", seller_id: "seller1" });
}
return chainable([]);
});

const req = new NextRequest("http://localhost/api/affiliates/offers/offer-1", {
method: "PATCH",
body: JSON.stringify({ product_url: "javascript:alert(1)" }),
});

const res = await PATCH(req, makeParams("offer-1"));
const body = await res.json();

expect(res.status).toBe(400);
expect(body.error).toBe("product_url must use http:// or https:// scheme");
});

it("trims valid product_url updates before saving", async () => {
mockFrom.mockImplementation((table: string) => {
if (table !== "affiliate_offers") return chainable([]);

const update = vi.fn((data: Record<string, unknown>) => ({
eq: vi.fn(() => ({
select: vi.fn(() => ({
single: vi.fn(() => Promise.resolve({
data: { id: "offer-1", seller_id: "seller1", ...data },
error: null,
})),
})),
})),
}));

return {
select: vi.fn(() => ({
eq: vi.fn(() => ({
single: vi.fn(() => Promise.resolve({
data: { id: "offer-1", seller_id: "seller1" },
error: null,
})),
})),
})),
update,
};
});

const req = new NextRequest("http://localhost/api/affiliates/offers/offer-1", {
method: "PATCH",
body: JSON.stringify({ product_url: " https://example.com/product " }),
});

const res = await PATCH(req, makeParams("offer-1"));
const body = await res.json();

expect(res.status).toBe(200);
expect(body.offer.product_url).toBe("https://example.com/product");
});

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 Missing tests for the null / blank-string clearing paths

The PR description calls out "allow explicit clearing with null or blank strings" as a key behavior, but neither branch is exercised. Sending { product_url: null } should persist null, and sending { product_url: " " } should also clear it — both paths exist in the handler (lines 104–111) but have no regression test. If a future refactor accidentally drops the === null guard or the length === 0 branch, nothing will catch it.

@ralyodio ralyodio closed this May 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: affiliate offer edits accept invalid product URLs

2 participants