/** * Contract test for POST /api/v1/hedging/sessions * Tests the API contract for creating hedging sessions */ import { describe, it, expect, beforeAll, afterAll } from '@jest/globals'; import axios, { AxiosInstance } from 'axios'; import { CreateHedgingSessionRequest, CreateHedgingSessionResponse } from '../../src/types/hedging'; describe('POST /api/v1/hedging/sessions', () => { let client: AxiosInstance; const baseURL = 'http://localhost:3000/api/v1/hedging'; beforeAll(() => { client = axios.create({ baseURL, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer test-api-key' }, timeout: 5000 }); }); afterAll(() => { // Cleanup if needed }); describe('Request Schema Validation', () => { it('should accept valid hedging session creation request', async () => { const validRequest: CreateHedgingSessionRequest = { name: 'Test Hedging Session', accountIds: ['account-1', 'account-2'], volumeTarget: 10000, strategy: { symbol: 'ETH/USD', volumeDistribution: 'equal', priceRange: { min: 0.001, max: 0.01 }, timing: { minInterval: 30, maxInterval: 120, orderSize: { min: 100, max: 500 } }, riskLimits: { maxPositionSize: 0.1, stopLossThreshold: 0.05, maxSlippage: 0.02 }, orderTypes: { primary: 'limit', fallback: 'market' } } }; try { const response = await client.post('/sessions', validRequest); // Should return 201 Created expect(response.status).toBe(201); // Response should match schema const responseData: CreateHedgingSessionResponse = response.data; expect(responseData.success).toBe(true); expect(responseData.session).toBeDefined(); expect(responseData.session.id).toBeDefined(); expect(responseData.session.name).toBe(validRequest.name); expect(responseData.session.status).toBe('pending'); expect(responseData.session.accounts).toEqual(validRequest.accountIds); expect(responseData.session.strategy).toEqual(validRequest.strategy); expect(responseData.session.volumeTarget).toBe(validRequest.volumeTarget); expect(responseData.session.volumeGenerated).toBe(0); expect(responseData.session.startTime).toBeDefined(); expect(responseData.session.riskBreaches).toEqual([]); expect(responseData.session.orders).toEqual([]); } catch (error) { // This test should fail initially since the endpoint doesn't exist yet expect(error.response?.status).toBe(404); } }); it('should reject request with invalid session name', async () => { const invalidRequest = { name: 'ab', // Too short (less than 3 characters) accountIds: ['account-1', 'account-2'], volumeTarget: 10000, strategy: { symbol: 'ETH/USD', volumeDistribution: 'equal', priceRange: { min: 0.001, max: 0.01 }, timing: { minInterval: 30, maxInterval: 120, orderSize: { min: 100, max: 500 } }, riskLimits: { maxPositionSize: 0.1, stopLossThreshold: 0.05, maxSlippage: 0.02 }, orderTypes: { primary: 'limit', fallback: 'market' } } }; try { await client.post('/sessions', invalidRequest); fail('Should have rejected invalid request'); } catch (error) { expect(error.response?.status).toBe(400); expect(error.response?.data.success).toBe(false); expect(error.response?.data.error.code).toBe('INVALID_STRATEGY'); } }); it('should reject request with insufficient accounts', async () => { const invalidRequest = { name: 'Test Session', accountIds: ['account-1'], // Only one account (need at least 2) volumeTarget: 10000, strategy: { symbol: 'ETH/USD', volumeDistribution: 'equal', priceRange: { min: 0.001, max: 0.01 }, timing: { minInterval: 30, maxInterval: 120, orderSize: { min: 100, max: 500 } }, riskLimits: { maxPositionSize: 0.1, stopLossThreshold: 0.05, maxSlippage: 0.02 }, orderTypes: { primary: 'limit', fallback: 'market' } } }; try { await client.post('/sessions', invalidRequest); fail('Should have rejected invalid request'); } catch (error) { expect(error.response?.status).toBe(400); expect(error.response?.data.success).toBe(false); expect(error.response?.data.error.code).toBe('INSUFFICIENT_ACCOUNTS'); } }); it('should reject request with negative volume target', async () => { const invalidRequest = { name: 'Test Session', accountIds: ['account-1', 'account-2'], volumeTarget: -1000, // Negative volume target strategy: { symbol: 'ETH/USD', volumeDistribution: 'equal', priceRange: { min: 0.001, max: 0.01 }, timing: { minInterval: 30, maxInterval: 120, orderSize: { min: 100, max: 500 } }, riskLimits: { maxPositionSize: 0.1, stopLossThreshold: 0.05, maxSlippage: 0.02 }, orderTypes: { primary: 'limit', fallback: 'market' } } }; try { await client.post('/sessions', invalidRequest); fail('Should have rejected invalid request'); } catch (error) { expect(error.response?.status).toBe(400); expect(error.response?.data.success).toBe(false); expect(error.response?.data.error.code).toBe('INVALID_STRATEGY'); } }); it('should reject request with invalid strategy parameters', async () => { const invalidRequest = { name: 'Test Session', accountIds: ['account-1', 'account-2'], volumeTarget: 10000, strategy: { symbol: 'ETH/USD', volumeDistribution: 'equal', priceRange: { min: 0.01, max: 0.001 // min > max (invalid) }, timing: { minInterval: 30, maxInterval: 120, orderSize: { min: 100, max: 500 } }, riskLimits: { maxPositionSize: 0.1, stopLossThreshold: 0.05, maxSlippage: 0.02 }, orderTypes: { primary: 'limit', fallback: 'market' } } }; try { await client.post('/sessions', invalidRequest); fail('Should have rejected invalid request'); } catch (error) { expect(error.response?.status).toBe(400); expect(error.response?.data.success).toBe(false); expect(error.response?.data.error.code).toBe('INVALID_STRATEGY'); } }); }); describe('Authentication', () => { it('should reject request without authorization header', async () => { const clientWithoutAuth = axios.create({ baseURL, headers: { 'Content-Type': 'application/json' } }); const validRequest = { name: 'Test Session', accountIds: ['account-1', 'account-2'], volumeTarget: 10000, strategy: { symbol: 'ETH/USD', volumeDistribution: 'equal', priceRange: { min: 0.001, max: 0.01 }, timing: { minInterval: 30, maxInterval: 120, orderSize: { min: 100, max: 500 } }, riskLimits: { maxPositionSize: 0.1, stopLossThreshold: 0.05, maxSlippage: 0.02 }, orderTypes: { primary: 'limit', fallback: 'market' } } }; try { await clientWithoutAuth.post('/sessions', validRequest); fail('Should have rejected unauthorized request'); } catch (error) { expect(error.response?.status).toBe(401); } }); it('should reject request with invalid authorization token', async () => { const clientWithInvalidAuth = axios.create({ baseURL, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer invalid-token' } }); const validRequest = { name: 'Test Session', accountIds: ['account-1', 'account-2'], volumeTarget: 10000, strategy: { symbol: 'ETH/USD', volumeDistribution: 'equal', priceRange: { min: 0.001, max: 0.01 }, timing: { minInterval: 30, maxInterval: 120, orderSize: { min: 100, max: 500 } }, riskLimits: { maxPositionSize: 0.1, stopLossThreshold: 0.05, maxSlippage: 0.02 }, orderTypes: { primary: 'limit', fallback: 'market' } } }; try { await clientWithInvalidAuth.post('/sessions', validRequest); fail('Should have rejected request with invalid token'); } catch (error) { expect(error.response?.status).toBe(401); } }); }); describe('Business Logic Validation', () => { it('should reject request with inactive accounts', async () => { const requestWithInactiveAccount = { name: 'Test Session', accountIds: ['account-1', 'inactive-account'], // inactive-account is not active volumeTarget: 10000, strategy: { symbol: 'ETH/USD', volumeDistribution: 'equal', priceRange: { min: 0.001, max: 0.01 }, timing: { minInterval: 30, maxInterval: 120, orderSize: { min: 100, max: 500 } }, riskLimits: { maxPositionSize: 0.1, stopLossThreshold: 0.05, maxSlippage: 0.02 }, orderTypes: { primary: 'limit', fallback: 'market' } } }; try { await client.post('/sessions', requestWithInactiveAccount); fail('Should have rejected request with inactive account'); } catch (error) { expect(error.response?.status).toBe(400); expect(error.response?.data.success).toBe(false); expect(error.response?.data.error.code).toBe('ACCOUNT_NOT_ACTIVE'); } }); it('should reject request with insufficient account balance', async () => { const requestWithInsufficientBalance = { name: 'Test Session', accountIds: ['account-1', 'poor-account'], // poor-account has insufficient balance volumeTarget: 1000000, // Very high volume target strategy: { symbol: 'ETH/USD', volumeDistribution: 'equal', priceRange: { min: 0.001, max: 0.01 }, timing: { minInterval: 30, maxInterval: 120, orderSize: { min: 100, max: 500 } }, riskLimits: { maxPositionSize: 0.1, stopLossThreshold: 0.05, maxSlippage: 0.02 }, orderTypes: { primary: 'limit', fallback: 'market' } } }; try { await client.post('/sessions', requestWithInsufficientBalance); fail('Should have rejected request with insufficient balance'); } catch (error) { expect(error.response?.status).toBe(400); expect(error.response?.data.success).toBe(false); expect(error.response?.data.error.code).toBe('INSUFFICIENT_BALANCE'); } }); }); });