"""KIS token cache tests (REQ-016). Uses InMemoryTokenCache as a deterministic double + a stubbed token endpoint (httpx.post monkeypatched) so no real Redis server or network is needed. """ from __future__ import annotations import httpx import pytest from trex_core.adapters.broker.kis import KISBroker from trex_core.adapters.broker.token_cache import InMemoryTokenCache _MOCK_URL = "https://openapivts.koreainvestment.com:29443" class _TokenResp: status_code = 200 def __init__(self, token: str) -> None: self._token = token def raise_for_status(self) -> None: pass def json(self) -> dict: return {"access_token": self._token, "expires_in": 86400} @pytest.fixture def count_token_calls(monkeypatch): calls = {"n": 0} def fake_post(url, **kwargs): assert url.endswith("/oauth2/tokenP") calls["n"] += 1 return _TokenResp(f"TOK-{calls['n']}") monkeypatch.setattr(httpx, "post", fake_post) return calls def _broker(cache, app_key="appkey0001"): return KISBroker("12345678-01", app_key=app_key, app_secret="s", base_url=_MOCK_URL, token_cache=cache) def test_cache_hit_avoids_issuance(count_token_calls): cache = InMemoryTokenCache() b = _broker(cache) t1 = b._ensure_token() # L3 issue + store assert count_token_calls["n"] == 1 b._token = None # drop L1 to force L2 lookup t2 = b._ensure_token() # L2 cache hit — no new issuance assert t2 == t1 and count_token_calls["n"] == 1 def test_shared_cache_across_instances(count_token_calls): cache = InMemoryTokenCache() a = _broker(cache) tok = a._ensure_token() # instance A issues + stores assert count_token_calls["n"] == 1 b = _broker(cache) # instance B (e.g. after restart) shares cache assert b._ensure_token() == tok and count_token_calls["n"] == 1 def test_graceful_fallback_when_cache_errors(count_token_calls): # RedisTokenCache swallows backend errors and returns None on miss; this # double models that post-swallow behaviour, so the broker still issues. class SilentCache: def load(self, *a, **k): return None def store(self, *a, **k): return None b = _broker(SilentCache()) assert b._ensure_token() == "TOK-1" and count_token_calls["n"] == 1 def test_app_key_suffix_mismatch_ignores_cache(count_token_calls): cache = InMemoryTokenCache() a = _broker(cache, app_key="AAAA1111") a._ensure_token() # cached under suffix 1111 assert count_token_calls["n"] == 1 b = _broker(cache, app_key="BBBB2222") # rotated credential, suffix 2222 b._ensure_token() # must NOT reuse stale cache -> re-issue assert count_token_calls["n"] == 2