""" Tests for broker_keys module. Tests the broker key parsing, building, and backward compatibility. """ import pytest from brokers.broker_keys import ( parse_broker_key, build_broker_key, normalize_broker_key, get_available_broker_keys, get_exchange_connection_key, is_margin_supported, ParsedBrokerKey, BROKER_KEY_ALIASES, ) class TestNormalizeBrokerKey: """Test backward compatibility aliases.""" def test_paper_alias(self): assert normalize_broker_key('paper') == 'paper_spot' def test_kucoin_production_alias(self): assert normalize_broker_key('kucoin_production') == 'kucoin_spot_production' def test_kucoin_testnet_alias(self): assert normalize_broker_key('kucoin_testnet') == 'kucoin_spot_testnet' def test_binance_production_alias(self): assert normalize_broker_key('binance_production') == 'binance_spot_production' def test_new_format_unchanged(self): assert normalize_broker_key('paper_spot') == 'paper_spot' assert normalize_broker_key('kucoin_spot_production') == 'kucoin_spot_production' assert normalize_broker_key('paper_margin_isolated') == 'paper_margin_isolated' class TestParseBrokerKey: """Test broker key parsing.""" def test_parse_paper_spot(self): parsed = parse_broker_key('paper_spot') assert parsed.exchange == 'paper' assert parsed.product == 'spot' assert parsed.margin_mode is None assert parsed.mode is None assert parsed.is_paper assert not parsed.is_live assert not parsed.is_margin def test_parse_paper_margin_isolated(self): parsed = parse_broker_key('paper_margin_isolated') assert parsed.exchange == 'paper' assert parsed.product == 'margin' assert parsed.margin_mode == 'isolated' assert parsed.mode is None assert parsed.is_paper assert parsed.is_margin def test_parse_kucoin_spot_production(self): parsed = parse_broker_key('kucoin_spot_production') assert parsed.exchange == 'kucoin' assert parsed.product == 'spot' assert parsed.margin_mode is None assert parsed.mode == 'production' assert parsed.is_live assert not parsed.is_paper assert not parsed.is_margin assert parsed.is_production def test_parse_kucoin_spot_testnet(self): parsed = parse_broker_key('kucoin_spot_testnet') assert parsed.exchange == 'kucoin' assert parsed.product == 'spot' assert parsed.mode == 'testnet' assert parsed.is_testnet def test_parse_kucoin_margin_isolated_production(self): parsed = parse_broker_key('kucoin_margin_isolated_production') assert parsed.exchange == 'kucoin' assert parsed.product == 'margin' assert parsed.margin_mode == 'isolated' assert parsed.mode == 'production' assert parsed.is_live assert parsed.is_margin assert parsed.is_production def test_parse_old_format_paper(self): """Old 'paper' format should be normalized and parsed correctly.""" parsed = parse_broker_key('paper') assert parsed.exchange == 'paper' assert parsed.product == 'spot' def test_parse_old_format_kucoin_production(self): """Old 'kucoin_production' format should be normalized and parsed.""" parsed = parse_broker_key('kucoin_production') assert parsed.exchange == 'kucoin' assert parsed.product == 'spot' assert parsed.mode == 'production' def test_parse_invalid_format(self): with pytest.raises(ValueError, match="Invalid broker key"): parse_broker_key('invalid') def test_parse_invalid_product(self): with pytest.raises(ValueError, match="Invalid product"): parse_broker_key('paper_futures') def test_parse_margin_missing_mode(self): with pytest.raises(ValueError, match="missing margin_mode"): parse_broker_key('paper_margin') def test_parse_invalid_margin_mode(self): with pytest.raises(ValueError, match="Invalid margin_mode"): parse_broker_key('paper_margin_cross') # cross not in v1 class TestBuildBrokerKey: """Test broker key building.""" def test_build_paper_spot(self): key = build_broker_key('paper', 'spot') assert key == 'paper_spot' def test_build_paper_margin_isolated(self): key = build_broker_key('paper', 'margin', 'isolated') assert key == 'paper_margin_isolated' def test_build_kucoin_spot_production(self): key = build_broker_key('kucoin', 'spot', mode='production') assert key == 'kucoin_spot_production' def test_build_kucoin_spot_testnet(self): key = build_broker_key('kucoin', 'spot', mode='testnet') assert key == 'kucoin_spot_testnet' def test_build_kucoin_margin_isolated_production(self): key = build_broker_key('kucoin', 'margin', 'isolated', 'production') assert key == 'kucoin_margin_isolated_production' def test_build_invalid_product(self): with pytest.raises(ValueError, match="Invalid product"): build_broker_key('paper', 'futures') def test_build_margin_missing_mode(self): with pytest.raises(ValueError, match="margin_mode"): build_broker_key('paper', 'margin') # Missing margin_mode def test_build_live_missing_mode(self): with pytest.raises(ValueError, match="mode"): build_broker_key('kucoin', 'spot') # Missing mode for live class TestRoundTrip: """Test that build -> parse -> build is consistent.""" def test_paper_spot_roundtrip(self): key = build_broker_key('paper', 'spot') parsed = parse_broker_key(key) rebuilt = build_broker_key(parsed.exchange, parsed.product, parsed.margin_mode, parsed.mode) assert key == rebuilt def test_paper_margin_roundtrip(self): key = build_broker_key('paper', 'margin', 'isolated') parsed = parse_broker_key(key) rebuilt = build_broker_key(parsed.exchange, parsed.product, parsed.margin_mode, parsed.mode) assert key == rebuilt def test_kucoin_spot_roundtrip(self): key = build_broker_key('kucoin', 'spot', mode='production') parsed = parse_broker_key(key) rebuilt = build_broker_key(parsed.exchange, parsed.product, parsed.margin_mode, parsed.mode) assert key == rebuilt def test_kucoin_margin_roundtrip(self): key = build_broker_key('kucoin', 'margin', 'isolated', 'testnet') parsed = parse_broker_key(key) rebuilt = build_broker_key(parsed.exchange, parsed.product, parsed.margin_mode, parsed.mode) assert key == rebuilt class TestGetAvailableBrokerKeys: """Test available broker key generation.""" def test_paper_keys(self): keys = get_available_broker_keys('paper') assert len(keys) == 2 spot = next(k for k in keys if k['product'] == 'spot') assert spot['key'] == 'paper_spot' assert spot['label'] == 'Spot' margin = next(k for k in keys if k['product'] == 'margin') assert margin['key'] == 'paper_margin_isolated' assert margin['margin_mode'] == 'isolated' def test_kucoin_testnet_keys(self): keys = get_available_broker_keys('kucoin', testnet=True) assert len(keys) == 2 spot = next(k for k in keys if k['product'] == 'spot') assert spot['key'] == 'kucoin_spot_testnet' margin = next(k for k in keys if k['product'] == 'margin') assert margin['key'] == 'kucoin_margin_isolated_testnet' def test_kucoin_production_keys(self): keys = get_available_broker_keys('kucoin', testnet=False) spot = next(k for k in keys if k['product'] == 'spot') assert spot['key'] == 'kucoin_spot_production' margin = next(k for k in keys if k['product'] == 'margin') assert margin['key'] == 'kucoin_margin_isolated_production' def test_unsupported_exchange_no_margin(self): # Exchange not in MARGIN_SUPPORTED_EXCHANGES keys = get_available_broker_keys('coinbase', testnet=True) assert len(keys) == 1 assert keys[0]['product'] == 'spot' def test_exclude_margin(self): keys = get_available_broker_keys('kucoin', testnet=True, include_margin=False) assert len(keys) == 1 assert keys[0]['product'] == 'spot' class TestGetExchangeConnectionKey: """Test exchange connection key derivation.""" def test_paper_connection_key(self): parsed = parse_broker_key('paper_spot') assert get_exchange_connection_key(parsed) == 'paper' def test_paper_margin_connection_key(self): # Paper margin uses same connection as paper spot parsed = parse_broker_key('paper_margin_isolated') assert get_exchange_connection_key(parsed) == 'paper' def test_kucoin_spot_connection_key(self): parsed = parse_broker_key('kucoin_spot_production') assert get_exchange_connection_key(parsed) == 'kucoin_production' def test_kucoin_margin_connection_key(self): # Margin uses same connection as spot (same CCXT session) parsed = parse_broker_key('kucoin_margin_isolated_production') assert get_exchange_connection_key(parsed) == 'kucoin_production' def test_testnet_connection_key(self): parsed = parse_broker_key('kucoin_spot_testnet') assert get_exchange_connection_key(parsed) == 'kucoin_testnet' class TestIsMarginSupported: """Test margin support checking.""" def test_kucoin_supports_margin(self): assert is_margin_supported('kucoin') is True def test_paper_supports_margin(self): assert is_margin_supported('paper') is True def test_coinbase_no_margin(self): assert is_margin_supported('coinbase') is False def test_binance_no_margin_v1(self): # Binance margin not in v1 scope assert is_margin_supported('binance') is False