Files
ai-terminal-assistant/packages/server/tests/unit/routes/files.test.ts
T
kurihada c44527a890 test(server): 添加 routes 模块单元测试
- agents.test.ts: Agent presets CRUD API (18 tests)
- tools.test.ts: Tool registration & execution (10 tests)
- files.test.ts: File browser API with path security (24 tests)
- commands.test.ts: Slash commands CRUD & execution (20 tests)
- hooks.test.ts: Hooks configuration management (20 tests)

覆盖率从 29.59% 提升到 60.35%,共 287 个测试
2025-12-15 00:14:27 +08:00

264 lines
8.0 KiB
TypeScript

/**
* Files Route 测试
*
* 测试文件浏览器 REST API 端点
*/
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { Hono } from 'hono';
import {
filesRouter,
setWorkingDirectory,
getWorkingDirectory,
} from '../../../src/routes/files.js';
import { join } from 'node:path';
// Create test app
const app = new Hono();
app.route('/files', filesRouter);
// Save original working directory
const originalWorkdir = process.cwd();
describe('Files Route', () => {
beforeEach(() => {
// Reset to current working directory
setWorkingDirectory(originalWorkdir);
});
afterEach(() => {
// Restore original working directory
setWorkingDirectory(originalWorkdir);
});
describe('setWorkingDirectory / getWorkingDirectory', () => {
it('设置工作目录', () => {
setWorkingDirectory('/tmp');
expect(getWorkingDirectory()).toBe('/tmp');
});
it('获取当前工作目录', () => {
expect(getWorkingDirectory()).toBeDefined();
expect(typeof getWorkingDirectory()).toBe('string');
});
});
describe('GET /files - 获取工作目录信息', () => {
it('返回工作目录信息', async () => {
const res = await app.request('/files');
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
expect(json.data.workingDirectory).toBeDefined();
expect(json.data.separator).toBe('/');
});
});
describe('GET /files/list - 列出目录内容', () => {
it('列出当前目录', async () => {
const res = await app.request('/files/list');
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
expect(Array.isArray(json.data.files)).toBe(true);
});
it('列出指定目录', async () => {
const res = await app.request('/files/list?path=.');
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
expect(json.data.path).toBe('.');
});
it('包含隐藏文件', async () => {
const res = await app.request('/files/list?path=.&hidden=true');
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
});
it('路径在工作目录外返回 403', async () => {
const res = await app.request('/files/list?path=../../../../../../etc');
const json = await res.json();
expect(res.status).toBe(403);
expect(json.success).toBe(false);
expect(json.error).toContain('Access denied');
});
it('非目录返回 400', async () => {
// package.json 是一个文件,不是目录
const res = await app.request('/files/list?path=package.json');
const json = await res.json();
expect(res.status).toBe(400);
expect(json.success).toBe(false);
expect(json.error).toContain('Not a directory');
});
it('文件信息包含正确字段', async () => {
const res = await app.request('/files/list?path=.');
const json = await res.json();
expect(res.status).toBe(200);
if (json.data.files.length > 0) {
const file = json.data.files[0];
expect(file).toHaveProperty('name');
expect(file).toHaveProperty('path');
expect(file).toHaveProperty('type');
expect(file).toHaveProperty('size');
expect(file).toHaveProperty('modified');
}
});
});
describe('GET /files/read - 读取文件内容', () => {
it('缺少路径参数返回 400', async () => {
const res = await app.request('/files/read');
const json = await res.json();
expect(res.status).toBe(400);
expect(json.success).toBe(false);
expect(json.error).toContain('Path is required');
});
it('读取文本文件', async () => {
const res = await app.request('/files/read?path=package.json');
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
expect(json.data.encoding).toBe('utf-8');
expect(json.data.content).toContain('"name"');
});
it('路径在工作目录外返回 403', async () => {
const res = await app.request('/files/read?path=../../../../../../etc/passwd');
const json = await res.json();
expect(res.status).toBe(403);
expect(json.success).toBe(false);
});
it('读取目录返回 400', async () => {
const res = await app.request('/files/read?path=src');
const json = await res.json();
expect(res.status).toBe(400);
expect(json.error).toContain('Cannot read directory');
});
it('返回文件元信息', async () => {
const res = await app.request('/files/read?path=package.json');
const json = await res.json();
expect(res.status).toBe(200);
expect(json.data).toHaveProperty('path');
expect(json.data).toHaveProperty('name');
expect(json.data).toHaveProperty('type');
expect(json.data).toHaveProperty('size');
expect(json.data).toHaveProperty('modified');
});
});
describe('GET /files/stat - 获取文件信息', () => {
it('缺少路径参数返回 400', async () => {
const res = await app.request('/files/stat');
const json = await res.json();
expect(res.status).toBe(400);
expect(json.success).toBe(false);
expect(json.error).toContain('Path is required');
});
it('获取文件信息', async () => {
const res = await app.request('/files/stat?path=package.json');
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
expect(json.data.type).toBe('file');
expect(json.data.name).toBe('package.json');
});
it('获取目录信息', async () => {
const res = await app.request('/files/stat?path=src');
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
expect(json.data.type).toBe('directory');
});
it('路径在工作目录外返回 403', async () => {
const res = await app.request('/files/stat?path=../../../../../../etc/passwd');
const json = await res.json();
expect(res.status).toBe(403);
expect(json.success).toBe(false);
});
it('文件不存在返回 500', async () => {
const res = await app.request('/files/stat?path=non-existent-file-12345.txt');
const json = await res.json();
expect(res.status).toBe(500);
expect(json.success).toBe(false);
});
});
describe('GET /files/tree - 获取目录树', () => {
it('获取目录树', async () => {
const res = await app.request('/files/tree?path=.&depth=1');
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
expect(json.data.path).toBe('.');
expect(Array.isArray(json.data.tree)).toBe(true);
});
it('限制深度', async () => {
const res = await app.request('/files/tree?path=.&depth=2');
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
});
it('显示隐藏文件', async () => {
const res = await app.request('/files/tree?path=.&depth=1&hidden=true');
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
});
it('路径在工作目录外返回 403', async () => {
const res = await app.request('/files/tree?path=../../../../../../etc');
const json = await res.json();
expect(res.status).toBe(403);
expect(json.success).toBe(false);
});
it('树节点包含正确字段', async () => {
const res = await app.request('/files/tree?path=.&depth=1');
const json = await res.json();
expect(res.status).toBe(200);
if (json.data.tree.length > 0) {
const node = json.data.tree[0];
expect(node).toHaveProperty('name');
expect(node).toHaveProperty('path');
expect(node).toHaveProperty('type');
}
});
});
});