# src/tests/test_config_validation.py """ HIGH-002: 설정 검증 로직 테스트 config.py의 validate_config() 함수에 추가된 검증 로직을 테스트합니다: 1. Auto Trade 활성화 시 API 키 필수 2. 손절/익절 주기 논리 검증 (경고) 3. 스레드 수 범위 검증 4. 최소 주문 금액 검증 5. 매수 금액 검증 """ import os from unittest.mock import patch from src.config import validate_config class TestConfigValidation: """설정 검증 로직 테스트""" def test_valid_config_minimal(self): """최소한의 필수 항목만 있는 유효한 설정""" cfg = { "buy_check_interval_minutes": 240, "stop_loss_check_interval_minutes": 60, "profit_taking_check_interval_minutes": 240, "dry_run": True, "auto_trade": { "enabled": False, "buy_enabled": False, }, "confirm": { "confirm_stop_loss": False, }, "max_threads": 3, } is_valid, error = validate_config(cfg) assert is_valid is True assert error == "" def test_missing_required_key(self): """필수 항목 누락 시 검증 실패""" cfg = { "buy_check_interval_minutes": 240, # "stop_loss_check_interval_minutes": 60, # 누락 "profit_taking_check_interval_minutes": 240, "dry_run": True, "auto_trade": {}, } is_valid, error = validate_config(cfg) assert is_valid is False assert "stop_loss_check_interval_minutes" in error def test_invalid_interval_value(self): """잘못된 간격 값 (0 이하)""" cfg = { "buy_check_interval_minutes": 0, # 잘못된 값 "stop_loss_check_interval_minutes": 60, "profit_taking_check_interval_minutes": 240, "dry_run": True, "auto_trade": {}, "confirm": {"confirm_stop_loss": False}, } is_valid, error = validate_config(cfg) assert is_valid is False assert "buy_check_interval_minutes" in error def test_auto_trade_without_api_keys(self): """HIGH-002-1: auto_trade 활성화 시 API 키 없으면 실패""" cfg = { "buy_check_interval_minutes": 240, "stop_loss_check_interval_minutes": 60, "profit_taking_check_interval_minutes": 240, "dry_run": False, "auto_trade": { "enabled": True, # 활성화 "buy_enabled": True, }, "confirm": {"confirm_stop_loss": False}, "max_threads": 3, } # API 키 없는 상태로 테스트 with patch.dict(os.environ, {}, clear=True): is_valid, error = validate_config(cfg) assert is_valid is False assert "UPBIT_ACCESS_KEY" in error or "UPBIT_SECRET_KEY" in error def test_auto_trade_with_api_keys(self): """HIGH-002-1: auto_trade 활성화 + API 키 있으면 성공""" cfg = { "buy_check_interval_minutes": 240, "stop_loss_check_interval_minutes": 60, "profit_taking_check_interval_minutes": 240, "dry_run": False, "auto_trade": { "enabled": True, "buy_enabled": True, }, "confirm": {"confirm_stop_loss": False}, "max_threads": 3, } # API 키 있는 상태로 테스트 with patch.dict(os.environ, {"UPBIT_ACCESS_KEY": "test_key", "UPBIT_SECRET_KEY": "test_secret"}): is_valid, error = validate_config(cfg) assert is_valid is True assert error == "" def test_stop_loss_interval_greater_than_profit(self, caplog): """HIGH-002-2: 손절 주기 > 익절 주기 시 경고 로그""" cfg = { "buy_check_interval_minutes": 240, "stop_loss_check_interval_minutes": 300, # 5시간 "profit_taking_check_interval_minutes": 60, # 1시간 "dry_run": True, "auto_trade": {"enabled": False}, "confirm": {"confirm_stop_loss": False}, "max_threads": 3, } is_valid, error = validate_config(cfg) assert is_valid is True # 검증은 통과 (경고만 출력) # 경고 로그 확인 assert any("손절 주기" in record.message for record in caplog.records) def test_max_threads_invalid_type(self): """HIGH-002-3: max_threads가 정수가 아니면 실패""" cfg = { "buy_check_interval_minutes": 240, "stop_loss_check_interval_minutes": 60, "profit_taking_check_interval_minutes": 240, "dry_run": True, "auto_trade": {"enabled": False}, "confirm": {"confirm_stop_loss": False}, "max_threads": "invalid", # 잘못된 타입 } is_valid, error = validate_config(cfg) assert is_valid is False assert "max_threads" in error def test_max_threads_too_high(self, caplog): """HIGH-002-3: max_threads > 10 시 경고""" cfg = { "buy_check_interval_minutes": 240, "stop_loss_check_interval_minutes": 60, "profit_taking_check_interval_minutes": 240, "dry_run": True, "auto_trade": {"enabled": False}, "confirm": {"confirm_stop_loss": False}, "max_threads": 15, # 과도한 스레드 } is_valid, error = validate_config(cfg) assert is_valid is True # 검증은 통과 (경고만) # 경고 로그 확인 assert any("max_threads" in record.message and "과도" in record.message for record in caplog.records) def test_min_order_value_too_low(self): """HIGH-002-4: 최소 주문 금액 < 5000원 시 실패""" cfg = { "buy_check_interval_minutes": 240, "stop_loss_check_interval_minutes": 60, "profit_taking_check_interval_minutes": 240, "dry_run": True, "auto_trade": { "enabled": False, "min_order_value_krw": 3000, # 너무 낮음 }, "confirm": {"confirm_stop_loss": False}, "max_threads": 3, } is_valid, error = validate_config(cfg) assert is_valid is False assert "min_order_value_krw" in error assert "5000" in error def test_buy_amount_less_than_min_order(self, caplog): """HIGH-002-5: buy_amount < min_order_value 시 경고""" cfg = { "buy_check_interval_minutes": 240, "stop_loss_check_interval_minutes": 60, "profit_taking_check_interval_minutes": 240, "dry_run": True, "auto_trade": { "enabled": False, "min_order_value_krw": 10000, "buy_amount_krw": 5000, # min_order보다 작음 }, "confirm": {"confirm_stop_loss": False}, "max_threads": 3, } is_valid, error = validate_config(cfg) assert is_valid is True # 검증은 통과 (경고만) # 경고 로그 확인 assert any( "buy_amount_krw" in record.message and "min_order_value_krw" in record.message for record in caplog.records ) def test_buy_amount_too_low(self): """HIGH-002-5: buy_amount_krw < 5000원 시 실패""" cfg = { "buy_check_interval_minutes": 240, "stop_loss_check_interval_minutes": 60, "profit_taking_check_interval_minutes": 240, "dry_run": True, "auto_trade": { "enabled": False, "buy_amount_krw": 3000, # 너무 낮음 }, "confirm": {"confirm_stop_loss": False}, "max_threads": 3, } is_valid, error = validate_config(cfg) assert is_valid is False assert "buy_amount_krw" in error assert "5000" in error def test_confirm_invalid_type(self): """confirm 설정이 딕셔너리가 아니면 실패""" cfg = { "buy_check_interval_minutes": 240, "stop_loss_check_interval_minutes": 60, "profit_taking_check_interval_minutes": 240, "dry_run": True, "auto_trade": {"enabled": False}, "confirm": "invalid", # 잘못된 타입 "max_threads": 3, } is_valid, error = validate_config(cfg) assert is_valid is False assert "confirm" in error def test_dry_run_invalid_type(self): """dry_run이 boolean이 아니면 실패""" cfg = { "buy_check_interval_minutes": 240, "stop_loss_check_interval_minutes": 60, "profit_taking_check_interval_minutes": 240, "dry_run": "yes", # 잘못된 타입 "auto_trade": {"enabled": False}, "confirm": {"confirm_stop_loss": False}, } is_valid, error = validate_config(cfg) assert is_valid is False assert "dry_run" in error class TestEdgeCases: """경계값 및 엣지 케이스 테스트""" def test_intervals_equal_one(self): """간격이 정확히 1일 때 (최소값)""" cfg = { "buy_check_interval_minutes": 1, "stop_loss_check_interval_minutes": 1, "profit_taking_check_interval_minutes": 1, "dry_run": True, "auto_trade": {"enabled": False}, "confirm": {"confirm_stop_loss": False}, "max_threads": 1, } is_valid, error = validate_config(cfg) assert is_valid is True def test_max_threads_equal_ten(self): """max_threads가 정확히 10일 때 (경계값)""" cfg = { "buy_check_interval_minutes": 240, "stop_loss_check_interval_minutes": 60, "profit_taking_check_interval_minutes": 240, "dry_run": True, "auto_trade": {"enabled": False}, "confirm": {"confirm_stop_loss": False}, "max_threads": 10, # 경계값 } is_valid, error = validate_config(cfg) assert is_valid is True # 10은 허용 (경고 없음) def test_min_order_equal_5000(self): """최소 주문 금액이 정확히 5000원일 때""" cfg = { "buy_check_interval_minutes": 240, "stop_loss_check_interval_minutes": 60, "profit_taking_check_interval_minutes": 240, "dry_run": True, "auto_trade": { "enabled": False, "min_order_value_krw": 5000, # 최소값 }, "confirm": {"confirm_stop_loss": False}, "max_threads": 3, } is_valid, error = validate_config(cfg) assert is_valid is True def test_only_buy_enabled_without_enabled(self): """enabled=False, buy_enabled=True일 때도 API 키 체크""" cfg = { "buy_check_interval_minutes": 240, "stop_loss_check_interval_minutes": 60, "profit_taking_check_interval_minutes": 240, "dry_run": False, "auto_trade": { "enabled": False, "buy_enabled": True, # buy만 활성화 }, "confirm": {"confirm_stop_loss": False}, "max_threads": 3, } with patch.dict(os.environ, {}, clear=True): is_valid, error = validate_config(cfg) assert is_valid is False # API 키 필수 assert "UPBIT" in error