Files
eSIM-Tools/tests/modules/server-logger.test.js
Abner 68e931b901 ♻️ refactor: 将日志输出格式从 JSON 重构为人类可读的字符串
- 修改 `server-logger.js` 核心逻辑,将日志输出从 `JSON.stringify` 改为模板字符串拼接,格式变更为 `[LEVEL] [function] [requestId] message | key=value`
- 同步更新 `bff-proxy.js` 和 `markdown-negotiation.js` 中的 Deno 环境内联日志函数,保持 Edge Function 与 Node.js 环境日志格式一致
- 调整 context 序列化方式,使用 `key=value` 空格分隔,无 context 时不输出分隔符 `|`,并移除 `timestamp` 字段
- 更新 `server-logger.test.js` 测试用例,断言日志输出为字符串匹配而非 JSON 解析,并增加单行输出及无 context 时不包含分隔符的验证
2026-06-26 21:48:03 +08:00

188 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* server-logger 模块单元测试
* 覆盖日志输出格式、日志级别控制、工厂函数等核心功能
*
* 输出格式: [LEVEL] [function] [requestId] message | key=value key=value
*/
'use strict';
const { createLogger, parseLogLevel, LOG_LEVELS } = require('../../netlify/functions/_shared/server-logger');
describe('server-logger', () => {
let consoleSpy;
beforeEach(() => {
consoleSpy = {
log: jest.spyOn(console, 'log').mockImplementation(),
warn: jest.spyOn(console, 'warn').mockImplementation(),
error: jest.spyOn(console, 'error').mockImplementation(),
};
});
afterEach(() => {
Object.values(consoleSpy).forEach(spy => spy.mockRestore());
});
describe('createLogger', () => {
it('应该返回具有 info/warn/error/debug 方法的 logger 实例', () => {
const logger = createLogger('test-fn', 'req-1');
expect(typeof logger.info).toBe('function');
expect(typeof logger.warn).toBe('function');
expect(typeof logger.error).toBe('function');
expect(typeof logger.debug).toBe('function');
});
it('应该输出人类可读格式的日志', () => {
const logger = createLogger('test-fn', 'req-1');
logger.info('test message');
expect(consoleSpy.log).toHaveBeenCalledTimes(1);
const output = consoleSpy.log.mock.calls[0][0];
expect(output).toBe('[INFO] [test-fn] [req-1] test message');
});
it('应该支持附加 context 字段key=value 格式)', () => {
const logger = createLogger('test-fn', 'req-1');
logger.info('with context', { status: 200, duration: 42 });
const output = consoleSpy.log.mock.calls[0][0];
expect(output).toContain('[INFO] [test-fn] [req-1] with context');
expect(output).toContain('status=200');
expect(output).toContain('duration=42');
expect(output).toContain(' | ');
});
it('无 context 时不应包含分隔符 |', () => {
const logger = createLogger('test-fn', 'req-1');
logger.info('no context');
const output = consoleSpy.log.mock.calls[0][0];
expect(output).not.toContain(' | ');
});
it('context 为 null 或 undefined 时不应报错', () => {
const logger = createLogger('test-fn', 'req-1');
expect(() => logger.info('msg', null)).not.toThrow();
expect(() => logger.info('msg', undefined)).not.toThrow();
expect(() => logger.warn('msg', null)).not.toThrow();
expect(() => logger.error('msg', null)).not.toThrow();
const output = consoleSpy.log.mock.calls[0][0];
expect(output).toContain('[INFO] [test-fn] [req-1] msg');
});
it('warn 应该输出到 console.warn', () => {
const logger = createLogger('test-fn', 'req-1');
logger.warn('warning msg');
expect(consoleSpy.warn).toHaveBeenCalledTimes(1);
const output = consoleSpy.warn.mock.calls[0][0];
expect(output).toContain('[WARN]');
expect(output).toContain('warning msg');
});
it('error 应该输出到 console.error', () => {
const logger = createLogger('test-fn', 'req-1');
logger.error('error msg', { stack: 'fake stack' });
expect(consoleSpy.error).toHaveBeenCalledTimes(1);
const output = consoleSpy.error.mock.calls[0][0];
expect(output).toContain('[ERROR]');
expect(output).toContain('error msg');
expect(output).toContain('stack=fake stack');
});
it('DEBUG 日志在非 DEBUG 环境下应该被抑制', () => {
const originalLevel = process.env.LOG_LEVEL;
delete process.env.LOG_LEVEL;
const logger = createLogger('test-fn', 'req-1');
logger.debug('debug msg');
expect(consoleSpy.log).not.toHaveBeenCalled();
if (originalLevel) process.env.LOG_LEVEL = originalLevel;
});
it('DEBUG 日志在 LOG_LEVEL=DEBUG 时应该输出', () => {
const originalLevel = process.env.LOG_LEVEL;
process.env.LOG_LEVEL = 'DEBUG';
const logger = createLogger('test-fn', 'req-1');
logger.debug('debug msg');
const output = consoleSpy.log.mock.calls[0][0];
expect(output).toContain('[DEBUG]');
expect(output).toContain('debug msg');
if (originalLevel) process.env.LOG_LEVEL = originalLevel;
else delete process.env.LOG_LEVEL;
});
it('LOG_LEVEL=ERROR 时应该抑制 INFO 和 WARN', () => {
const originalLevel = process.env.LOG_LEVEL;
process.env.LOG_LEVEL = 'ERROR';
const logger = createLogger('test-fn', 'req-1');
logger.info('should not appear');
logger.warn('should not appear');
expect(consoleSpy.log).not.toHaveBeenCalled();
expect(consoleSpy.warn).not.toHaveBeenCalled();
// ERROR 仍然输出
logger.error('should appear');
expect(consoleSpy.error).toHaveBeenCalledTimes(1);
if (originalLevel) process.env.LOG_LEVEL = originalLevel;
else delete process.env.LOG_LEVEL;
});
it('LOG_LEVEL=WARN 时应该抑制 INFO 和 DEBUG', () => {
const originalLevel = process.env.LOG_LEVEL;
process.env.LOG_LEVEL = 'WARN';
const logger = createLogger('test-fn', 'req-1');
logger.info('should not appear');
logger.debug('should not appear');
expect(consoleSpy.log).not.toHaveBeenCalled();
// WARN 和 ERROR 仍然输出
logger.warn('should appear');
logger.error('should appear');
expect(consoleSpy.warn).toHaveBeenCalledTimes(1);
expect(consoleSpy.error).toHaveBeenCalledTimes(1);
if (originalLevel) process.env.LOG_LEVEL = originalLevel;
else delete process.env.LOG_LEVEL;
});
it('多个 logger 实例应该互不干扰', () => {
const logger1 = createLogger('fn-a', 'req-1');
const logger2 = createLogger('fn-b', 'req-2');
logger1.info('from a');
logger2.info('from b');
const out1 = consoleSpy.log.mock.calls[0][0];
const out2 = consoleSpy.log.mock.calls[1][0];
expect(out1).toContain('[fn-a]');
expect(out1).toContain('[req-1]');
expect(out2).toContain('[fn-b]');
expect(out2).toContain('[req-2]');
});
it('日志格式应该方便用户复制(一行一条)', () => {
const logger = createLogger('giffgaff-graphql', 'abc-123');
logger.info('calling_upstream', { operationName: 'getESims', isSwap: false });
const output = consoleSpy.log.mock.calls[0][0];
// 确认是单行、无换行符、包含所有关键信息
expect(output).not.toContain('\n');
expect(output).toBe('[INFO] [giffgaff-graphql] [abc-123] calling_upstream | operationName=getESims isSwap=false');
});
});
describe('parseLogLevel', () => {
it('应该解析有效日志级别', () => {
expect(parseLogLevel('DEBUG')).toBe(0);
expect(parseLogLevel('INFO')).toBe(1);
expect(parseLogLevel('WARN')).toBe(2);
expect(parseLogLevel('ERROR')).toBe(3);
});
it('未知级别应该默认为 INFO', () => {
expect(parseLogLevel('UNKNOWN')).toBe(1);
});
it('undefined 应该默认为 INFO', () => {
expect(parseLogLevel(undefined)).toBe(1);
});
});
describe('LOG_LEVELS', () => {
it('应该包含四个标准级别', () => {
expect(LOG_LEVELS).toEqual({ DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 });
});
});
});