Browse Source

test: add tests for open-file route, getExtensionFromUrl, and fal.ai 3D responses

- 14 tests for /api/open-file: localhost guard, input validation, path
  restriction, file validation, platform commands
- 13 tests for getExtensionFromUrl: 3D extensions, media extensions,
  query strings, fragments, edge cases, zip exclusion
- 3 tests for fal.ai 3D response formats: model_glb.url, model_urls.glb.url,
  enhanced error log with Result keys

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
e2b949d8b0
  1. 160
      src/app/api/generate/__tests__/route.test.ts
  2. 276
      src/app/api/open-file/__tests__/route.test.ts
  3. 56
      src/app/api/save-generation/__tests__/route.test.ts

160
src/app/api/generate/__tests__/route.test.ts

@ -2637,5 +2637,165 @@ describe("/api/generate route", () => {
expect(data.success).toBe(false);
expect(data.error).toContain("No media URL in response");
});
it("should handle model_glb.url response format (Hunyuan 3D)", async () => {
// Schema fetch
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ models: [] }),
});
// Queue flow: submit → poll → result (model_glb format) → media fetch
// Queue submit
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ request_id: "test-req-id" }),
});
// Status poll → COMPLETED
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ status: "COMPLETED" }),
});
// Result fetch
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ model_glb: { url: "https://fal.media/hunyuan3d-output.glb" } }),
});
// Media fetch (needed to reach is3DModel check)
mockFetch.mockResolvedValueOnce({
ok: true,
headers: new Headers({ "content-type": "model/gltf-binary" }),
arrayBuffer: () => Promise.resolve(new ArrayBuffer(4096)),
});
const request = createMockPostRequest(
{
prompt: "A 3D model of a chair",
mediaType: "3d",
selectedModel: {
provider: "fal",
modelId: "fal-ai/hunyuan3d/mini",
displayName: "Hunyuan 3D Mini",
},
},
{ "X-Fal-API-Key": "test-fal-key" }
);
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(data.contentType).toBe("3d");
expect(data.model3dUrl).toBe("https://fal.media/hunyuan3d-output.glb");
});
it("should handle model_urls.glb.url response format (Hunyuan 3D variant)", async () => {
// Schema fetch
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ models: [] }),
});
// Queue flow: submit → poll → result (model_urls.glb format) → media fetch
// Queue submit
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ request_id: "test-req-id" }),
});
// Status poll → COMPLETED
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ status: "COMPLETED" }),
});
// Result fetch
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ model_urls: { glb: { url: "https://fal.media/hunyuan3d-v2-output.glb" } } }),
});
// Media fetch (needed to reach is3DModel check)
mockFetch.mockResolvedValueOnce({
ok: true,
headers: new Headers({ "content-type": "model/gltf-binary" }),
arrayBuffer: () => Promise.resolve(new ArrayBuffer(4096)),
});
const request = createMockPostRequest(
{
prompt: "A 3D model of a tree",
mediaType: "3d",
selectedModel: {
provider: "fal",
modelId: "fal-ai/hunyuan3d/standard",
displayName: "Hunyuan 3D Standard",
},
},
{ "X-Fal-API-Key": "test-fal-key" }
);
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(data.contentType).toBe("3d");
expect(data.model3dUrl).toBe("https://fal.media/hunyuan3d-v2-output.glb");
});
it("should include Result keys in error log when no media URL found", async () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
// Schema fetch
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ models: [] }),
});
// Queue submit
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ request_id: "test-req-id" }),
});
// Status poll → COMPLETED
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ status: "COMPLETED" }),
});
// Result fetch returns unrecognized format
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ unknown_field: "data", another: 123 }),
});
const request = createMockPostRequest(
{
prompt: "Test prompt",
selectedModel: {
provider: "fal",
modelId: "fal-ai/some-model",
displayName: "Some Model",
},
},
{ "X-Fal-API-Key": "test-fal-key" }
);
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(500);
expect(data.success).toBe(false);
expect(data.error).toContain("No media URL in response");
// Verify the enhanced error log includes Result keys
const errorCalls = consoleSpy.mock.calls;
const resultKeysLog = errorCalls.find(
(call) => typeof call[0] === "string" && call[0].includes("Result keys:")
);
expect(resultKeysLog).toBeDefined();
expect(resultKeysLog![0]).toContain("unknown_field");
expect(resultKeysLog![0]).toContain("another");
consoleSpy.mockRestore();
});
});
});

