Files
puaros/packages/ipuaro/tests/unit/infrastructure/tools/read/GetStructureTool.test.ts
imfozilbek 2c6eb6ce9b feat(ipuaro): add PathValidator security utility (v0.13.0)
Add centralized path validation to prevent path traversal attacks.

- PathValidator class with sync/async validation methods
- Protects against '..' and '~' traversal patterns
- Validates paths are within project root
- Refactored all 7 file tools to use PathValidator
- 51 new tests for PathValidator
2025-12-01 14:02:23 +05:00

275 lines
10 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { promises as fs } from "node:fs"
import * as path from "node:path"
import * as os from "node:os"
import {
GetStructureTool,
type GetStructureResult,
} from "../../../../../src/infrastructure/tools/read/GetStructureTool.js"
import type { ToolContext } from "../../../../../src/domain/services/ITool.js"
import type { IStorage } from "../../../../../src/domain/services/IStorage.js"
function createMockStorage(): IStorage {
return {
getFile: vi.fn(),
setFile: vi.fn(),
deleteFile: vi.fn(),
getAllFiles: vi.fn(),
getAST: vi.fn(),
setAST: vi.fn(),
getMeta: vi.fn(),
setMeta: vi.fn(),
getSymbolIndex: vi.fn(),
setSymbolIndex: vi.fn(),
getDepsGraph: vi.fn(),
setDepsGraph: vi.fn(),
getConfig: vi.fn(),
setConfig: vi.fn(),
clear: vi.fn(),
} as unknown as IStorage
}
function createMockContext(projectRoot: string): ToolContext {
return {
projectRoot,
storage: createMockStorage(),
requestConfirmation: vi.fn().mockResolvedValue(true),
onProgress: vi.fn(),
}
}
describe("GetStructureTool", () => {
let tool: GetStructureTool
let tempDir: string
beforeEach(async () => {
tool = new GetStructureTool()
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ipuaro-test-"))
})
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true })
})
describe("metadata", () => {
it("should have correct name", () => {
expect(tool.name).toBe("get_structure")
})
it("should have correct category", () => {
expect(tool.category).toBe("read")
})
it("should not require confirmation", () => {
expect(tool.requiresConfirmation).toBe(false)
})
it("should have correct parameters", () => {
expect(tool.parameters).toHaveLength(2)
expect(tool.parameters[0].name).toBe("path")
expect(tool.parameters[0].required).toBe(false)
expect(tool.parameters[1].name).toBe("depth")
expect(tool.parameters[1].required).toBe(false)
})
})
describe("validateParams", () => {
it("should return null for empty params", () => {
expect(tool.validateParams({})).toBeNull()
})
it("should return null for valid path", () => {
expect(tool.validateParams({ path: "src" })).toBeNull()
})
it("should return null for valid depth", () => {
expect(tool.validateParams({ depth: 3 })).toBeNull()
})
it("should return error for non-string path", () => {
expect(tool.validateParams({ path: 123 })).toBe("Parameter 'path' must be a string")
})
it("should return error for non-integer depth", () => {
expect(tool.validateParams({ depth: 2.5 })).toBe("Parameter 'depth' must be an integer")
})
it("should return error for depth < 1", () => {
expect(tool.validateParams({ depth: 0 })).toBe("Parameter 'depth' must be >= 1")
})
})
describe("execute", () => {
it("should return tree structure for empty directory", async () => {
const ctx = createMockContext(tempDir)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetStructureResult
expect(data.path).toBe(".")
expect(data.tree.type).toBe("directory")
expect(data.tree.children).toEqual([])
expect(data.stats.directories).toBe(1)
expect(data.stats.files).toBe(0)
})
it("should return tree structure with files", async () => {
await fs.writeFile(path.join(tempDir, "file1.ts"), "")
await fs.writeFile(path.join(tempDir, "file2.ts"), "")
const ctx = createMockContext(tempDir)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetStructureResult
expect(data.tree.children).toHaveLength(2)
expect(data.stats.files).toBe(2)
expect(data.content).toContain("file1.ts")
expect(data.content).toContain("file2.ts")
})
it("should return nested directory structure", async () => {
await fs.mkdir(path.join(tempDir, "src"))
await fs.writeFile(path.join(tempDir, "src", "index.ts"), "")
await fs.mkdir(path.join(tempDir, "src", "utils"))
await fs.writeFile(path.join(tempDir, "src", "utils", "helper.ts"), "")
const ctx = createMockContext(tempDir)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetStructureResult
expect(data.stats.directories).toBe(3)
expect(data.stats.files).toBe(2)
expect(data.content).toContain("src")
expect(data.content).toContain("index.ts")
expect(data.content).toContain("utils")
expect(data.content).toContain("helper.ts")
})
it("should respect depth parameter", async () => {
await fs.mkdir(path.join(tempDir, "level1"))
await fs.mkdir(path.join(tempDir, "level1", "level2"))
await fs.mkdir(path.join(tempDir, "level1", "level2", "level3"))
await fs.writeFile(path.join(tempDir, "level1", "level2", "level3", "deep.ts"), "")
const ctx = createMockContext(tempDir)
const result = await tool.execute({ depth: 2 }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetStructureResult
expect(data.content).toContain("level1")
expect(data.content).toContain("level2")
expect(data.content).not.toContain("level3")
expect(data.content).not.toContain("deep.ts")
})
it("should filter subdirectory when path specified", async () => {
await fs.mkdir(path.join(tempDir, "src"))
await fs.mkdir(path.join(tempDir, "tests"))
await fs.writeFile(path.join(tempDir, "src", "index.ts"), "")
await fs.writeFile(path.join(tempDir, "tests", "test.ts"), "")
const ctx = createMockContext(tempDir)
const result = await tool.execute({ path: "src" }, ctx)
expect(result.success).toBe(true)
const data = result.data as GetStructureResult
expect(data.path).toBe("src")
expect(data.content).toContain("index.ts")
expect(data.content).not.toContain("test.ts")
})
it("should ignore node_modules", async () => {
await fs.mkdir(path.join(tempDir, "node_modules"))
await fs.writeFile(path.join(tempDir, "node_modules", "pkg.js"), "")
await fs.writeFile(path.join(tempDir, "index.ts"), "")
const ctx = createMockContext(tempDir)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetStructureResult
expect(data.content).not.toContain("node_modules")
expect(data.content).toContain("index.ts")
})
it("should ignore .git directory", async () => {
await fs.mkdir(path.join(tempDir, ".git"))
await fs.writeFile(path.join(tempDir, ".git", "config"), "")
await fs.writeFile(path.join(tempDir, "index.ts"), "")
const ctx = createMockContext(tempDir)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetStructureResult
expect(data.content).not.toContain(".git")
})
it("should sort directories before files", async () => {
await fs.writeFile(path.join(tempDir, "aaa.ts"), "")
await fs.mkdir(path.join(tempDir, "zzz"))
const ctx = createMockContext(tempDir)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetStructureResult
const zzzIndex = data.content.indexOf("zzz")
const aaaIndex = data.content.indexOf("aaa.ts")
expect(zzzIndex).toBeLessThan(aaaIndex)
})
it("should return error for path outside project root", async () => {
const ctx = createMockContext(tempDir)
const result = await tool.execute({ path: "../outside" }, ctx)
expect(result.success).toBe(false)
expect(result.error).toBe("Path contains traversal patterns")
})
it("should return error for non-directory path", async () => {
await fs.writeFile(path.join(tempDir, "file.ts"), "")
const ctx = createMockContext(tempDir)
const result = await tool.execute({ path: "file.ts" }, ctx)
expect(result.success).toBe(false)
expect(result.error).toContain("is not a directory")
})
it("should return error for non-existent path", async () => {
const ctx = createMockContext(tempDir)
const result = await tool.execute({ path: "nonexistent" }, ctx)
expect(result.success).toBe(false)
expect(result.error).toContain("ENOENT")
})
it("should include callId in result", async () => {
const ctx = createMockContext(tempDir)
const result = await tool.execute({}, ctx)
expect(result.callId).toMatch(/^get_structure-\d+$/)
})
it("should use tree icons in output", async () => {
await fs.mkdir(path.join(tempDir, "src"))
await fs.writeFile(path.join(tempDir, "index.ts"), "")
const ctx = createMockContext(tempDir)
const result = await tool.execute({}, ctx)
expect(result.success).toBe(true)
const data = result.data as GetStructureResult
expect(data.content).toContain("📁")
expect(data.content).toContain("📄")
})
})
})