123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 |
- /**
- * T005: 契约测试 IUniversalHttpClient.request()
- *
- * 这个测试将验证 IUniversalHttpClient.request() 方法的契约是否正确实现
- *
- * 重要:按照TDD原则,这个测试现在必须失败,因为实现还不存在
- */
- import { describe, test, expect, beforeEach, afterEach } from '@jest/globals'
- // 导入接口定义(这些应该在contracts中定义的类型)
- interface IUniversalHttpClient {
- request<T = any>(request: HttpClientRequest): Promise<HttpClientResponse<T>>
- batchRequest<T = any>(requests: HttpClientRequest[]): Promise<HttpClientResponse<T>[]>
- registerPlatform(platform: string, adapter: IPlatformAdapter): void
- getHealth(): Promise<HealthStatus>
- close(): Promise<void>
- }
- interface HttpClientRequest {
- platform: string
- accountId: string
- method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
- url: string
- headers?: Record<string, string>
- body?: any
- options?: RequestOptions
- }
- interface RequestOptions {
- timeout?: TimeoutConfig
- retry?: RetryConfig
- proxy?: ProxyControlOptions
- logSensitiveData?: boolean
- idempotencyKey?: string
- }
- interface TimeoutConfig {
- connect?: number
- read?: number
- write?: number
- }
- interface RetryConfig {
- maxAttempts?: number
- delay?: number
- exponentialBackoff?: boolean
- shouldRetry?: (error: any) => boolean
- }
- interface ProxyControlOptions {
- enabled?: boolean
- forceProxy?: any
- disableProxy?: boolean
- strategy?: 'global' | 'account' | 'force' | 'disabled'
- }
- interface HttpClientResponse<T = any> {
- status: number
- statusText: string
- ok: boolean
- data: T
- headers: Record<string, string>
- metadata: ResponseMetadata
- }
- interface ResponseMetadata {
- requestId: string
- duration: number
- retryCount: number
- usedProxy: boolean
- proxyUsed?: string
- timestamp: Date
- platform: string
- }
- interface IPlatformAdapter {
- readonly platform: string
- readonly baseUrl: string
- request<T = any>(request: any): Promise<any>
- }
- interface HealthStatus {
- status: 'healthy' | 'degraded' | 'unhealthy'
- platforms: Record<string, any>
- metrics: any
- }
- // 导入实现(这应该会失败,因为实现还不存在)
- let UniversalHttpClient: any
- try {
- // 尝试从现有的HTTP客户端库导入
- UniversalHttpClient = require('../../libs/http-client/src/index.js').UniversalHttpClient
- } catch (error) {
- // 如果从库导入失败,尝试从src目录导入
- try {
- UniversalHttpClient = require('../../src/utils/universalHttpClient.js').UniversalHttpClient
- } catch (error2) {
- // 预期的失败:实现还不存在
- UniversalHttpClient = undefined
- }
- }
- describe('IUniversalHttpClient.request() 契约测试', () => {
- let httpClient: IUniversalHttpClient
- let mockPlatformAdapter: IPlatformAdapter
- beforeEach(() => {
- // 创建模拟平台适配器
- mockPlatformAdapter = {
- platform: 'test-platform',
- baseUrl: 'https://api.test-platform.com',
- request: jest.fn().mockResolvedValue({
- status: 200,
- data: { message: 'success' },
- headers: { 'content-type': 'application/json' },
- metadata: {
- platform: 'test-platform',
- requestId: 'test-123',
- serverTime: new Date(),
- rateLimit: undefined,
- platformData: {}
- }
- })
- }
- // 如果实现存在,创建客户端实例
- if (UniversalHttpClient) {
- httpClient = new UniversalHttpClient()
- httpClient.registerPlatform('test-platform', mockPlatformAdapter)
- }
- })
- afterEach(async () => {
- if (httpClient && httpClient.close) {
- await httpClient.close()
- }
- })
- test('应该存在 UniversalHttpClient 类', () => {
- // 这个测试现在应该失败
- expect(UniversalHttpClient).toBeDefined()
- expect(typeof UniversalHttpClient).toBe('function')
- })
- test('应该实现 IUniversalHttpClient 接口', () => {
- if (!UniversalHttpClient) {
- expect(UniversalHttpClient).toBeDefined() // 强制失败
- return
- }
- const client = new UniversalHttpClient()
- // 验证必需方法存在
- expect(typeof client.request).toBe('function')
- expect(typeof client.batchRequest).toBe('function')
- expect(typeof client.registerPlatform).toBe('function')
- expect(typeof client.getHealth).toBe('function')
- expect(typeof client.close).toBe('function')
- })
- test('request() 应该处理基本的GET请求', async () => {
- if (!httpClient) {
- expect(httpClient).toBeDefined() // 强制失败
- return
- }
- const request: HttpClientRequest = {
- platform: 'test-platform',
- accountId: 'test-account',
- method: 'GET',
- url: '/api/v1/test'
- }
- const response = await httpClient.request(request)
- // 验证响应结构符合契约
- expect(response).toHaveProperty('status')
- expect(response).toHaveProperty('statusText')
- expect(response).toHaveProperty('ok')
- expect(response).toHaveProperty('data')
- expect(response).toHaveProperty('headers')
- expect(response).toHaveProperty('metadata')
- // 验证元数据结构
- expect(response.metadata).toHaveProperty('requestId')
- expect(response.metadata).toHaveProperty('duration')
- expect(response.metadata).toHaveProperty('retryCount')
- expect(response.metadata).toHaveProperty('usedProxy')
- expect(response.metadata).toHaveProperty('timestamp')
- expect(response.metadata).toHaveProperty('platform')
- expect(response.metadata.platform).toBe('test-platform')
- })
- test('request() 应该处理带请求体的POST请求', async () => {
- if (!httpClient) {
- expect(httpClient).toBeDefined() // 强制失败
- return
- }
- const request: HttpClientRequest = {
- platform: 'test-platform',
- accountId: 'test-account',
- method: 'POST',
- url: '/api/v1/orders',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: {
- symbol: 'BTC-USD',
- side: 'buy',
- amount: 0.001
- }
- }
- const response = await httpClient.request(request)
- expect(response.status).toBe(200)
- expect(response.ok).toBe(true)
- expect(response.data).toBeDefined()
- })
- test('request() 应该支持超时配置', async () => {
- if (!httpClient) {
- expect(httpClient).toBeDefined() // 强制失败
- return
- }
- const request: HttpClientRequest = {
- platform: 'test-platform',
- accountId: 'test-account',
- method: 'GET',
- url: '/api/v1/slow-endpoint',
- options: {
- timeout: {
- connect: 5000,
- read: 30000,
- write: 15000
- }
- }
- }
- // 这应该不会抛出错误(模拟快速响应)
- const response = await httpClient.request(request)
- expect(response.status).toBe(200)
- })
- test('request() 应该支持重试配置', async () => {
- if (!httpClient) {
- expect(httpClient).toBeDefined() // 强制失败
- return
- }
- const request: HttpClientRequest = {
- platform: 'test-platform',
- accountId: 'test-account',
- method: 'GET',
- url: '/api/v1/test',
- options: {
- retry: {
- maxAttempts: 3,
- delay: 1000,
- exponentialBackoff: true
- }
- }
- }
- const response = await httpClient.request(request)
- expect(response.metadata.retryCount).toBeGreaterThanOrEqual(0)
- })
- test('request() 应该支持代理控制选项', async () => {
- if (!httpClient) {
- expect(httpClient).toBeDefined() // 强制失败
- return
- }
- const request: HttpClientRequest = {
- platform: 'test-platform',
- accountId: 'test-account',
- method: 'GET',
- url: '/api/v1/test',
- options: {
- proxy: {
- enabled: true,
- strategy: 'global'
- }
- }
- }
- const response = await httpClient.request(request)
- expect(response.metadata).toHaveProperty('usedProxy')
- })
- test('request() 应该支持幂等性键', async () => {
- if (!httpClient) {
- expect(httpClient).toBeDefined() // 强制失败
- return
- }
- const idempotencyKey = 'test-key-' + Date.now()
- const request: HttpClientRequest = {
- platform: 'test-platform',
- accountId: 'test-account',
- method: 'POST',
- url: '/api/v1/orders',
- body: { symbol: 'BTC-USD' },
- options: {
- idempotencyKey
- }
- }
- const response = await httpClient.request(request)
- expect(response.status).toBe(200)
- })
- test('request() 应该在未注册平台时抛出错误', async () => {
- if (!httpClient) {
- expect(httpClient).toBeDefined() // 强制失败
- return
- }
- const request: HttpClientRequest = {
- platform: 'unknown-platform',
- accountId: 'test-account',
- method: 'GET',
- url: '/api/v1/test'
- }
- await expect(httpClient.request(request)).rejects.toThrow()
- })
- test('request() 应该在无效请求时抛出错误', async () => {
- if (!httpClient) {
- expect(httpClient).toBeDefined() // 强制失败
- return
- }
- const invalidRequest = {
- platform: 'test-platform',
- // 缺少必需字段
- method: 'GET'
- } as HttpClientRequest
- await expect(httpClient.request(invalidRequest)).rejects.toThrow()
- })
- test('request() 响应应该包含正确的状态码和数据类型', async () => {
- if (!httpClient) {
- expect(httpClient).toBeDefined() // 强制失败
- return
- }
- const request: HttpClientRequest = {
- platform: 'test-platform',
- accountId: 'test-account',
- method: 'GET',
- url: '/api/v1/test'
- }
- const response = await httpClient.request(request)
- expect(typeof response.status).toBe('number')
- expect(typeof response.statusText).toBe('string')
- expect(typeof response.ok).toBe('boolean')
- expect(response.headers).toEqual(expect.any(Object))
- expect(response.metadata.timestamp).toBeInstanceOf(Date)
- expect(typeof response.metadata.duration).toBe('number')
- })
- })
|