276
src/app/api/open-file/__tests__/route.test.ts

@ -0,0 +1,276 @@
// @vitest-environment node
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NextRequest } from "next/server";
// Use vi.hoisted so mock fns are available during vi.mock() hoisting
const { mockExecFileAsync, mockStat, mockPlatform, mockHomedir } = vi.hoisted(() => ({
mockExecFileAsync: vi.fn(),
mockStat: vi.fn(),
mockPlatform: vi.fn(),
mockHomedir: vi.fn(),
}));
vi.mock(import("child_process"), async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
execFile: vi.fn(),
};
});
vi.mock(import("util"), async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
promisify: () => mockExecFileAsync,
};
});
vi.mock(import("fs/promises"), async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
stat: (...args: unknown[]) => mockStat(...args),
};
});
vi.mock(import("os"), async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
default: {
...actual,
platform: () => mockPlatform(),
homedir: () => mockHomedir(),
},
platform: () => mockPlatform(),
homedir: () => mockHomedir(),
};
});
import { POST } from "../route";
// Helper to create mock NextRequest
function createMockRequest(
body: unknown,
headers?: Record<string, string>
): NextRequest {
return {
json: vi.fn().mockResolvedValue(body),
headers: new Headers({ host: "localhost:3000", ...headers }),
} as unknown as NextRequest;
}
describe("/api/open-file route", () => {
beforeEach(() => {
vi.clearAllMocks();
mockPlatform.mockReturnValue("darwin");
mockHomedir.mockReturnValue("/Users/testuser");
});
describe("localhost guard", () => {
it("should return 403 for non-localhost x-forwarded-for", async () => {
const request = createMockRequest(
{ filePath: "/Users/testuser/file.glb" },
{ "x-forwarded-for": "203.0.113.50", host: "localhost:3000" }
);
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(403);
expect(data.error).toBe("Forbidden: localhost only");
});
it("should return 403 for non-localhost host header", async () => {
const request = createMockRequest(
{ filePath: "/Users/testuser/file.glb" },
{ host: "example.com" }
);
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(403);
expect(data.error).toBe("Forbidden: localhost only");
});
it("should allow requests from 127.0.0.1 x-forwarded-for", async () => {
mockStat.mockResolvedValue({ isFile: () => true });
mockExecFileAsync.mockResolvedValue({ stdout: "", stderr: "" });
const request = createMockRequest(
{ filePath: "/Users/testuser/file.glb" },
{ "x-forwarded-for": "127.0.0.1", host: "localhost:3000" }
);
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
});
it("should allow requests from ::1 x-forwarded-for", async () => {
mockStat.mockResolvedValue({ isFile: () => true });
mockExecFileAsync.mockResolvedValue({ stdout: "", stderr: "" });
const request = createMockRequest(
{ filePath: "/Users/testuser/file.glb" },
{ "x-forwarded-for": "::1", host: "localhost:3000" }
);
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
});
it("should allow requests with localhost host header", async () => {
mockStat.mockResolvedValue({ isFile: () => true });
mockExecFileAsync.mockResolvedValue({ stdout: "", stderr: "" });
const request = createMockRequest(
{ filePath: "/Users/testuser/file.glb" },
{ host: "localhost:3000" }
);
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
});
});
describe("input validation", () => {
it("should return 400 for missing filePath", async () => {
const request = createMockRequest({});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe("File path is required");
});
it("should return 400 for empty filePath", async () => {
const request = createMockRequest({ filePath: "" });
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe("File path is required");
});
it("should return 400 for non-string filePath", async () => {
const request = createMockRequest({ filePath: 12345 });
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe("File path is required");
});
});
describe("path restriction", () => {
it("should return 403 for path outside home directory", async () => {
const request = createMockRequest({ filePath: "/etc/passwd" });
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(403);
expect(data.error).toBe("Path is outside allowed directory");
});
});
describe("file validation", () => {
it("should return 400 when path is a directory", async () => {
mockStat.mockResolvedValue({ isFile: () => false });
const request = createMockRequest({
filePath: "/Users/testuser/generations",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe("Path is not a file");
});
it("should return 400 when file does not exist", async () => {
mockStat.mockRejectedValue(new Error("ENOENT"));
const request = createMockRequest({
filePath: "/Users/testuser/nonexistent.glb",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe("File does not exist");
});
});
describe("platform commands", () => {
it("should call 'open -R' on macOS", async () => {
mockPlatform.mockReturnValue("darwin");
mockStat.mockResolvedValue({ isFile: () => true });
mockExecFileAsync.mockResolvedValue({ stdout: "", stderr: "" });
const request = createMockRequest({
filePath: "/Users/testuser/generations/model.glb",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(mockExecFileAsync).toHaveBeenCalledWith("open", [
"-R",
"/Users/testuser/generations/model.glb",
]);
});
it("should call 'xdg-open' with parent directory on Linux", async () => {
mockPlatform.mockReturnValue("linux");
mockStat.mockResolvedValue({ isFile: () => true });
mockExecFileAsync.mockResolvedValue({ stdout: "", stderr: "" });
const request = createMockRequest({
filePath: "/Users/testuser/generations/model.glb",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(mockExecFileAsync).toHaveBeenCalledWith("xdg-open", [
"/Users/testuser/generations",
]);
});
it("should return 500 when command execution fails", async () => {
mockStat.mockResolvedValue({ isFile: () => true });
mockExecFileAsync.mockRejectedValue(new Error("Command not found"));
const request = createMockRequest({
filePath: "/Users/testuser/generations/model.glb",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(500);
expect(data.error).toBe("Failed to open file location");
});
});
});

56
src/app/api/save-generation/__tests__/route.test.ts

@ -27,7 +27,7 @@ vi.mock("@/utils/logger", () => ({
// Store original fetch
const originalFetch = global.fetch;
import { POST } from "../route";
import { POST, getExtensionFromUrl } from "../route";
// Helper to create mock NextRequest for POST
function createMockPostRequest(body: unknown): NextRequest {
@ -526,3 +526,57 @@ describe("/api/save-generation route", () => {
});
});
});
describe("getExtensionFromUrl", () => {
it("should extract .glb from a CDN URL", () => {
expect(getExtensionFromUrl("https://cdn.example.com/model.glb")).toBe("glb");
});
it("should extract .obj extension", () => {
expect(getExtensionFromUrl("https://cdn.example.com/model.obj")).toBe("obj");
});
it("should extract .fbx extension", () => {
expect(getExtensionFromUrl("https://cdn.example.com/model.fbx")).toBe("fbx");
});
it("should extract .usdz extension", () => {
expect(getExtensionFromUrl("https://cdn.example.com/model.usdz")).toBe("usdz");
});
it("should extract .stl extension", () => {
expect(getExtensionFromUrl("https://cdn.example.com/model.stl")).toBe("stl");
});
it("should extract .gltf extension", () => {
expect(getExtensionFromUrl("https://cdn.example.com/model.gltf")).toBe("gltf");
});
it("should return null for unrecognized extensions", () => {
expect(getExtensionFromUrl("https://cdn.example.com/file.xyz")).toBeNull();
});
it("should return null for URLs without extensions", () => {
expect(getExtensionFromUrl("https://cdn.example.com/model")).toBeNull();
});
it("should return null for invalid URLs", () => {
expect(getExtensionFromUrl("not-a-url")).toBeNull();
});
it("should handle query strings correctly", () => {
expect(getExtensionFromUrl("https://cdn.example.com/model.glb?token=abc123")).toBe("glb");
});
it("should handle fragments correctly", () => {
expect(getExtensionFromUrl("https://cdn.example.com/model.glb#section")).toBe("glb");
});
it("should return null for URL ending with dot", () => {
expect(getExtensionFromUrl("https://cdn.example.com/model.")).toBeNull();
});
it("should not recognize zip as a 3D extension", () => {
expect(getExtensionFromUrl("https://cdn.example.com/model.zip")).toBeNull();
});
});

Loading…
Cancel
Save