Skip to content

Validate affiliate conversion ids before mutation queries#459

Merged
ralyodio merged 1 commit into
profullstack:masterfrom
rissrice2105-agent:codex/affiliate-conversion-id-validation
Jun 14, 2026
Merged

Validate affiliate conversion ids before mutation queries#459
ralyodio merged 1 commit into
profullstack:masterfrom
rissrice2105-agent:codex/affiliate-conversion-id-validation

Conversation

@rissrice2105-agent

Copy link
Copy Markdown
Contributor

Closes #458.

Summary

  • Require conversion_id to be a non-empty string for affiliate conversion PUT and DELETE mutations.
  • Trim the accepted ID before passing it to Supabase queries.
  • Add regression coverage that non-string IDs return 400 before querying affiliate_conversions.

Verification

  • corepack pnpm vitest run 'src/app/api/affiliates/offers/[id]/conversions/route.test.ts'
  • corepack pnpm tsc --noEmit

@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR tightens conversion_id validation in the affiliate conversions PUT and DELETE handlers by replacing the loose falsy check with an explicit isNonEmptyString type guard, and trims the accepted value before forwarding it to Supabase.

  • route.ts: Adds the isNonEmptyString helper and applies it in both PUT and DELETE, preventing non-string values (objects, arrays, numbers) and whitespace-only strings from reaching the database layer.
  • route.test.ts: Adds a regression test for each handler asserting that a non-string conversion_id returns 400 before affiliate_conversions is queried; also imports and adds a makeDeleteRequest helper.

Confidence Score: 4/5

Safe to merge — the validation logic is correct and the existing conversion flows are unchanged.

The core guard and trim logic in route.ts is solid. The test additions cover the stated regression cases, but the whitespace-only string path (a newly rejected case) and DELETE’s success/auth/status branches are untested, leaving some branches dark.

route.test.ts — the DELETE describe block and the missing whitespace-only assertion would benefit from a bit more coverage before shipping.

Important Files Changed

Filename Overview
src/app/api/affiliates/offers/[id]/conversions/route.ts Adds isNonEmptyString guard for conversion_id in PUT and DELETE handlers; trims the accepted value before passing it to Supabase. Logic is correct and consistent across both handlers.
src/app/api/affiliates/offers/[id]/conversions/route.test.ts Adds regression tests for non-string conversion_id in PUT and DELETE; misses whitespace-only string case (newly rejectable) and DELETE happy-path/auth/status coverage.

Sequence Diagram

sequenceDiagram
    participant Client
    participant PUT/DELETE Handler
    participant affiliate_offers
    participant affiliate_conversions

    Client->>PUT/DELETE Handler: Request with conversion_id
    PUT/DELETE Handler->>affiliate_offers: SELECT (auth/ownership check)
    affiliate_offers-->>PUT/DELETE Handler: offer row
    PUT/DELETE Handler->>PUT/DELETE Handler: isNonEmptyString(conversion_id)?
    alt non-string or whitespace-only
        PUT/DELETE Handler-->>Client: 400 conversion_id is required
    else valid string
        PUT/DELETE Handler->>PUT/DELETE Handler: conversionId = conversion_id.trim()
        PUT/DELETE Handler->>affiliate_conversions: UPDATE/SELECT+DELETE .eq("id", conversionId)
        affiliate_conversions-->>PUT/DELETE Handler: result
        PUT/DELETE Handler-->>Client: 200 ok / 400 / 404
    end
Loading

Reviews (1): Last reviewed commit: "Validate affiliate conversion ids" | Re-trigger Greptile

Comment on lines +401 to +430
it("rejects non-string conversion_id before updating conversions", async () => {
mockGetAuthContext.mockResolvedValue({
user: { id: "user-seller", authMethod: "session" },
});

mockFrom.mockImplementation((table: string) => {
if (table === "affiliate_offers") {
return chainable({
id: "offer-1",
seller_id: "user-seller",
commission_rate: 0.2,
commission_type: "percentage",
commission_flat_sats: 0,
});
}
return chainable([]);
});

const res = await PUT(
makePutRequest("offer-1", {
conversion_id: { id: "conv-1" },
note: "paid elsewhere",
}),
makeParams("offer-1")
);

expect(res.status).toBe(400);
await expect(res.json()).resolves.toEqual({ error: "conversion_id is required" });
expect(mockFrom).not.toHaveBeenCalledWith("affiliate_conversions");
});

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 Whitespace-only conversion_id not tested

The new isNonEmptyString guard rejects whitespace-only strings (e.g., " "), which is a behavior change from the old !conversion_id check — a whitespace string is truthy and previously would have been forwarded to Supabase. Neither the PUT nor the DELETE new tests cover this path, so that specific validation branch has no regression coverage.

Comment on lines +519 to +550
describe("DELETE /api/affiliates/offers/[id]/conversions", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("rejects non-string conversion_id before querying conversions", async () => {
mockGetAuthContext.mockResolvedValue({
user: { id: "user-seller", authMethod: "session" },
});

mockFrom.mockImplementation((table: string) => {
if (table === "affiliate_offers") {
return chainable({
id: "offer-1",
seller_id: "user-seller",
});
}
return chainable([]);
});

const res = await DELETE(
makeDeleteRequest("offer-1", {
conversion_id: ["conv-1"],
}),
makeParams("offer-1")
);

expect(res.status).toBe(400);
await expect(res.json()).resolves.toEqual({ error: "conversion_id is required" });
expect(mockFrom).not.toHaveBeenCalledWith("affiliate_conversions");
});
});

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 DELETE handler has only one negative test

The new describe("DELETE ...") block adds a single rejection test. The DELETE handler also guards against unauthorized users, non-existent conversions (404), and deleting paid conversions — none of which are covered. A regression on any of those branches would go undetected.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@rissrice2105-agent

Copy link
Copy Markdown
Contributor Author

CI is green for PR #459.

Verification:

  • corepack pnpm vitest run 'src/app/api/affiliates/offers/[id]/conversions/route.test.ts'
  • corepack pnpm tsc --noEmit

uGig invoice evidence has been sent for this PR.

@ralyodio ralyodio merged commit 5c16251 into profullstack:master Jun 14, 2026
6 checks passed
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.

PUT/DELETE affiliate conversions accept non-string conversion_id values

2 participants