Skip to content

Handle malformed affiliate application bodies - #164

Merged
ralyodio merged 1 commit into
profullstack:masterfrom
absalonCRC:fix-affiliate-applications-malformed-json
May 23, 2026
Merged

Handle malformed affiliate application bodies#164
ralyodio merged 1 commit into
profullstack:masterfrom
absalonCRC:fix-affiliate-applications-malformed-json

Conversation

@absalonCRC

@absalonCRC absalonCRC commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • return 400 { "error": "Invalid request body" } when the affiliate application status endpoint receives malformed or non-object JSON
  • keep invalid field validation separate from parse failures
  • add route regression coverage for malformed JSON, non-object JSON, and the valid approval/notification path

Fixes #163

uGig bounty

Submitted for the active uGig affiliate-program testing task: https://ugig.net/gigs/4741218f-a723-46bb-82cb-6516120331ae

SOL payout address: 27sdMYXofqoM9qR13bZhccRNYeEgYn5EoHXTSJn4QWKP

Tests

  • 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-check
  • git 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

@greptile-apps

greptile-apps Bot commented May 23, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a readJsonObject helper to the affiliate applications PATCH endpoint that intercepts malformed or non-object JSON before any database work is done, returning a 400 { "error": "Invalid request body" } instead of an unhandled exception. It also tightens field extraction with explicit type-narrowing (typeof body.x === "string") and ships a new test file covering both malformed-body paths and the approval happy-path.

  • route.ts: Introduces readJsonObject to parse and validate the request body, returning null for malformed JSON, null, or arrays; the PATCH handler now short-circuits with 400 before touching Supabase.
  • route.test.ts: New test file with three scenarios — malformed JSON, non-object JSON (array), and a full approve-and-notify happy-path. The reject action path is not yet covered.

Confidence Score: 4/5

Safe 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

Filename Overview
src/app/api/affiliates/offers/[id]/applications/route.ts Adds readJsonObject helper to parse and validate the PATCH body; returns 400 for malformed or non-object JSON before touching the DB. Also removes an eslint-disable comment from the AnySupabase type alias.
src/app/api/affiliates/offers/[id]/applications/route.test.ts New test file covering malformed JSON, non-object JSON (array), and the approve happy-path. The reject path has no test coverage.

Sequence Diagram

sequenceDiagram
    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 }
Loading

Reviews (1): Last reviewed commit: "Handle malformed affiliate application b..." | Re-trigger Greptile

Comment on lines +55 to +135
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" },
});
});
});

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 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.

@ralyodio
ralyodio merged commit 96829e4 into profullstack:master May 23, 2026
4 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.

bug: affiliate application status API returns 500 on malformed JSON

2 participants