Files
X-Financial/web/tests/api-request.test.mjs

301 lines
7.4 KiB
JavaScript
Raw Normal View History

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)
}
2026-07-13 14:45:36 +08:00
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')
2026-07-13 14:45:36 +08:00
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)
}
2026-07-13 14:45:36 +08:00
async function testLoginCanDisableBearerInjection() {
const sessionStorage = new Map([
2026-07-13 14:45:36 +08:00
['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 }
}
}
}
2026-07-13 14:45:36 +08:00
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: '登录会话已失效。' }
}
})
2026-07-13 14:45:36 +08:00
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 addresspassword: 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()
2026-07-13 14:45:36 +08:00
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)
})