1 changed files with 298 additions and 0 deletions
@ -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<string, string>): 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"); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
Loading…
Reference in new issue