import assert from 'node:assert/strict' import { apiRequest } from '../src/services/api.js' async function testUsesCustomContentTypeHeader() { let capturedOptions = null global.fetch = async (_url, options) => { capturedOptions = options return { ok: true, async json() { return { ok: true } } } } await apiRequest('/knowledge/documents', { method: 'POST', body: 'payload', contentType: 'application/octet-stream' }) assert.equal(capturedOptions.headers['Content-Type'], 'application/octet-stream') } async function testSupportsBlobResponses() { const blob = new Blob(['preview']) global.fetch = async () => ({ ok: true, async blob() { return blob }, async json() { throw new Error('json parser should not be used for blob responses') } }) const payload = await apiRequest('/knowledge/documents/demo/content', { responseType: 'blob', contentType: null }) assert.equal(payload, blob) } async function testInjectsBearerTokenWithoutUserControlledIdentityHeaders() { const sessionStorage = new Map([ ['x-financial-auth-access-token', 'opaque-access-token'], ['x-financial-auth-expires-at', '2099-01-01T00:00:00.000Z'], [ 'x-financial-auth-user', JSON.stringify({ username: 'admin', name: 'Admin User', employeePosition: 'System Manager', employeeGrade: 'M5', employeeNo: 'E-001', managerName: 'Approver User', roleCodes: ['manager'], isAdmin: true }) ] ]) global.window = { sessionStorage: { getItem(key) { return sessionStorage.get(key) ?? null } } } let capturedOptions = null global.fetch = async (_url, options) => { capturedOptions = options return { ok: true, async json() { return { ok: true } } } } await apiRequest('/knowledge/library') assert.equal(capturedOptions.headers.Authorization, 'Bearer opaque-access-token') assert.equal(capturedOptions.headers['x-auth-username'], undefined) assert.equal(capturedOptions.headers['x-auth-role-codes'], undefined) assert.equal(capturedOptions.headers['x-auth-is-admin'], undefined) } async function testLoginCanDisableBearerInjection() { const sessionStorage = new Map([ ['x-financial-auth-access-token', 'stale-token'], ['x-financial-auth-expires-at', '2099-01-01T00:00:00.000Z'] ]) global.window = { sessionStorage: { getItem(key) { return sessionStorage.get(key) ?? null } } } let capturedOptions = null global.fetch = async (_url, options) => { capturedOptions = options return { ok: true, async json() { return { ok: true } } } } await apiRequest('/auth/login', { auth: false, handleUnauthorized: false, method: 'POST', body: '{}' }) assert.equal(capturedOptions.headers.Authorization, undefined) } async function testRejectsCustomAuthorizationOverride() { const sessionStorage = new Map([ ['x-financial-auth-access-token', 'server-issued-token'], ['x-financial-auth-expires-at', '2099-01-01T00:00:00.000Z'] ]) global.window = { sessionStorage: { getItem(key) { return sessionStorage.get(key) ?? null } } } let capturedOptions = null global.fetch = async (_url, options) => { capturedOptions = options return { ok: true, async json() { return { ok: true } } } } await apiRequest('/knowledge/library', { headers: { Authorization: 'Bearer attacker-token' } }) assert.equal(capturedOptions.headers.Authorization, 'Bearer server-issued-token') } async function testUnauthorizedResponsePublishesSessionExpiredEvent() { const sessionStorage = new Map([ ['x-financial-auth-access-token', 'expired-token'], ['x-financial-auth-expires-at', '2099-01-01T00:00:00.000Z'] ]) const events = [] global.window = { sessionStorage: { getItem(key) { return sessionStorage.get(key) ?? null } }, dispatchEvent(event) { events.push(event.type) } } global.fetch = async () => ({ ok: false, status: 401, async json() { return { detail: '登录会话已失效。' } } }) await assert.rejects( () => apiRequest('/auth/me'), (error) => { assert.equal(error.status, 401) assert.equal(error.code, 'AUTH_SESSION_EXPIRED') return true } ) assert.deepEqual(events, ['x-financial:auth-expired']) } async function testFormatsValidationErrors() { global.fetch = async () => ({ ok: false, async json() { return { detail: [ { loc: ['body', 'email'], msg: 'value is not a valid email address' }, { loc: ['body', 'password'], msg: 'String should have at least 5 characters' } ] } } }) await assert.rejects( () => apiRequest('/employees/demo', { method: 'PATCH', body: '{}' }), (error) => { assert.equal( error.message, 'email: value is not a valid email address;password: String should have at least 5 characters' ) return true } ) } async function testRejectsWithCustomTimeoutMessage() { global.fetch = async (_url, options) => new Promise((_, reject) => { options.signal.addEventListener('abort', () => { const error = new Error('aborted') error.name = 'AbortError' reject(error) }) }) await assert.rejects( () => apiRequest('/knowledge/library', { timeoutMs: 1, timeoutMessage: '知识问答整理超时,已停止等待。' }), (error) => { assert.equal(error.message, '知识问答整理超时,已停止等待。') assert.equal(error.code, 'REQUEST_TIMEOUT') return true } ) } async function testPreservesStructuredPreReviewConflict() { const review = { review_id: 'review-conflict-001', decision: 'needs_fix', message: '住宿费超过标准,请先调整。', findings: [{ risk_id: 'risk-001', severity: 'high' }] } global.fetch = async () => ({ ok: false, status: 409, async json() { return { detail: { code: 'PRE_REVIEW_NEEDS_FIX', message: '预审发现需整改风险。', review } } } }) await assert.rejects( () => apiRequest('/reimbursements/claims/claim-001/submit', { method: 'POST', body: '{}' }), (error) => { assert.equal(error.status, 409) assert.equal(error.code, 'PRE_REVIEW_NEEDS_FIX') assert.equal(error.message, '预审发现需整改风险。') assert.deepEqual(error.review, review) assert.deepEqual(error.detail.review, review) assert.deepEqual(error.payload.detail.review, review) return true } ) } async function run() { await testUsesCustomContentTypeHeader() await testSupportsBlobResponses() await testInjectsBearerTokenWithoutUserControlledIdentityHeaders() await testLoginCanDisableBearerInjection() await testRejectsCustomAuthorizationOverride() await testUnauthorizedResponsePublishesSessionExpiredEvent() await testFormatsValidationErrors() await testPreservesStructuredPreReviewConflict() await testRejectsWithCustomTimeoutMessage() console.log('api-request tests passed') } run().catch((error) => { console.error(error) process.exit(1) })