329 lines
12 KiB
Python
329 lines
12 KiB
Python
# 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
|