From b690900cfc72674362f32af07140c8c85703aeed Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 16 May 2026 10:25:58 +0900 Subject: [PATCH] fix(signal_v2-phase3a): V1 KIS env pattern + test isolation fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit config.py: kis_env_type (virtual/real) + KIS_REAL_*/KIS_VIRTUAL_* env variables (V1 호환). kis_app_key/kis_app_secret/kis_account properties auto-select based on env type. main.py: KIS not-configured warning uses descriptive message including env type + expected var name. test_main.py: monkeypatch load_dotenv to no-op + setenv empty string (instead of delenv) — defeats .env re-read on importlib.reload. Pre-existing test_startup_warns_if_webai_api_key_missing also fixed. 33/33 tests pass (was 31/33). Co-Authored-By: Claude Opus 4.7 (1M context) --- signal_v2/config.py | 31 ++++++++++++++++++++++++------- signal_v2/main.py | 3 ++- signal_v2/tests/test_main.py | 17 +++++++++++++---- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/signal_v2/config.py b/signal_v2/config.py index b0b3fbf..fdc29de 100644 --- a/signal_v2/config.py +++ b/signal_v2/config.py @@ -20,13 +20,14 @@ class Settings: db_path: Path = field( default_factory=lambda: Path(__file__).parent / "data" / "signal_v2.db" ) - # Phase 3a additions - kis_app_key: str = field(default_factory=lambda: os.getenv("KIS_APP_KEY", "").strip()) - kis_app_secret: str = field(default_factory=lambda: os.getenv("KIS_APP_SECRET", "").strip()) - kis_account: str = field(default_factory=lambda: os.getenv("KIS_ACCOUNT", "").strip()) - kis_is_virtual: bool = field( - default_factory=lambda: os.getenv("KIS_IS_VIRTUAL", "true").lower() == "true" - ) + # KIS — V1 호환 패턴 (KIS_ENV_TYPE virtual/real) + kis_env_type: str = field(default_factory=lambda: os.getenv("KIS_ENV_TYPE", "virtual").lower()) + kis_real_app_key: str = field(default_factory=lambda: os.getenv("KIS_REAL_APP_KEY", "").strip()) + kis_real_app_secret: str = field(default_factory=lambda: os.getenv("KIS_REAL_APP_SECRET", "").strip()) + kis_real_account: str = field(default_factory=lambda: os.getenv("KIS_REAL_ACCOUNT", "").strip()) + kis_virtual_app_key: str = field(default_factory=lambda: os.getenv("KIS_VIRTUAL_APP_KEY", "").strip()) + kis_virtual_app_secret: str = field(default_factory=lambda: os.getenv("KIS_VIRTUAL_APP_SECRET", "").strip()) + kis_virtual_account: str = field(default_factory=lambda: os.getenv("KIS_VIRTUAL_ACCOUNT", "").strip()) v1_token_path: Path = field( default_factory=lambda: Path( os.getenv("V1_TOKEN_PATH", @@ -34,6 +35,22 @@ class Settings: ) ) + @property + def kis_is_virtual(self) -> bool: + return self.kis_env_type != "real" + + @property + def kis_app_key(self) -> str: + return self.kis_real_app_key if self.kis_env_type == "real" else self.kis_virtual_app_key + + @property + def kis_app_secret(self) -> str: + return self.kis_real_app_secret if self.kis_env_type == "real" else self.kis_virtual_app_secret + + @property + def kis_account(self) -> str: + return self.kis_real_account if self.kis_env_type == "real" else self.kis_virtual_account + def get_settings() -> Settings: return Settings() diff --git a/signal_v2/main.py b/signal_v2/main.py index a42e2b6..5cf6d66 100644 --- a/signal_v2/main.py +++ b/signal_v2/main.py @@ -38,7 +38,8 @@ async def lifespan(app: FastAPI): ) if not settings.kis_app_key: logger.warning( - "KIS_APP_KEY not configured — KIS REST/WebSocket disabled" + "KIS app_key not configured (KIS_ENV_TYPE=%s, KIS_%s_APP_KEY missing) — KIS REST/WebSocket disabled", + settings.kis_env_type, settings.kis_env_type.upper() ) _ctx.client = StockClient(settings.stock_api_url, settings.webai_api_key) diff --git a/signal_v2/tests/test_main.py b/signal_v2/tests/test_main.py index 78222c7..851aeb0 100644 --- a/signal_v2/tests/test_main.py +++ b/signal_v2/tests/test_main.py @@ -23,11 +23,15 @@ def test_health_endpoint_returns_status_online(monkeypatch): def test_startup_warns_if_webai_api_key_missing(monkeypatch, caplog): - monkeypatch.delenv("WEBAI_API_KEY", raising=False) + # Use setenv with empty string + no-op load_dotenv to defeat .env re-read on reload + monkeypatch.setattr("signal_v2.config.load_dotenv", lambda *a, **k: None) + monkeypatch.setenv("WEBAI_API_KEY", "") monkeypatch.setenv("STOCK_API_URL", "https://test.stock.local") import importlib from signal_v2 import config as cfg importlib.reload(cfg) + # After reload, load_dotenv reference is fresh — re-patch + monkeypatch.setattr("signal_v2.config.load_dotenv", lambda *a, **k: None) from signal_v2 import main as main_mod importlib.reload(main_mod) with caplog.at_level(logging.WARNING, logger="signal_v2.main"): @@ -37,17 +41,22 @@ def test_startup_warns_if_webai_api_key_missing(monkeypatch, caplog): def test_startup_warns_if_kis_app_key_missing(monkeypatch, caplog): - """KIS_APP_KEY 미설정 시 startup WARNING (KIS 호출 disabled).""" + """KIS app_key 미설정 시 startup WARNING (KIS 호출 disabled) — V1 패턴.""" + monkeypatch.setattr("signal_v2.config.load_dotenv", lambda *a, **k: None) monkeypatch.setenv("STOCK_API_URL", "https://test.stock.local") monkeypatch.setenv("WEBAI_API_KEY", "test-secret") - monkeypatch.delenv("KIS_APP_KEY", raising=False) + # V1 pattern: kis_env_type=virtual, both virtual keys empty + monkeypatch.setenv("KIS_ENV_TYPE", "virtual") + monkeypatch.setenv("KIS_VIRTUAL_APP_KEY", "") + monkeypatch.setenv("KIS_REAL_APP_KEY", "") import importlib from signal_v2 import config as cfg importlib.reload(cfg) + monkeypatch.setattr("signal_v2.config.load_dotenv", lambda *a, **k: None) from signal_v2 import main as main_mod importlib.reload(main_mod) with caplog.at_level(logging.WARNING, logger="signal_v2.main"): with TestClient(main_mod.app) as client: client.get("/health") - assert any("KIS_APP_KEY" in rec.message for rec in caplog.records) + assert any("KIS" in rec.message and "app_key" in rec.message.lower() for rec in caplog.records)