From f7625ed1ca4d3333a289834a393a7f33e29ef0b6 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Tue, 13 Jan 2026 14:45:45 +1300 Subject: [PATCH] test(18-01): add workflow route tests with fs mocking Co-Authored-By: Claude Opus 4.5 --- src/app/api/workflow/__tests__/route.test.ts | 298 +++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 src/app/api/workflow/__tests__/route.test.ts diff --git a/src/app/api/workflow/__tests__/route.test.ts b/src/app/api/workflow/__tests__/route.test.ts new file mode 100644 index 00000000..c4b5ec5e --- /dev/null +++ b/src/app/api/workflow/__tests__/route.test.ts @@ -0,0 +1,298 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { NextRequest } from "next/server"; + +// Mock fs/promises before importing the route +const mockStat = vi.fn(); +const mockMkdir = vi.fn(); +const mockWriteFile = vi.fn(); + +vi.mock("fs/promises", () => ({ + stat: (...args: unknown[]) => mockStat(...args), + mkdir: (...args: unknown[]) => mockMkdir(...args), + writeFile: (...args: unknown[]) => mockWriteFile(...args), +})); + +// Mock logger to avoid console noise during tests +vi.mock("@/utils/logger", () => ({ + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }, +})); + +import { POST, GET } from "../route"; + +// Helper to create mock NextRequest for POST +function createMockPostRequest(body: unknown): NextRequest { + return { + json: vi.fn().mockResolvedValue(body), + } as unknown as NextRequest; +} + +// Helper to create mock NextRequest for GET +function createMockGetRequest(params: Record): NextRequest { + return { + nextUrl: { + searchParams: new URLSearchParams(params), + }, + } as unknown as NextRequest; +} + +describe("/api/workflow route", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + describe("POST - Save workflow", () => { + it("should save workflow successfully", async () => { + const mockWorkflow = { + nodes: [{ id: "node1", type: "prompt" }], + edges: [], + }; + + mockStat.mockResolvedValue({ + isDirectory: () => true, + }); + mockMkdir.mockResolvedValue(undefined); + mockWriteFile.mockResolvedValue(undefined); + + const request = createMockPostRequest({ + directoryPath: "/test/dir", + filename: "my-workflow", + workflow: mockWorkflow, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(data.success).toBe(true); + expect(data.filePath).toBe("/test/dir/my-workflow.json"); + expect(mockWriteFile).toHaveBeenCalledWith( + "/test/dir/my-workflow.json", + JSON.stringify(mockWorkflow, null, 2), + "utf-8" + ); + }); + + it("should sanitize filename with special characters", async () => { + const mockWorkflow = { nodes: [], edges: [] }; + + mockStat.mockResolvedValue({ + isDirectory: () => true, + }); + mockMkdir.mockResolvedValue(undefined); + mockWriteFile.mockResolvedValue(undefined); + + const request = createMockPostRequest({ + directoryPath: "/test/dir", + filename: "my workflow!@#$%", + workflow: mockWorkflow, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(data.success).toBe(true); + expect(data.filePath).toBe("/test/dir/my_workflow_____.json"); + }); + + it("should create inputs and generations subfolders", async () => { + const mockWorkflow = { nodes: [], edges: [] }; + + mockStat.mockResolvedValue({ + isDirectory: () => true, + }); + mockMkdir.mockResolvedValue(undefined); + mockWriteFile.mockResolvedValue(undefined); + + const request = createMockPostRequest({ + directoryPath: "/test/dir", + filename: "workflow", + workflow: mockWorkflow, + }); + + await POST(request); + + expect(mockMkdir).toHaveBeenCalledWith("/test/dir/inputs", { recursive: true }); + expect(mockMkdir).toHaveBeenCalledWith("/test/dir/generations", { recursive: true }); + }); + + it("should reject missing directoryPath", async () => { + const request = createMockPostRequest({ + filename: "workflow", + workflow: { nodes: [], edges: [] }, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.success).toBe(false); + expect(data.error).toBe("Missing required fields"); + }); + + it("should reject missing filename", async () => { + const request = createMockPostRequest({ + directoryPath: "/test/dir", + workflow: { nodes: [], edges: [] }, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.success).toBe(false); + expect(data.error).toBe("Missing required fields"); + }); + + it("should reject missing workflow", async () => { + const request = createMockPostRequest({ + directoryPath: "/test/dir", + filename: "workflow", + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.success).toBe(false); + expect(data.error).toBe("Missing required fields"); + }); + + it("should reject non-directory path", async () => { + mockStat.mockResolvedValue({ + isDirectory: () => false, + }); + + const request = createMockPostRequest({ + directoryPath: "/test/file.txt", + filename: "workflow", + workflow: { nodes: [], edges: [] }, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.success).toBe(false); + expect(data.error).toBe("Path is not a directory"); + }); + + it("should reject non-existent directory", async () => { + mockStat.mockRejectedValue(new Error("ENOENT")); + + const request = createMockPostRequest({ + directoryPath: "/nonexistent/dir", + filename: "workflow", + workflow: { nodes: [], edges: [] }, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.success).toBe(false); + expect(data.error).toBe("Directory does not exist"); + }); + + it("should continue saving even if subfolder creation fails", async () => { + const mockWorkflow = { nodes: [], edges: [] }; + + mockStat.mockResolvedValue({ + isDirectory: () => true, + }); + mockMkdir.mockRejectedValue(new Error("Permission denied")); + mockWriteFile.mockResolvedValue(undefined); + + const request = createMockPostRequest({ + directoryPath: "/test/dir", + filename: "workflow", + workflow: mockWorkflow, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(data.success).toBe(true); + expect(data.filePath).toBe("/test/dir/workflow.json"); + }); + + it("should return 500 on write failure", async () => { + mockStat.mockResolvedValue({ + isDirectory: () => true, + }); + mockMkdir.mockResolvedValue(undefined); + mockWriteFile.mockRejectedValue(new Error("Disk full")); + + const request = createMockPostRequest({ + directoryPath: "/test/dir", + filename: "workflow", + workflow: { nodes: [], edges: [] }, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(500); + expect(data.success).toBe(false); + expect(data.error).toBe("Disk full"); + }); + }); + + describe("GET - Validate directory", () => { + it("should return exists: true for existing directory", async () => { + mockStat.mockResolvedValue({ + isDirectory: () => true, + }); + + const request = createMockGetRequest({ path: "/test/dir" }); + const response = await GET(request); + const data = await response.json(); + + expect(data.success).toBe(true); + expect(data.exists).toBe(true); + expect(data.isDirectory).toBe(true); + }); + + it("should return isDirectory: false for file path", async () => { + mockStat.mockResolvedValue({ + isDirectory: () => false, + }); + + const request = createMockGetRequest({ path: "/test/file.txt" }); + const response = await GET(request); + const data = await response.json(); + + expect(data.success).toBe(true); + expect(data.exists).toBe(true); + expect(data.isDirectory).toBe(false); + }); + + it("should return exists: false for non-existent path", async () => { + mockStat.mockRejectedValue(new Error("ENOENT")); + + const request = createMockGetRequest({ path: "/nonexistent" }); + const response = await GET(request); + const data = await response.json(); + + expect(data.success).toBe(true); + expect(data.exists).toBe(false); + expect(data.isDirectory).toBe(false); + }); + + it("should reject missing path parameter", async () => { + const request = createMockGetRequest({}); + const response = await GET(request); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.success).toBe(false); + expect(data.error).toBe("Path parameter required"); + }); + }); +});