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
110 changes: 110 additions & 0 deletions src/app/api/zaps/history/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { describe, expect, it, vi, beforeEach } from "vitest";

const mockGetAuthContext = vi.fn();
const mockCreateServiceClient = vi.fn();

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

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

import { GET } from "./route";

function makeRequest(query = "") {
return { url: `http://localhost/api/zaps/history${query}` } as any;
}

function makeAdmin(
zaps: any[] | null = [{ id: "zap-1", sender_id: "sender-1", recipient_id: "user-1" }],
count = zaps?.length ?? 0
) {
const range = vi.fn().mockResolvedValue({
data: zaps,
count,
});
const order = vi.fn().mockReturnValue({ range });
const eq = vi.fn().mockReturnValue({ order });
const selectZaps = vi.fn().mockReturnValue({ eq });

const inProfiles = vi.fn().mockResolvedValue({
data: [
{
id: "sender-1",
username: "sender",
full_name: "Sender User",
avatar_url: "https://example.com/avatar.png",
},
],
});
const selectProfiles = vi.fn().mockReturnValue({ in: inProfiles });

const from = vi.fn((table: string) => {
if (table === "profiles") return { select: selectProfiles };
return { select: selectZaps };
});

return { admin: { from }, range, inProfiles, from };
}

describe("GET /api/zaps/history", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("requires authentication", async () => {
mockGetAuthContext.mockResolvedValue(null);

const res = await GET(makeRequest());

expect(res.status).toBe(401);
});

it("clamps invalid pagination values before querying", async () => {
const { admin, range } = makeAdmin();
mockCreateServiceClient.mockReturnValue(admin);
mockGetAuthContext.mockResolvedValue({ user: { id: "user-1" } });

const res = await GET(makeRequest("?limit=0&offset=-5"));

expect(res.status).toBe(200);
expect(range).toHaveBeenCalledWith(0, 49);
});

it("caps large limits at 100", async () => {
const { admin, range } = makeAdmin();
mockCreateServiceClient.mockReturnValue(admin);
mockGetAuthContext.mockResolvedValue({ user: { id: "user-1" } });

const res = await GET(makeRequest("?limit=500&offset=10"));

expect(res.status).toBe(200);
expect(range).toHaveBeenCalledWith(10, 109);
});

it("uses valid pagination values as provided", async () => {
const { admin, range } = makeAdmin();
mockCreateServiceClient.mockReturnValue(admin);
mockGetAuthContext.mockResolvedValue({ user: { id: "user-1" } });

const res = await GET(makeRequest("?limit=12&offset=24"));

expect(res.status).toBe(200);
expect(range).toHaveBeenCalledWith(24, 35);
});

it("does not query profiles when there are no zaps", async () => {
const { admin, inProfiles } = makeAdmin([], 3);
mockCreateServiceClient.mockReturnValue(admin);
mockGetAuthContext.mockResolvedValue({ user: { id: "user-1" } });

const res = await GET(makeRequest());
const body = await res.json();

expect(res.status).toBe(200);
expect(body).toEqual({ zaps: [], total: 3 });
expect(inProfiles).not.toHaveBeenCalled();
});
});
18 changes: 14 additions & 4 deletions src/app/api/zaps/history/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ import { NextRequest, NextResponse } from "next/server";
import { getAuthContext } from "@/lib/auth/get-user";
import { createServiceClient } from "@/lib/supabase/service";

function parsePositiveInt(value: string | null, fallback: number): number {
const parsed = Number.parseInt(value || "", 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}

function parseNonNegativeInt(value: string | null, fallback: number): number {
const parsed = Number.parseInt(value || "", 10);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
}

/**
* GET /api/zaps/history?direction=sent|received&limit=50&offset=0
*/
Expand All @@ -14,8 +24,8 @@ export async function GET(request: NextRequest) {

const url = new URL(request.url);
const direction = url.searchParams.get("direction") || "received";
const limit = Math.min(parseInt(url.searchParams.get("limit") || "50"), 100);
const offset = parseInt(url.searchParams.get("offset") || "0");
const limit = Math.min(parsePositiveInt(url.searchParams.get("limit"), 50), 100);
const offset = parseNonNegativeInt(url.searchParams.get("offset"), 0);

const admin = createServiceClient();
const userId = auth.user.id;
Expand All @@ -30,8 +40,8 @@ export async function GET(request: NextRequest) {
.order("created_at", { ascending: false })
.range(offset, offset + limit - 1) as any;

if (!zaps) {
return NextResponse.json({ zaps: [], total: 0 });
if (!zaps || zaps.length === 0) {
return NextResponse.json({ zaps: [], total: count || 0 });
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

// Fetch profiles for the other party
Expand Down
Loading