185 lines
5.7 KiB
TypeScript
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/)
|
|
})
|