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
11 changes: 6 additions & 5 deletions src/app/api/card/[username]/route.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { NextRequest } from "next/server";
import { describe, expect, it, vi } from "vitest";

vi.mock("@/lib/cardDataFetcher", () => ({
Expand All @@ -23,7 +24,7 @@ describe("GET /api/card/[username] cache headers", () => {
});

const { GET } = await import("./route");
const req = new Request("http://localhost/api/card/alice");
const req = new NextRequest("http://localhost/api/card/alice");
const response = await GET(req, { params: Promise.resolve({ username: "alice" }) });

expect(response.headers.get("Cache-Control")).toBe("public, s-maxage=1800, stale-while-revalidate=3600");
Expand All @@ -34,7 +35,7 @@ describe("GET /api/card/[username] cache headers", () => {
vi.mocked(fetchCardData).mockResolvedValueOnce(null);

const { GET } = await import("./route");
const req = new Request("http://localhost/api/card/ghost");
const req = new NextRequest("http://localhost/api/card/ghost");
const response = await GET(req, { params: Promise.resolve({ username: "ghost" }) });

expect(response.status).toBe(404);
Expand All @@ -46,7 +47,7 @@ describe("GET /api/card/[username] cache headers", () => {
vi.mocked(fetchCardData).mockRejectedValueOnce(new Error("API Error"));

const { GET } = await import("./route");
const req = new Request("http://localhost/api/card/erroruser");
const req = new NextRequest("http://localhost/api/card/erroruser");
const response = await GET(req, { params: Promise.resolve({ username: "erroruser" }) });

expect(response.status).toBe(503);
Expand All @@ -66,7 +67,7 @@ describe("GET /api/card/[username] error responses", () => {
}

const { GET } = await import("./route");
const req = new Request(`http://localhost/api/card/${username}`);
const req = new NextRequest(`http://localhost/api/card/${username}`);
await GET(req, { params: Promise.resolve({ username }) });

expect(renderErrorCardResponse).toHaveBeenCalledWith(expect.objectContaining({
Expand All @@ -92,7 +93,7 @@ describe("GET /api/card/[username] rate limiting", () => {
const { fetchCardData } = await import("@/lib/cardDataFetcher");
const { renderErrorCardResponse } = await import("@/lib/cardRenderer");

const req1 = new Request("http://localhost/api/card/testuser", {
const req1 = new NextRequest("http://localhost/api/card/testuser", {
headers: {
"x-forwarded-for": "127.0.0.1",
},
Expand Down
7 changes: 5 additions & 2 deletions src/app/api/card/[username]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ const rateLimiter = new RateLimiter(50, 60 * 1000); // 50 requests per minute
const SUCCESS_CACHE = "public, s-maxage=1800, stale-while-revalidate=3600";
const ERROR_CACHE = "public, s-maxage=60, stale-while-revalidate=120";

import { NextRequest } from "next/server";

export async function GET(
request: Request,
request: NextRequest,
{ params }: { params: Promise<{ username: string }> }
): Promise<Response> {
const { username } = await params;
Expand All @@ -19,7 +21,8 @@ export async function GET(
const allowedOrigin = process.env.APP_URL || "http://localhost:3000";
const fontUrl = `${allowedOrigin}/fonts/NotoSans-Regular.ttf`;

const ip = request.headers.get("x-forwarded-for") ?? "unknown";
const forwarded = request.headers.get("x-forwarded-for");
const ip = forwarded ? forwarded.split(",")[0]?.trim() ?? "unknown" : "unknown";
const rateLimitResult = rateLimiter.check(ip);

if (!rateLimitResult.success) {
Expand Down
8 changes: 4 additions & 4 deletions src/app/api/og/[username]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ describe("OG Image Route", () => {
});

it("should generate image for valid username", async () => {
const mockFetch = vi.spyOn(global, "fetch").mockResolvedValue(
new Response(JSON.stringify({ name: "Valid User" }), { status: 200 })
const mockFetch = vi.spyOn(global, "fetch").mockImplementation(() =>
Promise.resolve(new Response(JSON.stringify({ name: "Valid User" }), { status: 200 }))
);

const req = new NextRequest("http://localhost/api/og/validuser");
Expand All @@ -47,8 +47,8 @@ describe("OG Image Route", () => {
});

it("should return 429 and Retry-After header when rate limit is exceeded", async () => {
const mockFetch = vi.spyOn(global, "fetch").mockResolvedValue(
new Response(JSON.stringify({ name: "Valid User" }), { status: 200 })
const mockFetch = vi.spyOn(global, "fetch").mockImplementation(() =>
Promise.resolve(new Response(JSON.stringify({ name: "Valid User" }), { status: 200 }))
);

// Generate more than 50 requests to hit the rate limit (limit is 50 per minute)
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/og/[username]/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
const { username } = await params;

const forwarded = request.headers.get("x-forwarded-for");
const ip = forwarded ? forwarded.split(",").at(-1)?.trim() ?? "unknown" : "unknown";
const ip = forwarded ? forwarded.split(",")[0]?.trim() ?? "unknown" : "unknown";
const rateLimitResult = rateLimiter.check(ip);

if (!rateLimitResult.success) {
Expand Down Expand Up @@ -84,7 +84,7 @@
}}
>
{avatarUrl && (
<img

Check warning on line 87 in src/app/api/og/[username]/route.tsx

View workflow job for this annotation

GitHub Actions / Lint

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={sanitizeUrl(avatarUrl)}
alt=""
width={120}
Expand Down
Loading