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}`); } }); });