"""Unit tests for order.py - order placement and validation.""" from unittest.mock import MagicMock, Mock, patch from src.common import MIN_KRW_ORDER from src.order import ( adjust_price_to_tick_size, place_buy_order_upbit, place_sell_order_upbit, ) class TestAdjustPriceToTickSize: """Test price adjustment to Upbit tick size.""" def test_adjust_price_with_valid_price(self): """Test normal price adjustment.""" with patch("src.order.pyupbit.get_tick_size", return_value=1000): result = adjust_price_to_tick_size(50000000) assert result > 0 assert result % 1000 == 0 def test_adjust_price_returns_original_on_error(self): """Test fallback to original price on API error.""" with patch("src.order.pyupbit.get_tick_size", side_effect=Exception("API error")): result = adjust_price_to_tick_size(50000000) assert result == 50000000 class TestPlaceBuyOrderValidation: """Test buy order validation (dry-run mode).""" def test_buy_order_dry_run(self): """Test dry-run buy order simulation.""" cfg = Mock() cfg.dry_run = True cfg.config = {} with patch("src.holdings.get_current_price", return_value=50000000): result = place_buy_order_upbit("KRW-BTC", 100000, cfg) assert result["status"] == "simulated" assert result["market"] == "KRW-BTC" assert result["amount_krw"] == 99950.0 def test_buy_order_below_min_amount(self): """Test buy order rejected for amount below minimum.""" cfg = Mock() cfg.dry_run = True cfg.config = {"auto_trade": {"min_order_value_krw": MIN_KRW_ORDER}} with patch("src.holdings.get_current_price", return_value=50000000): # Try to buy with amount below minimum result = place_buy_order_upbit("KRW-BTC", MIN_KRW_ORDER - 1000, cfg) assert result["status"] == "skipped_too_small" assert result["reason"] == "min_order_value" def test_buy_order_zero_price(self): """Test buy order rejected when current price is 0 or invalid.""" cfg = Mock() cfg.dry_run = True cfg.config = {} with patch("src.holdings.get_current_price", return_value=0): result = place_buy_order_upbit("KRW-BTC", 100000, cfg) assert result["status"] == "failed" assert "error" in result def test_buy_order_no_api_key(self): """Test buy order fails gracefully without API keys.""" cfg = Mock() cfg.dry_run = False cfg.upbit_access_key = None cfg.upbit_secret_key = None cfg.config = {} result = place_buy_order_upbit("KRW-BTC", 100000, cfg) assert result["status"] == "failed" assert "error" in result class TestPlaceSellOrderValidation: """Test sell order validation (dry-run mode).""" def test_sell_order_dry_run(self): """Test dry-run sell order simulation.""" cfg = Mock() cfg.dry_run = True result = place_sell_order_upbit("KRW-BTC", 0.01, cfg) assert result["status"] == "simulated" assert result["market"] == "KRW-BTC" assert result["amount"] == 0.01 def test_sell_order_invalid_amount(self): """Test sell order rejected for invalid amount.""" cfg = Mock() cfg.dry_run = False cfg.upbit_access_key = "key" cfg.upbit_secret_key = "secret" cfg.config = {"auto_trade": {"min_order_value_krw": MIN_KRW_ORDER}} result = place_sell_order_upbit("KRW-BTC", 0, cfg) assert result["status"] == "failed" assert result["error"] == "invalid_amount" def test_sell_order_below_min_value(self): """Test sell order rejected when estimated value below minimum.""" cfg = Mock() cfg.dry_run = False cfg.upbit_access_key = "key" cfg.upbit_secret_key = "secret" cfg.config = {"auto_trade": {"min_order_value_krw": MIN_KRW_ORDER}} # Current price very low so amount * price < min_order_value with patch("src.holdings.get_current_price", return_value=100): # 0.001 BTC * 100 KRW = 0.1 KRW < 5000 KRW minimum result = place_sell_order_upbit("KRW-BTC", 0.001, cfg) assert result["status"] == "skipped_too_small" assert result["reason"] == "min_order_value" def test_sell_order_price_unavailable(self): """Test sell order fails when current price unavailable.""" cfg = Mock() cfg.dry_run = False cfg.upbit_access_key = "key" cfg.upbit_secret_key = "secret" cfg.config = {} with patch("src.holdings.get_current_price", return_value=0): result = place_sell_order_upbit("KRW-BTC", 0.01, cfg) assert result["status"] == "failed" assert result["error"] == "price_unavailable" def test_sell_order_no_api_key(self): """Test sell order fails gracefully without API keys.""" cfg = Mock() cfg.dry_run = False cfg.upbit_access_key = None cfg.upbit_secret_key = None cfg.config = {} result = place_sell_order_upbit("KRW-BTC", 0.01, cfg) assert result["status"] == "failed" assert "error" in result class TestBuyOrderResponseValidation: """Test buy order API response validation.""" def test_response_type_validation(self): """Test validation of response type (must be dict).""" cfg = Mock() cfg.dry_run = False cfg.upbit_access_key = "key" cfg.upbit_secret_key = "secret" cfg.config = {"auto_trade": {"min_order_value_krw": MIN_KRW_ORDER}} with patch("src.holdings.get_current_price", return_value=50000000): with patch("src.order.pyupbit.Upbit") as mock_upbit_class: mock_upbit = MagicMock() mock_upbit_class.return_value = mock_upbit # Invalid response: string instead of dict mock_upbit.buy_limit_order.return_value = "invalid_response" with patch("src.order.adjust_price_to_tick_size", return_value=50000000): with patch("src.common.krw_budget_manager.allocate", return_value=(True, 100000, "tok")): result = place_buy_order_upbit("KRW-BTC", 100000, cfg) assert result["status"] == "failed" assert result["error"] == "invalid_response_type" def test_response_uuid_validation(self): """Test validation of uuid in response.""" cfg = Mock() cfg.dry_run = False cfg.upbit_access_key = "key" cfg.upbit_secret_key = "secret" cfg.config = {"auto_trade": {"min_order_value_krw": MIN_KRW_ORDER, "buy_price_slippage_pct": 1.0}} with patch("src.holdings.get_current_price", return_value=50000000): with patch("src.order.pyupbit.Upbit") as mock_upbit_class: mock_upbit = MagicMock() mock_upbit_class.return_value = mock_upbit # Response without uuid (Upbit error format) mock_upbit.buy_limit_order.return_value = { "error": {"name": "insufficient_funds", "message": "잔액 부족"} } with patch("src.order.adjust_price_to_tick_size", return_value=50000000): with patch("src.common.krw_budget_manager.allocate", return_value=(True, 100000, "tok")): result = place_buy_order_upbit("KRW-BTC", 100000, cfg) assert result["status"] == "failed" assert result["error"] == "order_rejected" class TestSellOrderResponseValidation: """Test sell order API response validation.""" def test_sell_response_uuid_missing(self): """Test sell order fails when uuid missing from response.""" cfg = Mock() cfg.dry_run = False cfg.upbit_access_key = "key" cfg.upbit_secret_key = "secret" cfg.config = {"auto_trade": {"min_order_value_krw": MIN_KRW_ORDER}} with patch("src.holdings.get_current_price", return_value=50000000): with patch("src.order.pyupbit.Upbit") as mock_upbit_class: mock_upbit = MagicMock() mock_upbit_class.return_value = mock_upbit # Missing uuid mock_upbit.sell_market_order.return_value = {"market": "KRW-BTC"} result = place_sell_order_upbit("KRW-BTC", 0.01, cfg) assert result["status"] == "failed" assert result["error"] == "order_rejected" def test_sell_response_type_invalid(self): """Test sell order fails with invalid response type.""" cfg = Mock() cfg.dry_run = False cfg.upbit_access_key = "key" cfg.upbit_secret_key = "secret" cfg.config = {} with patch("src.holdings.get_current_price", return_value=50000000): with patch("src.order.pyupbit.Upbit") as mock_upbit_class: mock_upbit = MagicMock() mock_upbit_class.return_value = mock_upbit # Invalid response mock_upbit.sell_market_order.return_value = "not_a_dict" result = place_sell_order_upbit("KRW-BTC", 0.01, cfg) assert result["status"] == "failed" assert result["error"] == "invalid_response_type"