Handle malformed affiliate application bodies - #164
Conversation
Greptile SummaryThis PR adds a
Confidence Score: 4/5Safe to merge — the change is narrowly scoped to body parsing in a single endpoint and does not alter auth, DB schema, or business logic. The route change is correct and the two new malformed-body tests exercise the intended fix. The only gap is the reject action path in the test file: approved_at exclusion and the affiliate_rejected notification type are untested, so a regression there would be invisible. route.test.ts — missing coverage for the reject action path. Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant PATCH Handler
participant readJsonObject
participant Supabase
Client->>PATCH Handler: PATCH /api/affiliates/offers/[id]/applications
PATCH Handler->>PATCH Handler: getAuthContext()
alt Not authenticated
PATCH Handler-->>Client: 401 Unauthorized
end
PATCH Handler->>readJsonObject: request.json()
alt Malformed JSON or non-object
readJsonObject-->>PATCH Handler: null
PATCH Handler-->>Client: 400 Invalid request body
else Valid object
readJsonObject-->>PATCH Handler: { application_id, action, ... }
end
alt Missing/invalid fields
PATCH Handler-->>Client: 400 application_id and action required
end
PATCH Handler->>Supabase: affiliate_offers.select().eq(id)
alt Offer not found or not owned
PATCH Handler-->>Client: 404 Not found or not authorized
end
PATCH Handler->>Supabase: affiliate_applications.update(status, ...)
PATCH Handler->>Supabase: notifications.insert(affiliate_approved|rejected)
PATCH Handler-->>Client: 200 { application }
Reviews (1): Last reviewed commit: "Handle malformed affiliate application b..." | Re-trigger Greptile |
| expect(mockFrom).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("approves an application and sends an approval notification", async () => { | ||
| const updatedApplication = { | ||
| id: "app-1", | ||
| offer_id: "offer-1", | ||
| affiliate_id: "affiliate-1", | ||
| status: "approved", | ||
| profiles: { username: "alice" }, | ||
| }; | ||
| let updatePayload: Record<string, unknown> | undefined; | ||
| let notificationPayload: Record<string, unknown> | undefined; | ||
|
|
||
| mockFrom.mockImplementation((table: string) => { | ||
| if (table === "affiliate_offers") { | ||
| return { | ||
| select: () => ({ | ||
| eq: () => ({ | ||
| single: () => | ||
| Promise.resolve({ | ||
| data: { id: "offer-1", seller_id: "seller-1" }, | ||
| error: null, | ||
| }), | ||
| }), | ||
| }), | ||
| }; | ||
| } | ||
|
|
||
| if (table === "affiliate_applications") { | ||
| return { | ||
| update: (payload: Record<string, unknown>) => { | ||
| updatePayload = payload; | ||
| return { | ||
| eq: () => ({ | ||
| eq: () => ({ | ||
| select: () => ({ | ||
| single: () => | ||
| Promise.resolve({ | ||
| data: updatedApplication, | ||
| error: null, | ||
| }), | ||
| }), | ||
| }), | ||
| }), | ||
| }; | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| if (table === "notifications") { | ||
| return { | ||
| insert: (payload: Record<string, unknown>) => { | ||
| notificationPayload = payload; | ||
| return Promise.resolve({ data: null, error: null }); | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| throw new Error(`Unexpected table: ${table}`); | ||
| }); | ||
|
|
||
| const res = await PATCH( | ||
| makePatchRequest( | ||
| JSON.stringify({ application_id: "app-1", action: "approve" }) | ||
| ), | ||
| makeParams("offer-1") | ||
| ); | ||
| const body = await res.json(); | ||
|
|
||
| expect(res.status).toBe(200); | ||
| expect(body.application).toEqual(updatedApplication); | ||
| expect(updatePayload).toMatchObject({ status: "approved" }); | ||
| expect(updatePayload?.approved_at).toEqual(expect.any(String)); | ||
| expect(notificationPayload).toMatchObject({ | ||
| user_id: "affiliate-1", | ||
| type: "affiliate_approved", | ||
| data: { offer_id: "offer-1", application_id: "app-1" }, | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Reject path has no test coverage
The happy-path test only exercises action: "approve". The reject branch follows a different code path: status is "rejected", approved_at is intentionally omitted from updateData, and the notification type becomes "affiliate_rejected". None of these are covered, so a regression in the rejection flow (e.g. accidentally setting approved_at on rejections, or sending the wrong notification type) would go undetected.
Summary
400 { "error": "Invalid request body" }when the affiliate application status endpoint receives malformed or non-object JSONFixes #163
uGig bounty
Submitted for the active uGig affiliate-program testing task: https://ugig.net/gigs/4741218f-a723-46bb-82cb-6516120331ae
SOL payout address:
27sdMYXofqoM9qR13bZhccRNYeEgYn5EoHXTSJn4QWKPTests
pnpm test:run 'src/app/api/affiliates/offers/[id]/applications/route.test.ts'pnpm exec eslint 'src/app/api/affiliates/offers/[id]/applications/route.ts' 'src/app/api/affiliates/offers/[id]/applications/route.test.ts'pnpm type-checkgit diff --check -- 'src/app/api/affiliates/offers/[id]/applications/route.ts' 'src/app/api/affiliates/offers/[id]/applications/route.test.ts'Payment fallback: PayPal cultofrozen@gmail.com