2026-07-18 08:35:36 +08:00
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import test from 'node:test'
|
|
|
|
|
import type { AxiosResponse } from 'axios'
|
|
|
|
|
|
2026-07-18 08:59:40 +08:00
|
|
|
import {
|
|
|
|
|
createResponseDiagnostic,
|
|
|
|
|
createSafeApiErrorReport,
|
|
|
|
|
createValidationDiagnostic,
|
|
|
|
|
} from '../src/api/diagnostics'
|
|
|
|
|
import { RequestError } from '../src/api/errors'
|
2026-07-18 08:35:36 +08:00
|
|
|
|
|
|
|
|
function sensitiveResponse(): AxiosResponse<unknown> {
|
|
|
|
|
return {
|
|
|
|
|
config: {
|
|
|
|
|
baseURL: 'https://api.example.com/',
|
|
|
|
|
data: JSON.stringify({ phone: '13800000000' }),
|
|
|
|
|
headers: {
|
|
|
|
|
sessionId: 'secret-session',
|
|
|
|
|
userId: 'secret-user',
|
|
|
|
|
},
|
|
|
|
|
method: 'post',
|
|
|
|
|
url: '/am/example',
|
|
|
|
|
},
|
|
|
|
|
data: { token: 'secret-token' },
|
|
|
|
|
headers: { authorization: 'secret-authorization' },
|
|
|
|
|
status: 200,
|
|
|
|
|
statusText: 'OK',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('API diagnostics only retain non-sensitive request shape', () => {
|
|
|
|
|
const diagnostic = createResponseDiagnostic(sensitiveResponse())
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(diagnostic, {
|
|
|
|
|
httpStatus: 200,
|
|
|
|
|
method: 'POST',
|
|
|
|
|
path: '/am/example',
|
|
|
|
|
type: 'Response',
|
|
|
|
|
})
|
|
|
|
|
const serialized = JSON.stringify(diagnostic)
|
|
|
|
|
assert.doesNotMatch(serialized, /secret|phone|token|authorization|example\.com/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('validation diagnostics retain issue paths without response data', () => {
|
|
|
|
|
const diagnostic = createValidationDiagnostic(sensitiveResponse(), [
|
|
|
|
|
{
|
|
|
|
|
code: 'invalid_type',
|
|
|
|
|
expected: 'string',
|
|
|
|
|
message: 'Invalid input: expected string, received number',
|
|
|
|
|
path: ['data', 'userId'],
|
|
|
|
|
},
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
assert.equal(diagnostic.issues?.[0]?.path, 'data.userId')
|
|
|
|
|
const serialized = JSON.stringify(diagnostic)
|
|
|
|
|
assert.doesNotMatch(serialized, /secret|13800000000|authorization|example\.com/)
|
|
|
|
|
})
|
2026-07-18 08:59:40 +08:00
|
|
|
|
|
|
|
|
test('global API error reports never expose RequestError causes', () => {
|
|
|
|
|
const response = sensitiveResponse()
|
|
|
|
|
const cause = {
|
|
|
|
|
issues: [
|
|
|
|
|
{
|
|
|
|
|
code: 'invalid_type',
|
|
|
|
|
expected: 'string',
|
|
|
|
|
message: 'Invalid input: expected string, received number',
|
|
|
|
|
path: ['data', 'userId'],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
response,
|
|
|
|
|
}
|
|
|
|
|
const report = createSafeApiErrorReport(
|
|
|
|
|
new RequestError('server message may contain private data', 'ValidationError', cause),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert.equal(report.requestType, 'ValidationError')
|
|
|
|
|
assert.equal(report.diagnostic?.type, 'ValidationError')
|
|
|
|
|
const serialized = JSON.stringify(report)
|
|
|
|
|
assert.doesNotMatch(
|
|
|
|
|
serialized,
|
|
|
|
|
/private|secret|13800000000|authorization|example\.com|server message/,
|
|
|
|
|
)
|
|
|
|
|
})
|