Skip to content
Closed
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
98 changes: 98 additions & 0 deletions src/app/api/affiliates/offers/[id]/apply/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { NextRequest } from "next/server";
import { POST } from "./route";

const mockGetAuthContext = vi.fn();
vi.mock("@/lib/auth/get-user", () => ({
getAuthContext: (...args: unknown[]) => mockGetAuthContext(...args),
}));

const mockCheckRateLimit = vi.fn();
vi.mock("@/lib/rate-limit", () => ({
checkRateLimit: (...args: unknown[]) => mockCheckRateLimit(...args),
getRateLimitIdentifier: () => "rate-key",
rateLimitExceeded: () => new Response("rate limited", { status: 429 }),
}));

vi.mock("@/lib/affiliates/tracking", () => ({
generateTrackingCode: () => "alice-test123",
}));

const mockFrom = vi.fn();
vi.mock("@/lib/supabase/service", () => ({
createServiceClient: () => ({
from: (...args: unknown[]) => mockFrom(...args),
}),
}));

function makeRequest() {
return new NextRequest("http://localhost/api/affiliates/offers/offer-1/apply", {
method: "POST",
body: "{}",
});
}

function makeParams(id = "offer-1") {
return { params: Promise.resolve({ id }) };
}

function makeSingleResponse(data: unknown) {
return {
select: () => ({
eq: () => ({
eq: () => ({
single: () => Promise.resolve({ data, error: null }),
}),
single: () => Promise.resolve({ data, error: null }),
}),
}),
};
}

describe("POST /api/affiliates/offers/[id]/apply", () => {
beforeEach(() => {
vi.clearAllMocks();
mockCheckRateLimit.mockReturnValue({ allowed: true });
mockGetAuthContext.mockResolvedValue({
user: { id: "affiliate-1", authMethod: "session" },
});
delete process.env.NEXT_PUBLIC_APP_URL;
});

it("returns the existing tracking URL when an approved affiliate reapplies", async () => {
mockFrom.mockImplementation((table: string) => {
if (table === "affiliate_offers") {
return makeSingleResponse({
id: "offer-1",
seller_id: "seller-1",
status: "active",
slug: "test-offer",
});
}

if (table === "affiliate_applications") {
return makeSingleResponse({
id: "application-1",
status: "approved",
tracking_code: "alice-test123",
});
}

return {};
});

const res = await POST(makeRequest(), makeParams());

expect(res.status).toBe(409);
await expect(res.json()).resolves.toEqual({
error: "Already approved",
application: {
id: "application-1",
status: "approved",
tracking_code: "alice-test123",
},
tracking_code: "alice-test123",
tracking_url: "https://ugig.net/ref/alice-test123",
});
});
Comment on lines +62 to +97

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 test for null tracking_code path

The new test only covers the case where existing.tracking_code is set. The route also handles existing.tracking_code === null (e.g. a pending application created before tracking codes were introduced) by returning tracking_url: null. A test asserting that shape would guard against a future regression where the ? …: null branch is accidentally removed or changed.

});
12 changes: 8 additions & 4 deletions src/app/api/affiliates/offers/[id]/apply/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { NextRequest, NextResponse } from "next/server";
import { getAuthContext } from "@/lib/auth/get-user";
import { createServiceClient } from "@/lib/supabase/service";
import { checkRateLimit, rateLimitExceeded, getRateLimitIdentifier } from "@/lib/rate-limit";
import { generateTrackingCode } from "@/lib/affiliates/tracking";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnySupabase = any;
import { generateTrackingCode } from "@/lib/affiliates/tracking";

function trackingUrl(trackingCode: string): string {
return `${process.env.NEXT_PUBLIC_APP_URL || "https://ugig.net"}/ref/${trackingCode}`;
}

/**
* POST /api/affiliates/offers/[id]/apply - Apply to become an affiliate for an offer
Expand Down Expand Up @@ -46,7 +48,7 @@ export async function POST(
// Check if already applied
const { data: existing } = await (admin as AnySupabase)
.from("affiliate_applications")
.select("id, status")
.select("id, status, tracking_code")
.eq("offer_id", id)
.eq("affiliate_id", auth.user.id)
.single();
Expand All @@ -55,6 +57,8 @@ export async function POST(
return NextResponse.json({
error: `Already ${existing.status}`,
application: existing,
tracking_code: existing.tracking_code,
tracking_url: existing.tracking_code ? trackingUrl(existing.tracking_code) : null,
}, { status: 409 });
}

Expand Down Expand Up @@ -127,7 +131,7 @@ export async function POST(
return NextResponse.json({
application,
tracking_code: trackingCode,
tracking_url: `${process.env.NEXT_PUBLIC_APP_URL || "https://ugig.net"}/ref/${trackingCode}`,
tracking_url: trackingUrl(trackingCode),
}, { status: 201 });
} catch {
return NextResponse.json({ error: "An unexpected error occurred" }, { status: 500 });
Expand Down
Loading