Skip to content
Merged
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
50 changes: 50 additions & 0 deletions src/app/api/referrals/route.invalid-json.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { NextRequest } from "next/server";
import { POST } from "./route";

const mocks = vi.hoisted(() => ({
mockGetAuthContext: vi.fn(),
mockCreateServiceClient: vi.fn(),
mockSendEmail: vi.fn(),
}));

vi.mock("@/lib/auth/get-user", () => ({
getAuthContext: mocks.mockGetAuthContext,
}));

vi.mock("@/lib/supabase/service", () => ({
createServiceClient: mocks.mockCreateServiceClient,
}));

vi.mock("@/lib/email", () => ({
referralInviteEmail: vi.fn(),
sendEmail: mocks.mockSendEmail,
}));

function makeRawPostRequest(body: BodyInit) {
return new NextRequest("http://localhost/api/referrals", {
method: "POST",
body,
headers: { "Content-Type": "application/json" },
});
}

describe("POST /api/referrals invalid JSON handling", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.mockGetAuthContext.mockResolvedValue({
user: { id: "user1" },
supabase: { from: vi.fn() },
});
});

it("returns 400 for malformed JSON before referral side effects", async () => {
const res = await POST(makeRawPostRequest("{not valid json"));
const body = await res.json();

expect(res.status).toBe(400);
expect(body.error).toBe("Invalid JSON body");
expect(mocks.mockCreateServiceClient).not.toHaveBeenCalled();
expect(mocks.mockSendEmail).not.toHaveBeenCalled();
});
Comment on lines +32 to +49

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 Test covers only the parse-failure path

readJsonObject has two distinct rejection branches — a JSON.parse throw (malformed input) and a type check (valid JSON that isn't a plain object, e.g. arrays, null, or primitives). The test exercises only the first branch. Sending "[]" or "null" as the body would exercise the second branch and confirm both are mapped to 400 correctly.

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!

});
20 changes: 19 additions & 1 deletion src/app/api/referrals/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ type AnySupabase = any;
const MAX_EMAIL_ENTRIES_PER_REQUEST = 200;
const MAX_INVITES_PER_REQUEST = 20;

async function readJsonObject(request: NextRequest) {
try {
const body = await request.json();
if (!body || typeof body !== "object" || Array.isArray(body)) {
return null;
}
return body as Record<string, unknown>;
} catch {
return null;
}
}
Comment on lines +10 to +20

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 The error message "Invalid JSON body" is returned for two distinct cases: a parse failure (truly malformed JSON) and a successful parse that yields a non-object (e.g. a JSON array or null). Clients who send ["email@example.com"] will get "Invalid JSON body" even though their JSON is perfectly valid — which can make debugging confusing. Distinguishing the two cases gives callers a clearer signal.

Suggested change
async function readJsonObject(request: NextRequest) {
try {
const body = await request.json();
if (!body || typeof body !== "object" || Array.isArray(body)) {
return null;
}
return body as Record<string, unknown>;
} catch {
return null;
}
}
type JsonObjectResult =
| { ok: true; body: Record<string, unknown> }
| { ok: false; reason: "malformed" | "not-object" };
async function readJsonObject(request: NextRequest): Promise<JsonObjectResult> {
let parsed: unknown;
try {
parsed = await request.json();
} catch {
return { ok: false, reason: "malformed" };
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return { ok: false, reason: "not-object" };
}
return { ok: true, body: parsed as Record<string, unknown> };
}


// GET /api/referrals - List my referrals
export async function GET(request: NextRequest) {
try {
Expand Down Expand Up @@ -54,7 +66,13 @@ export async function POST(request: NextRequest) {
}
const { user, supabase } = auth;

const body = await request.json();
const body = await readJsonObject(request);
if (!body) {
return NextResponse.json(
{ error: "Invalid JSON body" },
{ status: 400 }
);
}
const { emails } = body;

if (!emails || !Array.isArray(emails) || emails.length === 0) {
Expand Down
Loading