from __future__ import annotations import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) import pytest from src.quant_engine.kis_api_client_v1 import ( KisCredentials, OrderEndpointBlockedError, _assert_read_only, ) # governance/rules/06_no_direct_api_trading.yaml — 이 테스트는 절대 약화/삭제하지 않는다. FORBIDDEN_ORDER_PATHS = ( "/uapi/domestic-stock/v1/trading/order-cash", "/uapi/domestic-stock/v1/trading/order-rvsecncl", "/uapi/domestic-stock/v1/trading/order-credit", "/uapi/domestic-stock/v1/trading/order-resv", "/uapi/domestic-stock/v1/trading/inquire-balance", # governance/rules/07 — 계좌 보유종목 조회 금지 ) FORBIDDEN_ORDER_TR_IDS = ( "TTTC0802U", "TTTC0801U", "VTTC0802U", "VTTC0801U", "TTTC8434R", "VTTC8434R", # governance/rules/07 — 주식잔고조회 금지 ) @pytest.mark.parametrize("path", FORBIDDEN_ORDER_PATHS) def test_order_path_is_blocked(path: str): with pytest.raises(OrderEndpointBlockedError): _assert_read_only(path, "FHKST01010100") @pytest.mark.parametrize("tr_id", FORBIDDEN_ORDER_TR_IDS) def test_order_tr_id_is_blocked(tr_id: str): with pytest.raises(OrderEndpointBlockedError): _assert_read_only("/uapi/domestic-stock/v1/quotations/inquire-price", tr_id) def test_known_readonly_endpoints_pass(): _assert_read_only("/uapi/domestic-stock/v1/quotations/inquire-price", "FHKST01010100") _assert_read_only("/uapi/domestic-stock/v1/quotations/inquire-asking-price-exp-ccn", "FHKST01010200") _assert_read_only("/uapi/domestic-stock/v1/quotations/daily-short-sale", "FHPST04830000") def test_no_order_endpoint_substring_anywhere_in_kis_client_source(): """정적 검증 — 누군가 향후 주문 함수를 추가하더라도 경로 문자열이 소스에 남으면 즉시 탐지. TTTC8434R/VTTC8434R(주식잔고조회)는 FORBIDDEN_TR_ID_PREFIXES 차단목록 '데이터'로 이 파일에 의도적으로 존재한다(prefix가 아닌 전체 TR_ID라 prefix-매칭으로는 막을 수 없어 명시적으로 등재) — 이 두 개는 검사에서 제외한다. 전체 코드베이스 차원의 "차단목록 외 파일에는 한 글자도 없어야 한다"는 보장은 tools/validate_no_direct_api_trading_v1.py(ALLOWLISTED_FILES 제외 전체 스캔)가 맡는다. """ source = (ROOT / "src" / "quant_engine" / "kis_api_client_v1.py").read_text(encoding="utf-8") blocklist_data_exceptions = {"TTTC8434R", "VTTC8434R"} for forbidden_path in FORBIDDEN_ORDER_PATHS: assert forbidden_path not in source, f"주문 엔드포인트 경로가 소스에 존재함: {forbidden_path}" for forbidden_tr_id in FORBIDDEN_ORDER_TR_IDS: if forbidden_tr_id in blocklist_data_exceptions: continue assert forbidden_tr_id not in source, f"주문 TR_ID가 소스에 존재함: {forbidden_tr_id}" def test_kis_client_module_defines_no_order_submission_function(): import src.quant_engine.kis_api_client_v1 as kis_module public_names = [name for name in dir(kis_module) if not name.startswith("_")] banned_keywords = ( "place_order", "submit_order", "cancel_order", "revise_order", "send_order", "inquire_balance", "account_balance", ) for name in public_names: lowered = name.lower() for banned in banned_keywords: assert banned not in lowered, f"주문 제출/정정/취소로 의심되는 함수가 존재함: {name}" def test_kis_credentials_load_uses_required_env_vars(monkeypatch): monkeypatch.setenv("KIS_APP_Key", "real-key") monkeypatch.setenv("KIS_APP_Secret", "real-secret") monkeypatch.setenv("KIS_APP_Key_TEST", "mock-key") monkeypatch.setenv("KIS_APP_Secret_TEST", "mock-secret") real = KisCredentials.load("real") mock = KisCredentials.load("mock") assert real.app_key == "real-key" assert real.app_secret == "real-secret" assert real.account == "real" assert mock.app_key == "mock-key" assert mock.app_secret == "mock-secret" assert mock.account == "mock"