diff --git a/src/app/api/generate/__tests__/route.test.ts b/src/app/api/generate/__tests__/route.test.ts index e3c151d5..4e689b26 100644 --- a/src/app/api/generate/__tests__/route.test.ts +++ b/src/app/api/generate/__tests__/route.test.ts @@ -602,4 +602,273 @@ describe("/api/generate route", () => { ); }); }); + + describe("Input validation", () => { + it("should reject request with no prompt, images, or dynamic inputs", async () => { + process.env.GEMINI_API_KEY = "test-gemini-key"; + + const request = createMockPostRequest({ + model: "nano-banana-pro", + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.success).toBe(false); + expect(data.error).toBe("Prompt or image input is required"); + }); + + it("should accept request with only images (image-to-image)", async () => { + process.env.GEMINI_API_KEY = "test-gemini-key"; + + mockGenerateContent.mockResolvedValueOnce(createGeminiImageResponse()); + + const request = createMockPostRequest({ + images: ["data:image/png;base64,imageOnlyData"], + model: "nano-banana-pro", + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + // Gemini is called with undefined prompt (which becomes empty text) + expect(mockGenerateContent).toHaveBeenCalled(); + }); + + it("should accept request with dynamicInputs containing prompt", async () => { + process.env.GEMINI_API_KEY = "test-gemini-key"; + + mockGenerateContent.mockResolvedValueOnce(createGeminiImageResponse()); + + const request = createMockPostRequest({ + dynamicInputs: { + prompt: "Dynamic prompt text", + }, + model: "nano-banana-pro", + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + }); + + it("should accept request with dynamicInputs containing image frames", async () => { + process.env.GEMINI_API_KEY = "test-gemini-key"; + + mockGenerateContent.mockResolvedValueOnce(createGeminiImageResponse()); + + const request = createMockPostRequest({ + dynamicInputs: { + first_frame: "data:image/png;base64,firstFrameData", + last_frame: "data:image/png;base64,lastFrameData", + }, + model: "nano-banana-pro", + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + }); + + it("should accept request with dynamicInputs containing image_url", async () => { + process.env.GEMINI_API_KEY = "test-gemini-key"; + + mockGenerateContent.mockResolvedValueOnce(createGeminiImageResponse()); + + const request = createMockPostRequest({ + dynamicInputs: { + image_url: "data:image/png;base64,imageUrlData", + }, + model: "nano-banana-pro", + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + }); + + it("should handle multiple images in request", async () => { + process.env.GEMINI_API_KEY = "test-gemini-key"; + + mockGenerateContent.mockResolvedValueOnce(createGeminiImageResponse()); + + const request = createMockPostRequest({ + prompt: "Combine these images", + images: [ + "data:image/png;base64,image1Data", + "data:image/jpeg;base64,image2Data", + "data:image/webp;base64,image3Data", + ], + model: "nano-banana-pro", + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + expect(mockGenerateContent).toHaveBeenCalledWith( + expect.objectContaining({ + contents: [ + { + role: "user", + parts: [ + { text: "Combine these images" }, + { + inlineData: { + mimeType: "image/png", + data: "image1Data", + }, + }, + { + inlineData: { + mimeType: "image/jpeg", + data: "image2Data", + }, + }, + { + inlineData: { + mimeType: "image/webp", + data: "image3Data", + }, + }, + ], + }, + ], + }) + ); + }); + }); + + describe("Provider routing", () => { + it("should route to Gemini when no selectedModel provided", async () => { + process.env.GEMINI_API_KEY = "test-gemini-key"; + + mockGenerateContent.mockResolvedValueOnce(createGeminiImageResponse()); + + const request = createMockPostRequest({ + prompt: "Test prompt", + model: "nano-banana-pro", + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + // Gemini mock should have been called + expect(mockGenerateContent).toHaveBeenCalled(); + }); + + it("should route to Gemini when selectedModel.provider is gemini", async () => { + process.env.GEMINI_API_KEY = "test-gemini-key"; + + mockGenerateContent.mockResolvedValueOnce(createGeminiImageResponse()); + + const request = createMockPostRequest({ + prompt: "Test prompt", + model: "nano-banana-pro", + selectedModel: { + provider: "gemini", + modelId: "gemini-3-pro-image-preview", + displayName: "Gemini 3 Pro", + }, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + expect(mockGenerateContent).toHaveBeenCalled(); + }); + + it("should return 401 for Replicate provider without API key", async () => { + delete process.env.REPLICATE_API_KEY; + + const request = createMockPostRequest({ + prompt: "Test prompt", + selectedModel: { + provider: "replicate", + modelId: "stability-ai/sdxl", + displayName: "SDXL", + }, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(401); + expect(data.success).toBe(false); + expect(data.error).toContain("Replicate API key not configured"); + }); + }); + + describe("Response handling", () => { + it("should return proper response structure with image", async () => { + process.env.GEMINI_API_KEY = "test-gemini-key"; + + mockGenerateContent.mockResolvedValueOnce( + createGeminiImageResponse("image/jpeg", "jpegOutputData") + ); + + const request = createMockPostRequest({ + prompt: "Generate a photo", + model: "nano-banana-pro", + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data).toEqual({ + success: true, + image: "data:image/jpeg;base64,jpegOutputData", + }); + }); + + it("should handle response with default MIME type", async () => { + process.env.GEMINI_API_KEY = "test-gemini-key"; + + // Response with no mimeType specified + mockGenerateContent.mockResolvedValueOnce({ + candidates: [ + { + content: { + parts: [ + { + inlineData: { + data: "noMimeTypeData", + // mimeType intentionally omitted + }, + }, + ], + }, + }, + ], + }); + + const request = createMockPostRequest({ + prompt: "Generate image", + model: "nano-banana-pro", + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + // Should default to image/png + expect(data.image).toBe("data:image/png;base64,noMimeTypeData"); + }); + }); });