"""KISClient — 토큰 발급/캐시 + quote/daily 파싱 (respx).""" import httpx import respx from kis_client import KISClient BASE = "https://openapi.koreainvestment.com:9443" def _client(): return KISClient("APPKEY", "APPSECRET", "12345678-01", is_virtual=False) @respx.mock async def test_issue_token_cached(): route = respx.post(f"{BASE}/oauth2/tokenP").mock( return_value=httpx.Response(200, json={"access_token": "TKN", "expires_in": 86400})) c = _client() t1 = await c._issue_token() t2 = await c._issue_token() assert t1 == "TKN" and t2 == "TKN" assert route.call_count == 1 # 캐시 → 1회만 발급 await c.close() @respx.mock async def test_get_quote_parses(): respx.post(f"{BASE}/oauth2/tokenP").mock( return_value=httpx.Response(200, json={"access_token": "TKN", "expires_in": 86400})) respx.get(f"{BASE}/uapi/domestic-stock/v1/quotations/inquire-price").mock( return_value=httpx.Response(200, json={"output": { "stck_prpr": "71500", "stck_oprc": "71000", "acml_vol": "1234567"}})) c = _client() q = await c.get_quote("005930") assert q["price"] == 71500 and q["day_open"] == 71000 and q["today_volume"] == 1234567 await c.close() @respx.mock async def test_get_daily_ascending(): respx.post(f"{BASE}/oauth2/tokenP").mock( return_value=httpx.Response(200, json={"access_token": "TKN", "expires_in": 86400})) # KIS는 내림차순 반환 → 오름차순으로 뒤집혀야 함 respx.get(f"{BASE}/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice").mock( return_value=httpx.Response(200, json={"output2": [ {"stck_bsop_date": "20260702", "stck_oprc": "100", "stck_hgpr": "110", "stck_lwpr": "90", "stck_clpr": "105", "acml_vol": "5"}, {"stck_bsop_date": "20260701", "stck_oprc": "95", "stck_hgpr": "102", "stck_lwpr": "94", "stck_clpr": "100", "acml_vol": "4"}]})) c = _client() bars = await c.get_daily_ohlcv("005930", days=250) assert bars[0]["datetime"] == "2026-07-01" assert bars[-1]["close"] == 105 await c.close()