httpClient.contract.test.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. /**
  2. * T005: 契约测试 IUniversalHttpClient.request()
  3. *
  4. * 这个测试将验证 IUniversalHttpClient.request() 方法的契约是否正确实现
  5. *
  6. * 重要:按照TDD原则,这个测试现在必须失败,因为实现还不存在
  7. */
  8. import { describe, test, expect, beforeEach, afterEach } from '@jest/globals'
  9. // 导入接口定义(这些应该在contracts中定义的类型)
  10. interface IUniversalHttpClient {
  11. request<T = any>(request: HttpClientRequest): Promise<HttpClientResponse<T>>
  12. batchRequest<T = any>(requests: HttpClientRequest[]): Promise<HttpClientResponse<T>[]>
  13. registerPlatform(platform: string, adapter: IPlatformAdapter): void
  14. getHealth(): Promise<HealthStatus>
  15. close(): Promise<void>
  16. }
  17. interface HttpClientRequest {
  18. platform: string
  19. accountId: string
  20. method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
  21. url: string
  22. headers?: Record<string, string>
  23. body?: any
  24. options?: RequestOptions
  25. }
  26. interface RequestOptions {
  27. timeout?: TimeoutConfig
  28. retry?: RetryConfig
  29. proxy?: ProxyControlOptions
  30. logSensitiveData?: boolean
  31. idempotencyKey?: string
  32. }
  33. interface TimeoutConfig {
  34. connect?: number
  35. read?: number
  36. write?: number
  37. }
  38. interface RetryConfig {
  39. maxAttempts?: number
  40. delay?: number
  41. exponentialBackoff?: boolean
  42. shouldRetry?: (error: any) => boolean
  43. }
  44. interface ProxyControlOptions {
  45. enabled?: boolean
  46. forceProxy?: any
  47. disableProxy?: boolean
  48. strategy?: 'global' | 'account' | 'force' | 'disabled'
  49. }
  50. interface HttpClientResponse<T = any> {
  51. status: number
  52. statusText: string
  53. ok: boolean
  54. data: T
  55. headers: Record<string, string>
  56. metadata: ResponseMetadata
  57. }
  58. interface ResponseMetadata {
  59. requestId: string
  60. duration: number
  61. retryCount: number
  62. usedProxy: boolean
  63. proxyUsed?: string
  64. timestamp: Date
  65. platform: string
  66. }
  67. interface IPlatformAdapter {
  68. readonly platform: string
  69. readonly baseUrl: string
  70. request<T = any>(request: any): Promise<any>
  71. }
  72. interface HealthStatus {
  73. status: 'healthy' | 'degraded' | 'unhealthy'
  74. platforms: Record<string, any>
  75. metrics: any
  76. }
  77. // 导入实现(这应该会失败,因为实现还不存在)
  78. let UniversalHttpClient: any
  79. try {
  80. // 尝试从现有的HTTP客户端库导入
  81. UniversalHttpClient = require('../../libs/http-client/src/index.js').UniversalHttpClient
  82. } catch (error) {
  83. // 如果从库导入失败,尝试从src目录导入
  84. try {
  85. UniversalHttpClient = require('../../src/utils/universalHttpClient.js').UniversalHttpClient
  86. } catch (error2) {
  87. // 预期的失败:实现还不存在
  88. UniversalHttpClient = undefined
  89. }
  90. }
  91. describe('IUniversalHttpClient.request() 契约测试', () => {
  92. let httpClient: IUniversalHttpClient
  93. let mockPlatformAdapter: IPlatformAdapter
  94. beforeEach(() => {
  95. // 创建模拟平台适配器
  96. mockPlatformAdapter = {
  97. platform: 'test-platform',
  98. baseUrl: 'https://api.test-platform.com',
  99. request: jest.fn().mockResolvedValue({
  100. status: 200,
  101. data: { message: 'success' },
  102. headers: { 'content-type': 'application/json' },
  103. metadata: {
  104. platform: 'test-platform',
  105. requestId: 'test-123',
  106. serverTime: new Date(),
  107. rateLimit: undefined,
  108. platformData: {}
  109. }
  110. })
  111. }
  112. // 如果实现存在,创建客户端实例
  113. if (UniversalHttpClient) {
  114. httpClient = new UniversalHttpClient()
  115. httpClient.registerPlatform('test-platform', mockPlatformAdapter)
  116. }
  117. })
  118. afterEach(async () => {
  119. if (httpClient && httpClient.close) {
  120. await httpClient.close()
  121. }
  122. })
  123. test('应该存在 UniversalHttpClient 类', () => {
  124. // 这个测试现在应该失败
  125. expect(UniversalHttpClient).toBeDefined()
  126. expect(typeof UniversalHttpClient).toBe('function')
  127. })
  128. test('应该实现 IUniversalHttpClient 接口', () => {
  129. if (!UniversalHttpClient) {
  130. expect(UniversalHttpClient).toBeDefined() // 强制失败
  131. return
  132. }
  133. const client = new UniversalHttpClient()
  134. // 验证必需方法存在
  135. expect(typeof client.request).toBe('function')
  136. expect(typeof client.batchRequest).toBe('function')
  137. expect(typeof client.registerPlatform).toBe('function')
  138. expect(typeof client.getHealth).toBe('function')
  139. expect(typeof client.close).toBe('function')
  140. })
  141. test('request() 应该处理基本的GET请求', async () => {
  142. if (!httpClient) {
  143. expect(httpClient).toBeDefined() // 强制失败
  144. return
  145. }
  146. const request: HttpClientRequest = {
  147. platform: 'test-platform',
  148. accountId: 'test-account',
  149. method: 'GET',
  150. url: '/api/v1/test'
  151. }
  152. const response = await httpClient.request(request)
  153. // 验证响应结构符合契约
  154. expect(response).toHaveProperty('status')
  155. expect(response).toHaveProperty('statusText')
  156. expect(response).toHaveProperty('ok')
  157. expect(response).toHaveProperty('data')
  158. expect(response).toHaveProperty('headers')
  159. expect(response).toHaveProperty('metadata')
  160. // 验证元数据结构
  161. expect(response.metadata).toHaveProperty('requestId')
  162. expect(response.metadata).toHaveProperty('duration')
  163. expect(response.metadata).toHaveProperty('retryCount')
  164. expect(response.metadata).toHaveProperty('usedProxy')
  165. expect(response.metadata).toHaveProperty('timestamp')
  166. expect(response.metadata).toHaveProperty('platform')
  167. expect(response.metadata.platform).toBe('test-platform')
  168. })
  169. test('request() 应该处理带请求体的POST请求', async () => {
  170. if (!httpClient) {
  171. expect(httpClient).toBeDefined() // 强制失败
  172. return
  173. }
  174. const request: HttpClientRequest = {
  175. platform: 'test-platform',
  176. accountId: 'test-account',
  177. method: 'POST',
  178. url: '/api/v1/orders',
  179. headers: {
  180. 'Content-Type': 'application/json'
  181. },
  182. body: {
  183. symbol: 'BTC-USD',
  184. side: 'buy',
  185. amount: 0.001
  186. }
  187. }
  188. const response = await httpClient.request(request)
  189. expect(response.status).toBe(200)
  190. expect(response.ok).toBe(true)
  191. expect(response.data).toBeDefined()
  192. })
  193. test('request() 应该支持超时配置', async () => {
  194. if (!httpClient) {
  195. expect(httpClient).toBeDefined() // 强制失败
  196. return
  197. }
  198. const request: HttpClientRequest = {
  199. platform: 'test-platform',
  200. accountId: 'test-account',
  201. method: 'GET',
  202. url: '/api/v1/slow-endpoint',
  203. options: {
  204. timeout: {
  205. connect: 5000,
  206. read: 30000,
  207. write: 15000
  208. }
  209. }
  210. }
  211. // 这应该不会抛出错误(模拟快速响应)
  212. const response = await httpClient.request(request)
  213. expect(response.status).toBe(200)
  214. })
  215. test('request() 应该支持重试配置', async () => {
  216. if (!httpClient) {
  217. expect(httpClient).toBeDefined() // 强制失败
  218. return
  219. }
  220. const request: HttpClientRequest = {
  221. platform: 'test-platform',
  222. accountId: 'test-account',
  223. method: 'GET',
  224. url: '/api/v1/test',
  225. options: {
  226. retry: {
  227. maxAttempts: 3,
  228. delay: 1000,
  229. exponentialBackoff: true
  230. }
  231. }
  232. }
  233. const response = await httpClient.request(request)
  234. expect(response.metadata.retryCount).toBeGreaterThanOrEqual(0)
  235. })
  236. test('request() 应该支持代理控制选项', async () => {
  237. if (!httpClient) {
  238. expect(httpClient).toBeDefined() // 强制失败
  239. return
  240. }
  241. const request: HttpClientRequest = {
  242. platform: 'test-platform',
  243. accountId: 'test-account',
  244. method: 'GET',
  245. url: '/api/v1/test',
  246. options: {
  247. proxy: {
  248. enabled: true,
  249. strategy: 'global'
  250. }
  251. }
  252. }
  253. const response = await httpClient.request(request)
  254. expect(response.metadata).toHaveProperty('usedProxy')
  255. })
  256. test('request() 应该支持幂等性键', async () => {
  257. if (!httpClient) {
  258. expect(httpClient).toBeDefined() // 强制失败
  259. return
  260. }
  261. const idempotencyKey = 'test-key-' + Date.now()
  262. const request: HttpClientRequest = {
  263. platform: 'test-platform',
  264. accountId: 'test-account',
  265. method: 'POST',
  266. url: '/api/v1/orders',
  267. body: { symbol: 'BTC-USD' },
  268. options: {
  269. idempotencyKey
  270. }
  271. }
  272. const response = await httpClient.request(request)
  273. expect(response.status).toBe(200)
  274. })
  275. test('request() 应该在未注册平台时抛出错误', async () => {
  276. if (!httpClient) {
  277. expect(httpClient).toBeDefined() // 强制失败
  278. return
  279. }
  280. const request: HttpClientRequest = {
  281. platform: 'unknown-platform',
  282. accountId: 'test-account',
  283. method: 'GET',
  284. url: '/api/v1/test'
  285. }
  286. await expect(httpClient.request(request)).rejects.toThrow()
  287. })
  288. test('request() 应该在无效请求时抛出错误', async () => {
  289. if (!httpClient) {
  290. expect(httpClient).toBeDefined() // 强制失败
  291. return
  292. }
  293. const invalidRequest = {
  294. platform: 'test-platform',
  295. // 缺少必需字段
  296. method: 'GET'
  297. } as HttpClientRequest
  298. await expect(httpClient.request(invalidRequest)).rejects.toThrow()
  299. })
  300. test('request() 响应应该包含正确的状态码和数据类型', async () => {
  301. if (!httpClient) {
  302. expect(httpClient).toBeDefined() // 强制失败
  303. return
  304. }
  305. const request: HttpClientRequest = {
  306. platform: 'test-platform',
  307. accountId: 'test-account',
  308. method: 'GET',
  309. url: '/api/v1/test'
  310. }
  311. const response = await httpClient.request(request)
  312. expect(typeof response.status).toBe('number')
  313. expect(typeof response.statusText).toBe('string')
  314. expect(typeof response.ok).toBe('boolean')
  315. expect(response.headers).toEqual(expect.any(Object))
  316. expect(response.metadata.timestamp).toBeInstanceOf(Date)
  317. expect(typeof response.metadata.duration).toBe('number')
  318. })
  319. })