测试基础设施 - 统一 SQLite 测试 DB/临时目录 helper(tests/helpers.ts),支持 Windows EBUSY 重试清理 - 测试库使用 PRAGMA journal_mode=DELETE 避免 WAL 句柄延迟 - 路由 handler 测试改用 createMigratedMemoryTestDatabase 避免 File DB 锁 - SQLite 聚焦 --rerun-each=20 全部通过(720 pass) 后端测试补强 - 新增 tests/server/app.test.ts 真实 startServer 集成测试 - 覆盖 /api/meta、项目 CRUD、错误路径、静态 fallback、安全 header - bootstrap/logger 测试捕获预期输出,消除测试噪音 前端测试补强 - 移除 .ant-* 内部类名依赖,改为角色/文本/导航/请求契约断言 - 项目页补充搜索、Tab 切换、表单、表格操作、错误反馈行为测试 - 新增 hooks(use-theme-preference、use-sidebar-collapsed、use-projects)纯逻辑测试 - 新增 ErrorBoundary 错误展示和刷新按钮测试 - 新增搜索清空行为测试 - 测试 setup 过滤 antd/rc-trigger NaN height warning 产品修复(测试暴露) - 修复 ProjectToolbar 搜索框无法输入(新增 draftKeyword 状态) - 加固 ProjectFormModal 表单字段同步(useEffect 替代不可靠的 afterOpenChange) - 清理 ProjectFormModal 冗余 afterOpenChange 同步逻辑 重构与合规 - ProjectContext 拆分为三文件满足 React Fast Refresh 规则 - use-projects.ts 导出内部 helper 函数供测试验证 - scripts/build.ts 提取纯生成函数供测试使用,修复构建步骤日志编号 - 修复 build 测试覆盖真实生成逻辑 文档同步 - 更新后端/前端/开发文档测试规范、质量门禁和 helper 使用说明
163 lines
4.9 KiB
TypeScript
163 lines
4.9 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
||
|
||
import type { Logger } from "../../src/server/logger";
|
||
|
||
import { createConsoleFallback, createMemoryLogger, createNoopLogger, REDACT_PATHS } from "../../src/server/logger";
|
||
|
||
function captureConsole(callback: () => void): { errors: string[]; logs: string[]; warns: string[] } {
|
||
const originalError = console.error;
|
||
const originalLog = console.log;
|
||
const originalWarn = console.warn;
|
||
const errors: string[] = [];
|
||
const logs: string[] = [];
|
||
const warns: string[] = [];
|
||
|
||
console.error = (...args: unknown[]) => {
|
||
errors.push(args.map(String).join(" "));
|
||
};
|
||
console.log = (...args: unknown[]) => {
|
||
logs.push(args.map(String).join(" "));
|
||
};
|
||
console.warn = (...args: unknown[]) => {
|
||
warns.push(args.map(String).join(" "));
|
||
};
|
||
|
||
try {
|
||
callback();
|
||
} finally {
|
||
console.error = originalError;
|
||
console.log = originalLog;
|
||
console.warn = originalWarn;
|
||
}
|
||
|
||
return { errors, logs, warns };
|
||
}
|
||
|
||
describe("NoopLogger", () => {
|
||
test("所有方法不抛异常", () => {
|
||
const logger = createNoopLogger();
|
||
logger.trace("trace");
|
||
logger.debug("debug");
|
||
logger.info("info");
|
||
logger.warn("warn");
|
||
logger.error("error");
|
||
logger.fatal("fatal");
|
||
logger.flush();
|
||
const child = logger.child({ component: "test" });
|
||
expect(child).toBeDefined();
|
||
});
|
||
});
|
||
|
||
describe("MemoryLogger", () => {
|
||
test("记录所有等级日志", () => {
|
||
const logger = createMemoryLogger();
|
||
logger.trace("trace-msg");
|
||
logger.debug("debug-msg");
|
||
logger.info("info-msg");
|
||
logger.warn("warn-msg");
|
||
logger.error("error-msg");
|
||
logger.fatal("fatal-msg");
|
||
|
||
expect(logger.entries).toHaveLength(6);
|
||
expect(logger.entries[0]).toEqual({ level: "trace", msg: "trace-msg" });
|
||
expect(logger.entries[5]).toEqual({ level: "fatal", msg: "fatal-msg" });
|
||
});
|
||
|
||
test("记录结构化日志", () => {
|
||
const logger = createMemoryLogger();
|
||
logger.info({ matched: true, targetId: "abc" }, "check complete");
|
||
|
||
expect(logger.entries).toHaveLength(1);
|
||
expect(logger.entries[0]!.level).toBe("info");
|
||
expect(logger.entries[0]!.msg).toBe("check complete");
|
||
expect(logger.entries[0]!.obj).toEqual({ matched: true, targetId: "abc" });
|
||
});
|
||
|
||
test("child 返回自身", () => {
|
||
const logger = createMemoryLogger();
|
||
const child = logger.child({ component: "test" });
|
||
child.info("child-msg");
|
||
expect(logger.entries).toHaveLength(1);
|
||
});
|
||
|
||
test("flush 不抛异常", () => {
|
||
const logger = createMemoryLogger();
|
||
logger.flush();
|
||
});
|
||
});
|
||
|
||
describe("ConsoleFallbackLogger", () => {
|
||
test("不抛异常", () => {
|
||
const child = captureConsole(() => {
|
||
const logger = createConsoleFallback();
|
||
logger.trace("trace");
|
||
logger.debug("debug");
|
||
logger.info("info");
|
||
logger.warn("warn");
|
||
logger.error("error");
|
||
logger.fatal("fatal");
|
||
logger.flush();
|
||
return logger.child({ component: "test" });
|
||
});
|
||
expect(child).toBeDefined();
|
||
});
|
||
|
||
test("按等级写入对应 console 通道", () => {
|
||
const output = captureConsole(() => {
|
||
const logger = createConsoleFallback();
|
||
logger.info("info");
|
||
logger.warn("warn");
|
||
logger.error("error");
|
||
logger.fatal("fatal");
|
||
});
|
||
|
||
expect(output.logs).toContain("info");
|
||
expect(output.warns).toContain("warn");
|
||
expect(output.errors).toEqual(["error", "fatal"]);
|
||
});
|
||
});
|
||
|
||
describe("Logger 接口契约", () => {
|
||
function assertLogger(logger: Logger): void {
|
||
logger.trace("trace");
|
||
logger.debug("debug");
|
||
logger.info("info");
|
||
logger.warn("warn");
|
||
logger.error("error");
|
||
logger.fatal("fatal");
|
||
logger.info({ key: "value" }, "structured");
|
||
logger.child({ component: "test" }).info("child");
|
||
logger.flush();
|
||
}
|
||
|
||
test("NoopLogger 满足 Logger 接口", () => {
|
||
expect(() => assertLogger(createNoopLogger())).not.toThrow();
|
||
});
|
||
|
||
test("MemoryLogger 满足 Logger 接口", () => {
|
||
expect(() => assertLogger(createMemoryLogger())).not.toThrow();
|
||
});
|
||
|
||
test("ConsoleFallbackLogger 满足 Logger 接口", () => {
|
||
expect(() => captureConsole(() => assertLogger(createConsoleFallback()))).not.toThrow();
|
||
});
|
||
});
|
||
|
||
describe("redaction 敏感信息保护", () => {
|
||
test("MemoryLogger 不做 redaction(测试用途,仅 Pino 运行时 redact)", () => {
|
||
const logger = createMemoryLogger();
|
||
logger.info({ authorization: "Bearer secret", password: "hunter2" }, "test");
|
||
const entry = logger.entries[0]!;
|
||
expect(entry.obj!["authorization"]).toBe("Bearer secret");
|
||
expect(entry.obj!["password"]).toBe("hunter2");
|
||
});
|
||
|
||
test("REDACT_PATHS 覆盖所有敏感字段键名", () => {
|
||
const sensitiveKeys = ["authorization", "cookie", "set-cookie", "authToken", "key", "password", "token", "apiKey"];
|
||
for (const key of sensitiveKeys) {
|
||
expect(REDACT_PATHS).toContain(key);
|
||
expect(REDACT_PATHS).toContain(`*.${key}`);
|
||
}
|
||
});
|
||
});
|