123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- /**
- * 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');
- }
- });
- });
- });
|