feat: correct error codes in responses for /social/og/image (#22015)
* fix error codes in og/image route * test * simplify
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { describe, expect, test, vi, beforeEach } from "vitest";
|
||||
|
||||
import { GET } from "../route";
|
||||
|
||||
vi.mock("next/og", () => ({
|
||||
ImageResponse: vi.fn().mockImplementation(() => ({
|
||||
body: new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new Uint8Array([1, 2, 3, 4]));
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/OgImages", () => ({
|
||||
Meeting: vi.fn(() => null),
|
||||
App: vi.fn(() => null),
|
||||
Generic: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/constants", () => ({
|
||||
WEBAPP_URL: "http://localhost:3000",
|
||||
}));
|
||||
|
||||
global.fetch = vi.fn();
|
||||
|
||||
function createNextRequest(url: string): NextRequest {
|
||||
const request = new Request(url, { method: "GET" });
|
||||
return new NextRequest(request);
|
||||
}
|
||||
|
||||
describe("GET /api/social/og/image", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValue({
|
||||
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
|
||||
} as any);
|
||||
});
|
||||
|
||||
describe("Validation errors (400 Bad Request)", () => {
|
||||
test("meeting type: returns 400 when required parameters are missing", async () => {
|
||||
const request = createNextRequest("http://example.com/api/social/og/image?type=meeting");
|
||||
const response = await GET(request);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.headers.get("Content-Type")).toBe("application/json");
|
||||
|
||||
const errorData = await response.json();
|
||||
expect(errorData.error).toBe("Invalid parameters for meeting image");
|
||||
expect(errorData.message).toBe(
|
||||
"Required parameters: title, meetingProfileName. Optional: names, usernames, meetingImage"
|
||||
);
|
||||
});
|
||||
|
||||
test("app type: returns 400 when required parameters are missing", async () => {
|
||||
const request = createNextRequest("http://example.com/api/social/og/image?type=app");
|
||||
const response = await GET(request);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
const errorData = await response.json();
|
||||
expect(errorData.error).toBe("Invalid parameters for app image");
|
||||
expect(errorData.message).toBe("Required parameters: name, description, slug");
|
||||
});
|
||||
|
||||
test("generic type: returns 400 when required parameters are missing", async () => {
|
||||
const request = createNextRequest("http://example.com/api/social/og/image?type=generic");
|
||||
const response = await GET(request);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
const errorData = await response.json();
|
||||
expect(errorData.error).toBe("Invalid parameters for generic image");
|
||||
expect(errorData.message).toBe("Required parameters: title, description");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Not found errors (404 Not Found)", () => {
|
||||
test("returns 404 when no type parameter is provided", async () => {
|
||||
const request = createNextRequest("http://example.com/api/social/og/image");
|
||||
const response = await GET(request);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(await response.text()).toBe("What you're looking for is not here..");
|
||||
});
|
||||
|
||||
test("returns 404 when invalid type parameter is provided", async () => {
|
||||
const request = createNextRequest("http://example.com/api/social/og/image?type=invalid");
|
||||
const response = await GET(request);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(await response.text()).toBe("What you're looking for is not here..");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Server errors (500 Internal Server Error)", () => {
|
||||
test("returns 500 when font loading fails", async () => {
|
||||
vi.mocked(global.fetch).mockRejectedValue(new Error("Font loading failed"));
|
||||
|
||||
const request = createNextRequest(
|
||||
"http://example.com/api/social/og/image?type=meeting&title=Test&meetingProfileName=John"
|
||||
);
|
||||
const response = await GET(request);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(await response.text()).toBe("Internal server error");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import type { NextRequest } from "next/server";
|
||||
import type { SatoriOptions } from "satori";
|
||||
import { z } from "zod";
|
||||
import { z, ZodError } from "zod";
|
||||
|
||||
import { Meeting, App, Generic } from "@calcom/lib/OgImages";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
@@ -34,75 +34,128 @@ async function handler(req: NextRequest) {
|
||||
const { searchParams } = req.nextUrl;
|
||||
const imageType = searchParams.get("type");
|
||||
|
||||
const [calFontData, interFontData, interFontMediumData] = await Promise.all([
|
||||
fetch(new URL("/fonts/cal.ttf", WEBAPP_URL)).then((res) => res.arrayBuffer()),
|
||||
fetch(new URL("/fonts/Inter-Regular.ttf", WEBAPP_URL)).then((res) => res.arrayBuffer()),
|
||||
fetch(new URL("/fonts/Inter-Medium.ttf", WEBAPP_URL)).then((res) => res.arrayBuffer()),
|
||||
]);
|
||||
const ogConfig = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{ name: "inter", data: interFontData, weight: 400 },
|
||||
{ name: "inter", data: interFontMediumData, weight: 500 },
|
||||
{ name: "cal", data: calFontData, weight: 400 },
|
||||
{ name: "cal", data: calFontData, weight: 600 },
|
||||
] as SatoriOptions["fonts"],
|
||||
};
|
||||
try {
|
||||
const [calFontData, interFontData, interFontMediumData] = await Promise.all([
|
||||
fetch(new URL("/fonts/cal.ttf", WEBAPP_URL)).then((res) => res.arrayBuffer()),
|
||||
fetch(new URL("/fonts/Inter-Regular.ttf", WEBAPP_URL)).then((res) => res.arrayBuffer()),
|
||||
fetch(new URL("/fonts/Inter-Medium.ttf", WEBAPP_URL)).then((res) => res.arrayBuffer()),
|
||||
]);
|
||||
const ogConfig = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{ name: "inter", data: interFontData, weight: 400 },
|
||||
{ name: "inter", data: interFontMediumData, weight: 500 },
|
||||
{ name: "cal", data: calFontData, weight: 400 },
|
||||
{ name: "cal", data: calFontData, weight: 600 },
|
||||
] as SatoriOptions["fonts"],
|
||||
};
|
||||
|
||||
switch (imageType) {
|
||||
case "meeting": {
|
||||
const { names, usernames, title, meetingProfileName, meetingImage } = meetingSchema.parse({
|
||||
names: searchParams.getAll("names"),
|
||||
usernames: searchParams.getAll("usernames"),
|
||||
title: searchParams.get("title"),
|
||||
meetingProfileName: searchParams.get("meetingProfileName"),
|
||||
meetingImage: searchParams.get("meetingImage"),
|
||||
imageType,
|
||||
});
|
||||
switch (imageType) {
|
||||
case "meeting": {
|
||||
try {
|
||||
const { names, usernames, title, meetingProfileName, meetingImage } = meetingSchema.parse({
|
||||
names: searchParams.getAll("names"),
|
||||
usernames: searchParams.getAll("usernames"),
|
||||
title: searchParams.get("title"),
|
||||
meetingProfileName: searchParams.get("meetingProfileName"),
|
||||
meetingImage: searchParams.get("meetingImage"),
|
||||
imageType,
|
||||
});
|
||||
|
||||
const img = new ImageResponse(
|
||||
(
|
||||
<Meeting
|
||||
title={title}
|
||||
profile={{ name: meetingProfileName, image: meetingImage }}
|
||||
users={names.map((name, index) => ({ name, username: usernames[index] }))}
|
||||
/>
|
||||
),
|
||||
ogConfig
|
||||
);
|
||||
const img = new ImageResponse(
|
||||
(
|
||||
<Meeting
|
||||
title={title}
|
||||
profile={{ name: meetingProfileName, image: meetingImage }}
|
||||
users={names.map((name, index) => ({ name, username: usernames[index] }))}
|
||||
/>
|
||||
),
|
||||
ogConfig
|
||||
);
|
||||
|
||||
return new Response(img.body, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "image/png", "cache-control": "max-age=0" },
|
||||
});
|
||||
return new Response(img.body, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "image/png", "cache-control": "max-age=0" },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Invalid parameters for meeting image",
|
||||
message:
|
||||
"Required parameters: title, meetingProfileName. Optional: names, usernames, meetingImage",
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
case "app": {
|
||||
try {
|
||||
const { name, description, slug } = appSchema.parse({
|
||||
name: searchParams.get("name"),
|
||||
description: searchParams.get("description"),
|
||||
slug: searchParams.get("slug"),
|
||||
imageType,
|
||||
});
|
||||
const img = new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig);
|
||||
|
||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Invalid parameters for app image",
|
||||
message: "Required parameters: name, description, slug",
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
case "generic": {
|
||||
try {
|
||||
const { title, description } = genericSchema.parse({
|
||||
title: searchParams.get("title"),
|
||||
description: searchParams.get("description"),
|
||||
imageType,
|
||||
});
|
||||
|
||||
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig);
|
||||
|
||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Invalid parameters for generic image",
|
||||
message: "Required parameters: title, description",
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return new Response("What you're looking for is not here..", { status: 404 });
|
||||
}
|
||||
case "app": {
|
||||
const { name, description, slug } = appSchema.parse({
|
||||
name: searchParams.get("name"),
|
||||
description: searchParams.get("description"),
|
||||
slug: searchParams.get("slug"),
|
||||
imageType,
|
||||
});
|
||||
const img = new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig);
|
||||
|
||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||
}
|
||||
|
||||
case "generic": {
|
||||
const { title, description } = genericSchema.parse({
|
||||
title: searchParams.get("title"),
|
||||
description: searchParams.get("description"),
|
||||
imageType,
|
||||
});
|
||||
|
||||
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig);
|
||||
|
||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||
}
|
||||
|
||||
default:
|
||||
return new Response("What you're looking for is not here..", { status: 404 });
|
||||
} catch (error) {
|
||||
return new Response("Internal server error", { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user