Files
order_site/apps/backend/src/utils/logger.test.ts
T

185 lines
5.7 KiB
TypeScript

import test from 'node:test'
import assert from 'node:assert/strict'
import {
formatLogEntry,
formatLogTimestamp,
normalizeLogLevel,
resolveExpiredLogFilenames,
resolveLogFilePath,
shouldWriteLog,
} from './logger.js'
test('normalizeLogLevel falls back to info for unsupported values', () => {
assert.equal(normalizeLogLevel('debug'), 'debug')
assert.equal(normalizeLogLevel('WARN'), 'warn')
assert.equal(normalizeLogLevel('verbose'), 'info')
assert.equal(normalizeLogLevel(''), 'info')
})
test('shouldWriteLog respects configured minimum log level', () => {
assert.equal(shouldWriteLog('debug', 'debug'), true)
assert.equal(shouldWriteLog('info', 'debug'), true)
assert.equal(shouldWriteLog('debug', 'info'), false)
assert.equal(shouldWriteLog('info', 'warn'), false)
assert.equal(shouldWriteLog('warn', 'warn'), true)
assert.equal(shouldWriteLog('error', 'warn'), true)
})
test('shouldWriteLog at warn filters info-style integration noise', () => {
// 与默认 LOG_INTEGRATION_LEVEL=warn 行为一致
assert.equal(shouldWriteLog('info', 'warn'), false)
assert.equal(shouldWriteLog('warn', 'warn'), true)
assert.equal(shouldWriteLog('error', 'warn'), true)
})
test('resolveLogFilePath uses daily log file names by channel', () => {
assert.match(resolveLogFilePath('app', '2026-05-28T00:00:19.194+08:00'), /app-2026-05-28\.log$/)
assert.match(
resolveLogFilePath('integration', '2026-05-28T00:00:19.194+08:00'),
/integration-2026-05-28\.log$/,
)
})
test('resolveLogFilePath uses local date for utc timestamps', () => {
assert.match(resolveLogFilePath('app', '2026-05-27T16:00:19.194Z'), /app-2026-05-28\.log$/)
})
test('resolveExpiredLogFilenames keeps latest thirty days by default and ignores unknown files', () => {
const expired = resolveExpiredLogFilenames(
[
'app-2026-05-30.log',
'app-2026-05-01.log',
'app-2026-04-30.log',
'integration-2026-04-29.log',
'app.log',
'integration.log',
'random.txt',
],
'2026-05-30',
)
assert.deepEqual(expired, [
'app-2026-04-30.log',
'integration-2026-04-29.log',
'app.log',
'integration.log',
])
})
test('resolveExpiredLogFilenames supports explicit retention days', () => {
const expired = resolveExpiredLogFilenames(
['app-2026-04-14.log', 'app-2026-04-08.log', 'app-2026-04-07.log'],
'2026-04-14',
7,
)
assert.deepEqual(expired, ['app-2026-04-07.log'])
})
test('resolveExpiredLogFilenames includes rotated files', () => {
const expired = resolveExpiredLogFilenames(
['app-2026-04-14.1.log', 'app-2026-04-07.5.log', 'app-2026-04-14.log'],
'2026-04-14',
7,
)
assert.deepEqual(expired, ['app-2026-04-07.5.log'])
})
test('formatLogTimestamp renders readable local timestamp with timezone offset', () => {
const rendered = formatLogTimestamp('2026-04-14T10:36:24.974Z')
assert.match(rendered, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [+-]\d{2}:\d{2}$/)
})
test('formatLogEntry prints readable one-line output for flat details', () => {
const text = formatLogEntry(
{
time: '2026-04-14T10:36:24.974Z',
level: 'info',
scope: '[http/access]',
message: 'request completed',
pid: 4671,
detail: {
requestId: 'req-123',
method: 'GET',
statusCode: 401,
},
},
{ color: false },
)
assert.match(text, /INFO\s+\[http\/access\] request completed/)
assert.match(text, /pid=4671/)
assert.match(text, /requestId=req-123/)
assert.match(text, /method=GET/)
assert.match(text, /statusCode=401/)
assert.doesNotMatch(text, /^\{/)
})
test('formatLogEntry prints nested detail blocks without ansi colors in file mode', () => {
const text = formatLogEntry(
{
time: '2026-04-14T10:36:24.974Z',
level: 'warn',
scope: '[admin/auth]',
message: '后台鉴权失败',
pid: 4671,
detail: {
statusCode: 401,
errorCode: 'admin_auth_required',
error: {
message: '未登录或登录已失效',
stack: 'Error: test',
},
},
},
{ color: false },
)
assert.match(text, /WARN\s+\[admin\/auth\] 后台鉴权失败/)
assert.match(text, /statusCode=401/)
assert.match(text, /errorCode=admin_auth_required/)
assert.match(text, /\n \{/)
assert.match(text, /未登录或登录已失效/)
assert.doesNotMatch(text, /\u001B\[/)
})
test('formatLogEntry masks sensitive fields in inline and nested details', () => {
const text = formatLogEntry(
{
time: '2026-04-14T10:36:24.974Z',
level: 'info',
scope: '[security]',
message: 'masked',
pid: 4671,
detail: {
token: 'abcdef1234567890',
authorization: 'Bearer abcdef1234567890',
sign: '0123456789abcdef',
jumpLink:
'https://feifei.example.com/h5/bind?code=very-secret-code&token=very-secret-token',
rawText:
'{"token":"raw-json-token","sign":"raw-json-sign","h5":{"recharge_url":"https://feifei.example.com/h5/bind?code=raw-secret-code"}}',
nested: {
cookie: 'sessionid=abcdef1234567890',
note: 'password=super-secret-value',
},
},
},
{ color: false },
)
assert.doesNotMatch(text, /abcdef1234567890/)
assert.doesNotMatch(text, /super-secret-value/)
assert.doesNotMatch(text, /0123456789abcdef/)
assert.doesNotMatch(text, /very-secret-code/)
assert.doesNotMatch(text, /very-secret-token/)
assert.doesNotMatch(text, /raw-json-token/)
assert.doesNotMatch(text, /raw-json-sign/)
assert.doesNotMatch(text, /raw-secret-code/)
assert.match(text, /abcdef\*\*\*\*567890/)
assert.match(text, /Bearer\*\*\*\*567890/)
assert.match(text, /password=super-\*\*\*\*-value/)
})