Checks / verify (push) Failing after 9m52s
统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
43 lines
3.2 KiB
TypeScript
43 lines
3.2 KiB
TypeScript
import {test} from "node:test"
|
|
import assert from "node:assert/strict"
|
|
import {ApiError,createClient,normalizeBaseURL} from "../src/lib/api.ts"
|
|
test("API URL does not carry credentials or route state",()=>{
|
|
assert.equal(normalizeBaseURL(" https://api.example.com/ "),"https://api.example.com")
|
|
for(const value of ["","ftp://api.example.com","https://user:secret@api.example.com","https://api.example.com?token=secret","https://api.example.com/path"]){assert.throws(()=>normalizeBaseURL(value),ApiError)}
|
|
})
|
|
test("App uses its bearer token and the server confirmed envelope",async()=>{
|
|
let observed:RequestInit|undefined
|
|
const transport:typeof fetch=async(input,options)=>{assert.equal(input,"https://api.example.com/api/v1/accounts");observed=options;return new Response(JSON.stringify({data:[{cash_balance:"100000.01"}],meta:{request_id:"test"}}),{status:200})}
|
|
const client=createClient("https://api.example.com","app-token",undefined,transport)
|
|
const result=await client<Array<{cash_balance:string}>>("/api/v1/accounts",{method:"POST",body:{mode:"simulation",initial_cash:"100000.01"}})
|
|
assert.equal(result[0].cash_balance,"100000.01")
|
|
assert.equal(new Headers(observed?.headers).get("Authorization"),"Bearer app-token")
|
|
assert.equal(observed?.credentials,"omit");assert.equal(observed?.redirect,"error")
|
|
assert.equal(JSON.parse(observed?.body as string).initial_cash,"100000.01")
|
|
})
|
|
test("401 expires the session, infrastructure failures do not",async()=>{
|
|
let expired=0
|
|
for(const status of [503,401]){
|
|
const client=createClient("https://api.example.com","token",()=>expired++,async()=>new Response(JSON.stringify({error:{code:"TEST_FAILURE",message:"服务错误"}}),{status}))
|
|
await assert.rejects(()=>client("/api/v1/me"),(e:unknown)=>e instanceof ApiError&&e.status===status)
|
|
}
|
|
assert.equal(expired,1)
|
|
})
|
|
test("stale responses cannot expire or populate a newer session",async()=>{
|
|
let expired=0
|
|
const client=createClient("https://api.example.com","previous-token",()=>expired++,async()=>new Response(JSON.stringify({data:{id:"old-user"}}),{status:200}),()=>false)
|
|
await assert.rejects(()=>client("/api/v1/me"),(e:unknown)=>e instanceof ApiError&&e.code==="SESSION_CHANGED")
|
|
assert.equal(expired,0)
|
|
})
|
|
test("broken proxy response and private transport errors are sanitized",async()=>{
|
|
const client=createClient("https://api.example.com",undefined,undefined,async()=>new Response("<html>proxy</html>",{status:502}))
|
|
await assert.rejects(()=>client("/api/v1/me"),(e:unknown)=>e instanceof ApiError&&e.code==="INVALID_RESPONSE")
|
|
const failed=createClient("https://api.example.com",undefined,undefined,async()=>{throw new Error("private request details")})
|
|
await assert.rejects(()=>failed("/api/v1/me"),(e:unknown)=>e instanceof ApiError&&e.code==="NETWORK_ERROR"&&!e.message.includes("private"))
|
|
})
|
|
test("caller cancellation stays distinguishable from a network failure",async()=>{
|
|
const controller=new AbortController();controller.abort()
|
|
const transport:typeof fetch=async(_url,options)=>{assert.equal(options?.signal?.aborted,true);throw new DOMException("Aborted","AbortError")}
|
|
await assert.rejects(()=>createClient("https://api.example.com",undefined,undefined,transport)("/api/v1/accounts",{signal:controller.signal}),(e:unknown)=>e instanceof DOMException&&e.name==="AbortError")
|
|
})
|