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

301 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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()
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)
})