chore: initialize qiming workspace repository
This commit is contained in:
275
qiming-file-server/tests/README.md
Normal file
275
qiming-file-server/tests/README.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# 测试文档
|
||||
|
||||
## 测试框架
|
||||
|
||||
本项目使用 **Jest** 作为测试框架,配合 **Supertest** 进行 API 集成测试。
|
||||
|
||||
## 测试目录结构
|
||||
|
||||
```
|
||||
tests/
|
||||
├── setup.js # 测试环境配置
|
||||
├── unit/ # 单元测试
|
||||
│ ├── errorHandler.test.js
|
||||
│ ├── restartJudgeUtils.test.js
|
||||
│ └── dependencyManager.test.js
|
||||
└── integration/ # 集成测试(待添加)
|
||||
└── api.test.js
|
||||
```
|
||||
|
||||
## 运行测试
|
||||
|
||||
### 运行所有测试
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
### 运行测试并监听文件变化
|
||||
|
||||
```bash
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
### 只运行单元测试
|
||||
|
||||
```bash
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### 只运行集成测试
|
||||
|
||||
```bash
|
||||
npm run test:integration
|
||||
```
|
||||
|
||||
### 查看测试覆盖率
|
||||
|
||||
测试覆盖率报告会自动生成在 `coverage/` 目录下。
|
||||
|
||||
```bash
|
||||
# 运行测试后查看覆盖率
|
||||
npm test
|
||||
|
||||
# 在浏览器中查看详细报告
|
||||
open coverage/lcov-report/index.html
|
||||
```
|
||||
|
||||
## 测试覆盖率目标
|
||||
|
||||
当前设置的覆盖率阈值:
|
||||
|
||||
- **分支覆盖率**: 50%
|
||||
- **函数覆盖率**: 50%
|
||||
- **行覆盖率**: 50%
|
||||
- **语句覆盖率**: 50%
|
||||
|
||||
## 编写测试
|
||||
|
||||
### 单元测试示例
|
||||
|
||||
```javascript
|
||||
// tests/unit/myModule.test.js
|
||||
const { myFunction } = require("../../src/utils/myModule");
|
||||
|
||||
describe("我的模块测试", () => {
|
||||
test("应该返回正确的结果", () => {
|
||||
const result = myFunction("input");
|
||||
expect(result).toBe("expected output");
|
||||
});
|
||||
|
||||
test("应该处理错误情况", () => {
|
||||
expect(() => {
|
||||
myFunction(null);
|
||||
}).toThrow("错误信息");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 集成测试示例
|
||||
|
||||
```javascript
|
||||
// tests/integration/api.test.js
|
||||
const request = require("supertest");
|
||||
const app = require("../../src/server");
|
||||
|
||||
describe("API 集成测试", () => {
|
||||
test("GET /api/build/list-dev 应该返回进程列表", async () => {
|
||||
const response = await request(app).get("/api/build/list-dev").expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(Array.isArray(response.body.list)).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 测试工具函数
|
||||
|
||||
在 `tests/setup.js` 中提供了全局测试工具:
|
||||
|
||||
```javascript
|
||||
// 生成测试用的 projectId
|
||||
const projectId = global.testUtils.generateProjectId();
|
||||
|
||||
// 生成测试用的版本号
|
||||
const version = global.testUtils.generateVersion();
|
||||
|
||||
// 等待函数
|
||||
await global.testUtils.sleep(1000); // 等待1秒
|
||||
```
|
||||
|
||||
## 测试最佳实践
|
||||
|
||||
### 1. 测试命名
|
||||
|
||||
- 使用清晰的描述性名称
|
||||
- 使用 `describe` 分组相关测试
|
||||
- 使用 `test` 或 `it` 描述单个测试用例
|
||||
|
||||
### 2. 测试隔离
|
||||
|
||||
- 每个测试应该独立运行
|
||||
- 使用 `beforeEach` 和 `afterEach` 清理状态
|
||||
- 避免测试之间的依赖关系
|
||||
|
||||
### 3. 测试覆盖
|
||||
|
||||
- 测试正常情况
|
||||
- 测试边界条件
|
||||
- 测试错误情况
|
||||
- 测试异常输入
|
||||
|
||||
### 4. Mock 和 Stub
|
||||
|
||||
```javascript
|
||||
// Mock 外部依赖
|
||||
jest.mock("../../src/utils/externalService");
|
||||
|
||||
// Mock 函数
|
||||
const mockFn = jest.fn().mockReturnValue("mocked value");
|
||||
|
||||
// 验证调用
|
||||
expect(mockFn).toHaveBeenCalledWith("expected argument");
|
||||
expect(mockFn).toHaveBeenCalledTimes(1);
|
||||
```
|
||||
|
||||
### 5. 异步测试
|
||||
|
||||
```javascript
|
||||
// 使用 async/await
|
||||
test("异步操作", async () => {
|
||||
const result = await asyncFunction();
|
||||
expect(result).toBe("expected");
|
||||
});
|
||||
|
||||
// 使用 Promise
|
||||
test("Promise 操作", () => {
|
||||
return promiseFunction().then((result) => {
|
||||
expect(result).toBe("expected");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 常用断言
|
||||
|
||||
```javascript
|
||||
// 相等性
|
||||
expect(value).toBe(expected); // ===
|
||||
expect(value).toEqual(expected); // 深度相等
|
||||
|
||||
// 真值
|
||||
expect(value).toBeTruthy();
|
||||
expect(value).toBeFalsy();
|
||||
expect(value).toBeNull();
|
||||
expect(value).toBeUndefined();
|
||||
expect(value).toBeDefined();
|
||||
|
||||
// 数字
|
||||
expect(value).toBeGreaterThan(3);
|
||||
expect(value).toBeGreaterThanOrEqual(3.5);
|
||||
expect(value).toBeLessThan(5);
|
||||
expect(value).toBeLessThanOrEqual(4.5);
|
||||
|
||||
// 字符串
|
||||
expect(string).toMatch(/pattern/);
|
||||
expect(string).toContain("substring");
|
||||
|
||||
// 数组
|
||||
expect(array).toContain(item);
|
||||
expect(array).toHaveLength(3);
|
||||
|
||||
// 对象
|
||||
expect(object).toHaveProperty("key");
|
||||
expect(object).toHaveProperty("key", "value");
|
||||
|
||||
// 异常
|
||||
expect(() => {
|
||||
throw new Error("error");
|
||||
}).toThrow("error");
|
||||
```
|
||||
|
||||
## 调试测试
|
||||
|
||||
### 1. 运行单个测试文件
|
||||
|
||||
```bash
|
||||
npm test -- tests/unit/errorHandler.test.js
|
||||
```
|
||||
|
||||
### 2. 运行匹配的测试
|
||||
|
||||
```bash
|
||||
npm test -- --testNamePattern="应该正确创建"
|
||||
```
|
||||
|
||||
### 3. 使用 Node 调试器
|
||||
|
||||
```bash
|
||||
node --inspect-brk node_modules/.bin/jest --runInBand
|
||||
```
|
||||
|
||||
## 持续集成
|
||||
|
||||
测试应该在每次提交前运行:
|
||||
|
||||
```bash
|
||||
# 提交前运行测试
|
||||
npm test
|
||||
|
||||
# 如果测试通过,再提交代码
|
||||
git add .
|
||||
git commit -m "feat: 添加新功能"
|
||||
```
|
||||
|
||||
## 测试环境配置
|
||||
|
||||
测试环境使用独立的配置:
|
||||
|
||||
- 端口: 10003
|
||||
- 日志: 禁用控制台输出
|
||||
- 超时: 10 秒
|
||||
|
||||
这些配置在 `tests/setup.js` 中设置。
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **不要提交失败的测试** - 确保所有测试通过后再提交
|
||||
2. **保持测试快速** - 单元测试应该在毫秒级完成
|
||||
3. **定期更新测试** - 代码变更时同步更新测试
|
||||
4. **编写有意义的测试** - 测试应该验证真实的业务逻辑
|
||||
5. **避免测试实现细节** - 测试行为,而非实现
|
||||
|
||||
## 贡献测试
|
||||
|
||||
欢迎为项目添加更多测试!优先添加:
|
||||
|
||||
1. 核心业务逻辑的单元测试
|
||||
2. API 端点的集成测试
|
||||
3. 边界条件和错误处理测试
|
||||
4. 工具函数的测试
|
||||
|
||||
## 相关资源
|
||||
|
||||
- [Jest 文档](https://jestjs.io/docs/getting-started)
|
||||
- [Supertest 文档](https://github.com/visionmedia/supertest)
|
||||
- [测试最佳实践](https://github.com/goldbergyoni/javascript-testing-best-practices)
|
||||
33
qiming-file-server/tests/setup.js
Normal file
33
qiming-file-server/tests/setup.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// Jest 测试环境设置
|
||||
|
||||
// 设置测试环境变量
|
||||
process.env.NODE_ENV = "test";
|
||||
process.env.PORT = "10003";
|
||||
process.env.LOG_CONSOLE_ENABLED = "false";
|
||||
|
||||
// 增加测试超时时间(ESM 兼容写法)
|
||||
if (typeof jest !== "undefined") {
|
||||
jest.setTimeout(10000);
|
||||
}
|
||||
|
||||
// 全局测试工具
|
||||
globalThis.testUtils = {
|
||||
// 生成测试用的 projectId
|
||||
generateProjectId: () => `test-project-${Date.now()}`,
|
||||
|
||||
// 生成测试用的版本号
|
||||
generateVersion: () => Math.floor(Math.random() * 100),
|
||||
|
||||
// 等待函数
|
||||
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
||||
};
|
||||
|
||||
// 测试前清理
|
||||
beforeAll(() => {
|
||||
console.log("🧪 开始测试...");
|
||||
});
|
||||
|
||||
// 测试后清理
|
||||
afterAll(() => {
|
||||
console.log("✅ 测试完成!");
|
||||
});
|
||||
406
qiming-file-server/tests/unit/cli.test.js
Normal file
406
qiming-file-server/tests/unit/cli.test.js
Normal file
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* CLI 模块测试
|
||||
*
|
||||
* 测试 qiming-file-server CLI 相关的功能
|
||||
*
|
||||
* 覆盖范围:
|
||||
* - serviceManager.js 服务管理器
|
||||
* - envUtils.js 环境变量工具
|
||||
* - PID 文件操作
|
||||
* - 跨平台兼容性
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import os from "os";
|
||||
|
||||
// 测试配置文件路径
|
||||
const testConfig = {
|
||||
testPidDir: path.join(os.tmpdir(), "qiming-file-server-test"),
|
||||
testPidFile: path.join(
|
||||
os.tmpdir(),
|
||||
"qiming-file-server-test",
|
||||
"server.pid"
|
||||
),
|
||||
};
|
||||
|
||||
describe("CLI Service Manager", () => {
|
||||
let serviceManager;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await import("../../src/utils/serviceManager.js");
|
||||
serviceManager = module.default || module;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
if (fs.existsSync(testConfig.testPidFile)) {
|
||||
fs.removeSync(testConfig.testPidFile);
|
||||
}
|
||||
} catch (err) {
|
||||
// 忽略清理错误
|
||||
}
|
||||
});
|
||||
|
||||
describe("getPidFilePath", () => {
|
||||
it("应该返回有效的 PID 文件路径", () => {
|
||||
const pidPath = serviceManager.getPidFilePath();
|
||||
|
||||
expect(pidPath).toBeDefined();
|
||||
expect(typeof pidPath).toBe("string");
|
||||
expect(pidPath.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("应该在临时目录中", () => {
|
||||
const pidPath = serviceManager.getPidFilePath();
|
||||
const tmpDir = os.tmpdir();
|
||||
|
||||
expect(pidPath.startsWith(tmpDir)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isWindows", () => {
|
||||
it("应该正确检测当前操作系统", () => {
|
||||
const isWin = serviceManager.isWindows();
|
||||
|
||||
const currentPlatform = process.platform;
|
||||
|
||||
if (currentPlatform === "win32") {
|
||||
expect(isWin).toBe(true);
|
||||
} else {
|
||||
expect(isWin).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("isProcessRunning", () => {
|
||||
it("应该正确判断不存在的进程", () => {
|
||||
const result = serviceManager.isProcessRunning(999999999);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("应该正确判断当前进程", () => {
|
||||
const result = serviceManager.isProcessRunning(process.pid);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readPidFile", () => {
|
||||
it("应该返回 null 如果文件不存在", () => {
|
||||
const result = serviceManager.readPidFile();
|
||||
|
||||
expect(result === null || result === undefined).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("writePidFile and readPidFile", () => {
|
||||
it("应该能够写入和读取 PID 文件", () => {
|
||||
const testPidInfo = {
|
||||
pid: 12345,
|
||||
startedAt: new Date().toISOString(),
|
||||
env: "test",
|
||||
port: "60000",
|
||||
version: "1.0.0",
|
||||
platform: process.platform,
|
||||
};
|
||||
|
||||
const testPidPath = path.join(testConfig.testPidDir, "server.pid");
|
||||
fs.ensureDirSync(testConfig.testPidDir);
|
||||
fs.writeFileSync(testPidPath, JSON.stringify(testPidInfo, null, 2));
|
||||
|
||||
const content = fs.readFileSync(testPidPath, "utf8");
|
||||
const readPidInfo = JSON.parse(content);
|
||||
|
||||
expect(readPidInfo.pid).toBe(12345);
|
||||
expect(readPidInfo.env).toBe("test");
|
||||
expect(readPidInfo.port).toBe("60000");
|
||||
|
||||
fs.removeSync(testConfig.testPidDir);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatUptime", () => {
|
||||
it("应该正确格式化运行时间", () => {
|
||||
const startedAt = new Date(Date.now() - 3600000).toISOString();
|
||||
const result = serviceManager.formatUptime(startedAt);
|
||||
|
||||
expect(result).toContain("小时");
|
||||
});
|
||||
|
||||
it("应该处理无效的日期", () => {
|
||||
const result = serviceManager.formatUptime("invalid-date");
|
||||
|
||||
expect(result).toBe("未知");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getServiceStatus", () => {
|
||||
it("应该返回正确的服务状态", () => {
|
||||
const status = serviceManager.getServiceStatus();
|
||||
|
||||
expect(status).toHaveProperty("running");
|
||||
expect(status).toHaveProperty("pidInfo");
|
||||
expect(status).toHaveProperty("message");
|
||||
expect(typeof status.running).toBe("boolean");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("CLI Environment Utils", () => {
|
||||
let envUtils;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await import("../../src/utils/envUtils.js");
|
||||
envUtils = module.default || module;
|
||||
});
|
||||
|
||||
describe("isWindows", () => {
|
||||
it("应该正确检测 Windows 平台", () => {
|
||||
const isWin = envUtils.isWindows();
|
||||
const currentPlatform = process.platform;
|
||||
|
||||
expect(isWin).toBe(currentPlatform === "win32");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeEnvName", () => {
|
||||
it("Windows 环境应该转为大写", () => {
|
||||
if (process.platform === "win32") {
|
||||
const result = envUtils.normalizeEnvName("test_env");
|
||||
expect(result).toBe("TEST_ENV");
|
||||
} else {
|
||||
const result = envUtils.normalizeEnvName("test_env");
|
||||
expect(result).toBe("test_env");
|
||||
}
|
||||
});
|
||||
|
||||
it("应该处理空字符串", () => {
|
||||
const result = envUtils.normalizeEnvName("");
|
||||
expect(result).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEnv", () => {
|
||||
beforeEach(() => {
|
||||
process.env.TEST_VAR = "test-value";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.TEST_VAR;
|
||||
});
|
||||
|
||||
it("应该返回已设置的环境变量", () => {
|
||||
const result = envUtils.getEnv("TEST_VAR");
|
||||
expect(result).toBe("test-value");
|
||||
});
|
||||
|
||||
it("应该返回默认值如果未设置", () => {
|
||||
const result = envUtils.getEnv("NON_EXISTENT_VAR", "default");
|
||||
expect(result).toBe("default");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBoolEnv", () => {
|
||||
beforeEach(() => {
|
||||
process.env.TEST_BOOL_TRUE = "true";
|
||||
process.env.TEST_BOOL_FALSE = "false";
|
||||
process.env.TEST_BOOL_ONE = "1";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.TEST_BOOL_TRUE;
|
||||
delete process.env.TEST_BOOL_FALSE;
|
||||
delete process.env.TEST_BOOL_ONE;
|
||||
});
|
||||
|
||||
it("应该正确解析 true 值", () => {
|
||||
expect(envUtils.getBoolEnv("TEST_BOOL_TRUE")).toBe(true);
|
||||
});
|
||||
|
||||
it("应该正确解析 1 值", () => {
|
||||
expect(envUtils.getBoolEnv("TEST_BOOL_ONE")).toBe(true);
|
||||
});
|
||||
|
||||
it("应该正确解析 false 值", () => {
|
||||
expect(envUtils.getBoolEnv("TEST_BOOL_FALSE")).toBe(false);
|
||||
});
|
||||
|
||||
it("应该返回默认值如果未设置", () => {
|
||||
expect(envUtils.getBoolEnv("NON_EXISTENT", true)).toBe(true);
|
||||
expect(envUtils.getBoolEnv("NON_EXISTENT", false)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getNumberEnv", () => {
|
||||
beforeEach(() => {
|
||||
process.env.TEST_NUMBER = "123";
|
||||
process.env.TEST_NAN = "abc";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.TEST_NUMBER;
|
||||
delete process.env.TEST_NAN;
|
||||
});
|
||||
|
||||
it("应该正确解析数字", () => {
|
||||
const result = envUtils.getNumberEnv("TEST_NUMBER");
|
||||
expect(result).toBe(123);
|
||||
});
|
||||
|
||||
it("应该返回默认值对于 NaN", () => {
|
||||
const result = envUtils.getNumberEnv("TEST_NAN", 0);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it("应该返回默认值如果未设置", () => {
|
||||
const result = envUtils.getNumberEnv("NON_EXISTENT", 42);
|
||||
expect(result).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseEnvType", () => {
|
||||
it("应该正确解析 dev", () => {
|
||||
expect(envUtils.parseEnvType("dev")).toBe("development");
|
||||
});
|
||||
|
||||
it("应该正确解析 prod", () => {
|
||||
expect(envUtils.parseEnvType("prod")).toBe("production");
|
||||
});
|
||||
|
||||
it("应该正确解析 test", () => {
|
||||
expect(envUtils.parseEnvType("test")).toBe("test");
|
||||
});
|
||||
|
||||
it("应该处理大小写", () => {
|
||||
expect(envUtils.parseEnvType("DEV")).toBe("development");
|
||||
expect(envUtils.parseEnvType("PROD")).toBe("production");
|
||||
});
|
||||
|
||||
it("应该处理空值", () => {
|
||||
expect(envUtils.parseEnvType("")).toBe("production");
|
||||
expect(envUtils.parseEnvType(null)).toBe("production");
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadEnvFromArgv", () => {
|
||||
it("应该解析命令行参数", () => {
|
||||
const originalArgv = process.argv;
|
||||
process.argv = ["node", "test", "--test-key", "test-value"];
|
||||
|
||||
const result = envUtils.loadEnvFromArgv();
|
||||
|
||||
process.argv = originalArgv;
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("CLI Cross-Platform Compatibility", () => {
|
||||
describe("Platform Detection", () => {
|
||||
it("应该正确检测 darwin (macOS)", () => {
|
||||
if (process.platform === "darwin") {
|
||||
expect(process.platform).toBe("darwin");
|
||||
}
|
||||
});
|
||||
|
||||
it("应该正确检测 linux", () => {
|
||||
if (process.platform === "linux") {
|
||||
expect(process.platform).toBe("linux");
|
||||
}
|
||||
});
|
||||
|
||||
it("应该正确检测 win32", () => {
|
||||
if (process.platform === "win32") {
|
||||
expect(process.platform).toBe("win32");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Path Handling", () => {
|
||||
it("应该使用 path.join 进行路径拼接", () => {
|
||||
const result = path.join("dir", "subdir", "file.js");
|
||||
|
||||
expect(result).toMatch(/dir[\/\\]subdir[\/\\]file\.js/);
|
||||
});
|
||||
|
||||
it("应该使用 os.tmpdir() 获取临时目录", () => {
|
||||
const tmpDir = os.tmpdir();
|
||||
|
||||
expect(tmpDir).toBeDefined();
|
||||
expect(tmpDir.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("应该使用 os.homedir() 获取主目录", () => {
|
||||
const homeDir = os.homedir();
|
||||
|
||||
expect(homeDir).toBeDefined();
|
||||
expect(homeDir.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Environment Variables", () => {
|
||||
it("应该能够读取环境变量", () => {
|
||||
process.env.TEST_READ = "test";
|
||||
|
||||
expect(process.env.TEST_READ).toBe("test");
|
||||
|
||||
delete process.env.TEST_READ;
|
||||
});
|
||||
|
||||
it("应该支持设置和删除环境变量", () => {
|
||||
process.env.TEMP_TEST_VAR = "temp-value";
|
||||
|
||||
expect(process.env.TEMP_TEST_VAR).toBe("temp-value");
|
||||
|
||||
delete process.env.TEMP_TEST_VAR;
|
||||
|
||||
expect(process.env.TEMP_TEST_VAR).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("CLI Config", () => {
|
||||
let config;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await import("../../src/appConfig/index.js");
|
||||
config = module.default || module;
|
||||
});
|
||||
|
||||
describe("CLI Configuration", () => {
|
||||
it("应该包含 CLI 服务名称配置", () => {
|
||||
expect(config.CLI_SERVICE_NAME).toBe("qiming-file-server");
|
||||
});
|
||||
|
||||
it("应该包含 CLI PID 目录配置", () => {
|
||||
expect(config.CLI_PID_DIR).toBeDefined();
|
||||
expect(typeof config.CLI_PID_DIR).toBe("string");
|
||||
});
|
||||
|
||||
it("应该包含 CLI 停止超时配置", () => {
|
||||
expect(config.CLI_STOP_TIMEOUT).toBeDefined();
|
||||
expect(typeof config.CLI_STOP_TIMEOUT).toBe("number");
|
||||
});
|
||||
|
||||
it("应该包含 CLI 检查间隔配置", () => {
|
||||
expect(config.CLI_CHECK_INTERVAL).toBeDefined();
|
||||
expect(typeof config.CLI_CHECK_INTERVAL).toBe("number");
|
||||
});
|
||||
|
||||
it("应该包含 Windows 平台检测配置", () => {
|
||||
expect(config.CLI_IS_WINDOWS).toBeDefined();
|
||||
expect(typeof config.CLI_IS_WINDOWS).toBe("boolean");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Standard Configuration", () => {
|
||||
it("应该包含标准配置项", () => {
|
||||
expect(config.NODE_ENV).toBeDefined();
|
||||
expect(config.PORT).toBeDefined();
|
||||
expect(config.PROJECT_SOURCE_DIR).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
70
qiming-file-server/tests/unit/dependencyManager.test.js
Normal file
70
qiming-file-server/tests/unit/dependencyManager.test.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import fs from "fs";
|
||||
import {
|
||||
getFileMtime,
|
||||
shouldInstallDeps,
|
||||
} from "../../src/utils/buildDependency/dependencyManager.js";
|
||||
|
||||
// 在 ESM 模块中获取当前文件的实际路径
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
describe("依赖管理器测试", () => {
|
||||
describe("getFileMtime", () => {
|
||||
test("存在的文件应该返回时间戳", () => {
|
||||
// 使用当前测试文件作为测试对象,确保它存在且可访问
|
||||
const filePath = path.join(__dirname, "dependencyManager.test.js");
|
||||
const mtime = getFileMtime(filePath);
|
||||
expect(typeof mtime).toBe("number");
|
||||
expect(mtime).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("不存在的文件应该返回0", () => {
|
||||
const filePath = "/path/to/nonexistent/file.txt";
|
||||
const mtime = getFileMtime(filePath);
|
||||
expect(mtime).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldInstallDeps", () => {
|
||||
const testProjectPath = "/tmp/test-project-deps";
|
||||
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(testProjectPath)) {
|
||||
fs.rmSync(testProjectPath, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(testProjectPath, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(testProjectPath)) {
|
||||
fs.rmSync(testProjectPath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("不存在 node_modules 应该返回true", () => {
|
||||
fs.writeFileSync(path.join(testProjectPath, "package.json"), "{}");
|
||||
const result = shouldInstallDeps(testProjectPath);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("存在 node_modules 但 package.json 更新应该返回true", async () => {
|
||||
const nodeModulesPath = path.join(testProjectPath, "node_modules");
|
||||
fs.mkdirSync(nodeModulesPath);
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
fs.writeFileSync(path.join(testProjectPath, "package.json"), "{}");
|
||||
const result = shouldInstallDeps(testProjectPath);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("package.json 和 node_modules 都存在且时间正常应该返回false", async () => {
|
||||
fs.writeFileSync(path.join(testProjectPath, "package.json"), "{}");
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
const nodeModulesPath = path.join(testProjectPath, "node_modules");
|
||||
fs.mkdirSync(nodeModulesPath);
|
||||
const result = shouldInstallDeps(testProjectPath);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
96
qiming-file-server/tests/unit/errorHandler.test.js
Normal file
96
qiming-file-server/tests/unit/errorHandler.test.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
ValidationError,
|
||||
BusinessError,
|
||||
SystemError,
|
||||
ResourceError,
|
||||
FileError,
|
||||
ProcessError,
|
||||
formatErrorResponse,
|
||||
classifyError,
|
||||
} from "../../src/utils/error/errorHandler.js";
|
||||
|
||||
describe("错误处理工具测试", () => {
|
||||
describe("自定义错误类", () => {
|
||||
test("ValidationError 应该正确创建", () => {
|
||||
const error = new ValidationError("测试错误", { field: "test" });
|
||||
expect(error.name).toBe("ValidationError");
|
||||
expect(error.message).toBe("测试错误");
|
||||
expect(error.statusCode).toBe(400);
|
||||
expect(error.details).toEqual({ field: "test" });
|
||||
expect(error.isOperational).toBe(true);
|
||||
});
|
||||
|
||||
test("BusinessError 应该正确创建", () => {
|
||||
const error = new BusinessError("业务错误");
|
||||
expect(error.name).toBe("BusinessError");
|
||||
expect(error.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test("SystemError 应该正确创建", () => {
|
||||
const error = new SystemError("系统错误");
|
||||
expect(error.name).toBe("SystemError");
|
||||
expect(error.statusCode).toBe(500);
|
||||
});
|
||||
|
||||
test("ResourceError 应该正确创建", () => {
|
||||
const error = new ResourceError("资源不存在");
|
||||
expect(error.name).toBe("ResourceError");
|
||||
expect(error.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test("FileError 应该正确创建", () => {
|
||||
const error = new FileError("文件错误");
|
||||
expect(error.name).toBe("FileError");
|
||||
expect(error.statusCode).toBe(500);
|
||||
});
|
||||
|
||||
test("ProcessError 应该正确创建", () => {
|
||||
const error = new ProcessError("进程错误");
|
||||
expect(error.name).toBe("ProcessError");
|
||||
expect(error.statusCode).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatErrorResponse", () => {
|
||||
test("应该正确格式化错误响应", () => {
|
||||
const error = new ValidationError("测试错误", { field: "test" });
|
||||
const response = formatErrorResponse(error, "req-123");
|
||||
|
||||
expect(response.success).toBe(false);
|
||||
expect(response.error.type).toBe("VALIDATION_ERROR");
|
||||
expect(response.error.message).toBe("测试错误");
|
||||
expect(response.error.requestId).toBe("req-123");
|
||||
});
|
||||
|
||||
test("应该包含 timestamp", () => {
|
||||
const error = new BusinessError("测试");
|
||||
const response = formatErrorResponse(error);
|
||||
|
||||
expect(response.error.timestamp).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("classifyError", () => {
|
||||
test("应该正确分类 ValidationError", () => {
|
||||
const error = new ValidationError("测试");
|
||||
expect(classifyError(error)).toBe("VALIDATION_ERROR");
|
||||
});
|
||||
|
||||
test("应该正确分类 ENOENT 错误", () => {
|
||||
const error = new Error("文件不存在");
|
||||
error.code = "ENOENT";
|
||||
expect(classifyError(error)).toBe("RESOURCE_ERROR");
|
||||
});
|
||||
|
||||
test("应该正确分类 EACCES 错误", () => {
|
||||
const error = new Error("权限不足");
|
||||
error.code = "EACCES";
|
||||
expect(classifyError(error)).toBe("PERMISSION_ERROR");
|
||||
});
|
||||
|
||||
test("应该正确分类未知错误", () => {
|
||||
const error = new Error("未知错误");
|
||||
expect(classifyError(error)).toBe("UNKNOWN_ERROR");
|
||||
});
|
||||
});
|
||||
});
|
||||
96
qiming-file-server/tests/unit/restartJudgeUtils.test.js
Normal file
96
qiming-file-server/tests/unit/restartJudgeUtils.test.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
shouldRestartForSingleFile,
|
||||
shouldRestartDevServer,
|
||||
} from "../../src/utils/buildJudge/restartJudgeUtils.js";
|
||||
|
||||
describe("重启判断工具测试", () => {
|
||||
describe("shouldRestartForSingleFile", () => {
|
||||
test("package.json 应该需要重启", () => {
|
||||
expect(shouldRestartForSingleFile("package.json")).toBe(true);
|
||||
});
|
||||
|
||||
test("vite.config.js 应该需要重启", () => {
|
||||
expect(shouldRestartForSingleFile("vite.config.js")).toBe(true);
|
||||
expect(shouldRestartForSingleFile("vite.config.ts")).toBe(true);
|
||||
});
|
||||
|
||||
test("webpack.config.js 应该需要重启", () => {
|
||||
expect(shouldRestartForSingleFile("webpack.config.js")).toBe(true);
|
||||
});
|
||||
|
||||
test(".env 文件应该需要重启", () => {
|
||||
expect(shouldRestartForSingleFile(".env")).toBe(true);
|
||||
expect(shouldRestartForSingleFile(".env.development")).toBe(true);
|
||||
});
|
||||
|
||||
test("index.html 应该需要重启", () => {
|
||||
expect(shouldRestartForSingleFile("index.html")).toBe(true);
|
||||
});
|
||||
|
||||
test("入口文件应该需要重启", () => {
|
||||
expect(shouldRestartForSingleFile("src/main.js")).toBe(true);
|
||||
expect(shouldRestartForSingleFile("src/index.ts")).toBe(true);
|
||||
expect(shouldRestartForSingleFile("main.jsx")).toBe(true);
|
||||
expect(shouldRestartForSingleFile("App.tsx")).toBe(true);
|
||||
});
|
||||
|
||||
test("lock 文件应该需要重启", () => {
|
||||
expect(shouldRestartForSingleFile("package-lock.json")).toBe(true);
|
||||
expect(shouldRestartForSingleFile("yarn.lock")).toBe(true);
|
||||
expect(shouldRestartForSingleFile("pnpm-lock.yaml")).toBe(true);
|
||||
});
|
||||
|
||||
test("普通组件文件不需要重启", () => {
|
||||
expect(shouldRestartForSingleFile("src/components/Button.tsx")).toBe(
|
||||
false
|
||||
);
|
||||
expect(shouldRestartForSingleFile("src/utils/helper.js")).toBe(false);
|
||||
expect(shouldRestartForSingleFile("src/styles/main.css")).toBe(false);
|
||||
});
|
||||
|
||||
test("空字符串不需要重启", () => {
|
||||
expect(shouldRestartForSingleFile("")).toBe(false);
|
||||
});
|
||||
|
||||
test("非字符串参数不需要重启", () => {
|
||||
expect(shouldRestartForSingleFile(null)).toBe(false);
|
||||
expect(shouldRestartForSingleFile(undefined)).toBe(false);
|
||||
expect(shouldRestartForSingleFile(123)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldRestartDevServer", () => {
|
||||
test("包含需要重启的文件应该返回true", () => {
|
||||
const files = [
|
||||
{ name: "src/components/Button.tsx" },
|
||||
{ name: "package.json" },
|
||||
{ name: "src/App.tsx" },
|
||||
];
|
||||
expect(shouldRestartDevServer(files)).toBe(true);
|
||||
});
|
||||
|
||||
test("都是普通文件应该返回false", () => {
|
||||
const files = [
|
||||
{ name: "src/components/Button.tsx" },
|
||||
{ name: "src/utils/helper.js" },
|
||||
{ name: "src/styles/main.css" },
|
||||
];
|
||||
expect(shouldRestartDevServer(files)).toBe(false);
|
||||
});
|
||||
|
||||
test("空数组应该返回false", () => {
|
||||
expect(shouldRestartDevServer([])).toBe(false);
|
||||
});
|
||||
|
||||
test("非数组参数应该返回false", () => {
|
||||
expect(shouldRestartDevServer(null)).toBe(false);
|
||||
expect(shouldRestartDevServer(undefined)).toBe(false);
|
||||
expect(shouldRestartDevServer("test")).toBe(false);
|
||||
});
|
||||
|
||||
test("文件对象缺少name属性应该被忽略", () => {
|
||||
const files = [{ contents: "test" }, { name: "package.json" }];
|
||||
expect(shouldRestartDevServer(files)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
uploadAttachmentFile,
|
||||
} from "../../src/utils/project/uploadAttachmentFileUtils.js";
|
||||
import {
|
||||
ValidationError,
|
||||
} from "../../src/utils/error/errorHandler.js";
|
||||
|
||||
describe("uploadAttachmentFileUtils", () => {
|
||||
describe("参数验证测试", () => {
|
||||
test("项目ID为空应该抛出 ValidationError", async () => {
|
||||
const mockFile = {
|
||||
originalname: "test.txt",
|
||||
path: "/tmp/test.txt",
|
||||
size: 4,
|
||||
};
|
||||
|
||||
await expect(uploadAttachmentFile("", mockFile)).rejects.toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test("文件对象为空应该抛出 ValidationError", async () => {
|
||||
await expect(uploadAttachmentFile("test-project", null)).rejects.toThrow(ValidationError);
|
||||
});
|
||||
|
||||
test("文件路径为空应该抛出 ValidationError", async () => {
|
||||
const mockFile = {
|
||||
originalname: "test.txt",
|
||||
path: "",
|
||||
size: 4,
|
||||
};
|
||||
|
||||
await expect(uploadAttachmentFile("test-project", mockFile)).rejects.toThrow(ValidationError);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user