Some checks failed
CI / test (pull_request) Has been cancelled
mock_settings fixture에 MODE 미지정 시 .env의 MODE=live가 적용되어 paper TR_ID를 검증하는 테스트 3개가 실패. - test_buy_market_order: VTTT1002U 기대 → TTTT1002U 실제 - test_sell_limit_order: VTTT1001U 기대 → TTTT1006U 실제 - test_us_paper_uses_vttt1004u: VTTT1004U 기대 → TTTT1004U 실제 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1035 lines
39 KiB
Python
1035 lines
39 KiB
Python
"""Tests for OverseasBroker — rankings, price, balance, order, and helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import aiohttp
|
|
import pytest
|
|
|
|
from src.broker.kis_api import KISBroker
|
|
from src.broker.overseas import OverseasBroker, _PRICE_EXCHANGE_MAP, _RANKING_EXCHANGE_MAP
|
|
from src.config import Settings
|
|
|
|
|
|
def _make_async_cm(mock_resp: AsyncMock) -> MagicMock:
|
|
"""Create an async context manager that returns mock_resp on __aenter__."""
|
|
cm = MagicMock()
|
|
cm.__aenter__ = AsyncMock(return_value=mock_resp)
|
|
cm.__aexit__ = AsyncMock(return_value=False)
|
|
return cm
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_settings() -> Settings:
|
|
"""Provide mock settings with correct default TR_IDs/paths."""
|
|
return Settings(
|
|
KIS_APP_KEY="test_key",
|
|
KIS_APP_SECRET="test_secret",
|
|
KIS_ACCOUNT_NO="12345678-01",
|
|
GEMINI_API_KEY="test_gemini_key",
|
|
MODE="paper", # Explicitly set to avoid .env MODE=live override
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_broker(mock_settings: Settings) -> KISBroker:
|
|
"""Provide a mock KIS broker."""
|
|
broker = KISBroker(mock_settings)
|
|
broker.get_orderbook = AsyncMock() # type: ignore[method-assign]
|
|
return broker
|
|
|
|
|
|
@pytest.fixture
|
|
def overseas_broker(mock_broker: KISBroker) -> OverseasBroker:
|
|
"""Provide an OverseasBroker wrapping a mock KISBroker."""
|
|
return OverseasBroker(mock_broker)
|
|
|
|
|
|
def _setup_broker_mocks(overseas_broker: OverseasBroker, mock_session: MagicMock) -> None:
|
|
"""Wire up common broker mocks."""
|
|
overseas_broker._broker._rate_limiter.acquire = AsyncMock()
|
|
overseas_broker._broker._get_session = MagicMock(return_value=mock_session)
|
|
overseas_broker._broker._auth_headers = AsyncMock(return_value={})
|
|
|
|
|
|
class TestRankingExchangeMap:
|
|
"""Test exchange code mapping for ranking API."""
|
|
|
|
def test_nasd_maps_to_nas(self) -> None:
|
|
assert _RANKING_EXCHANGE_MAP["NASD"] == "NAS"
|
|
|
|
def test_nyse_maps_to_nys(self) -> None:
|
|
assert _RANKING_EXCHANGE_MAP["NYSE"] == "NYS"
|
|
|
|
def test_amex_maps_to_ams(self) -> None:
|
|
assert _RANKING_EXCHANGE_MAP["AMEX"] == "AMS"
|
|
|
|
def test_sehk_maps_to_hks(self) -> None:
|
|
assert _RANKING_EXCHANGE_MAP["SEHK"] == "HKS"
|
|
|
|
def test_unmapped_exchange_passes_through(self) -> None:
|
|
assert _RANKING_EXCHANGE_MAP.get("UNKNOWN", "UNKNOWN") == "UNKNOWN"
|
|
|
|
def test_tse_unchanged(self) -> None:
|
|
assert _RANKING_EXCHANGE_MAP["TSE"] == "TSE"
|
|
|
|
|
|
class TestConfigDefaults:
|
|
"""Test that config defaults match KIS official API specs."""
|
|
|
|
def test_fluct_tr_id(self, mock_settings: Settings) -> None:
|
|
assert mock_settings.OVERSEAS_RANKING_FLUCT_TR_ID == "HHDFS76290000"
|
|
|
|
def test_volume_tr_id(self, mock_settings: Settings) -> None:
|
|
assert mock_settings.OVERSEAS_RANKING_VOLUME_TR_ID == "HHDFS76270000"
|
|
|
|
def test_fluct_path(self, mock_settings: Settings) -> None:
|
|
assert mock_settings.OVERSEAS_RANKING_FLUCT_PATH == "/uapi/overseas-stock/v1/ranking/updown-rate"
|
|
|
|
def test_volume_path(self, mock_settings: Settings) -> None:
|
|
assert mock_settings.OVERSEAS_RANKING_VOLUME_PATH == "/uapi/overseas-stock/v1/ranking/volume-surge"
|
|
|
|
|
|
class TestFetchOverseasRankings:
|
|
"""Test fetch_overseas_rankings method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fluctuation_uses_correct_params(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""Fluctuation ranking should use HHDFS76290000, updown-rate path, and correct params."""
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(
|
|
return_value={"output": [{"symb": "AAPL", "name": "Apple"}]}
|
|
)
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
overseas_broker._broker._auth_headers = AsyncMock(
|
|
return_value={"authorization": "Bearer test"}
|
|
)
|
|
|
|
result = await overseas_broker.fetch_overseas_rankings("NASD", "fluctuation")
|
|
|
|
assert len(result) == 1
|
|
assert result[0]["symb"] == "AAPL"
|
|
|
|
call_args = mock_session.get.call_args
|
|
url = call_args[0][0]
|
|
params = call_args[1]["params"]
|
|
|
|
assert "/uapi/overseas-stock/v1/ranking/updown-rate" in url
|
|
assert params["EXCD"] == "NAS"
|
|
assert params["NDAY"] == "0"
|
|
assert params["GUBN"] == "0" # 0=전체(상승+하락), 변동성 스캐너에 필요
|
|
assert params["VOL_RANG"] == "0"
|
|
|
|
overseas_broker._broker._auth_headers.assert_called_with("HHDFS76290000")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_volume_uses_correct_params(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""Volume ranking should use HHDFS76270000, volume-surge path, and correct params."""
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(
|
|
return_value={"output": [{"symb": "TSLA", "name": "Tesla"}]}
|
|
)
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
overseas_broker._broker._auth_headers = AsyncMock(
|
|
return_value={"authorization": "Bearer test"}
|
|
)
|
|
|
|
result = await overseas_broker.fetch_overseas_rankings("NYSE", "volume")
|
|
|
|
assert len(result) == 1
|
|
|
|
call_args = mock_session.get.call_args
|
|
url = call_args[0][0]
|
|
params = call_args[1]["params"]
|
|
|
|
assert "/uapi/overseas-stock/v1/ranking/volume-surge" in url
|
|
assert params["EXCD"] == "NYS"
|
|
assert params["MIXN"] == "0"
|
|
assert params["VOL_RANG"] == "0"
|
|
assert "NDAY" not in params
|
|
assert "GUBN" not in params
|
|
|
|
overseas_broker._broker._auth_headers.assert_called_with("HHDFS76270000")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_404_returns_empty_list(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""HTTP 404 should return empty list (fallback) instead of raising."""
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 404
|
|
mock_resp.text = AsyncMock(return_value="Not Found")
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
|
|
result = await overseas_broker.fetch_overseas_rankings("AMEX", "fluctuation")
|
|
assert result == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_non_404_error_raises(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""Non-404 HTTP errors should raise ConnectionError."""
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 500
|
|
mock_resp.text = AsyncMock(return_value="Internal Server Error")
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
|
|
with pytest.raises(ConnectionError, match="500"):
|
|
await overseas_broker.fetch_overseas_rankings("NASD")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_empty_response_returns_empty(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""Empty output in response should return empty list."""
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"output": []})
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
|
|
result = await overseas_broker.fetch_overseas_rankings("NASD")
|
|
assert result == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ranking_disabled_returns_empty(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""When OVERSEAS_RANKING_ENABLED=False, should return empty immediately."""
|
|
overseas_broker._broker._settings.OVERSEAS_RANKING_ENABLED = False
|
|
result = await overseas_broker.fetch_overseas_rankings("NASD")
|
|
assert result == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_limit_truncates_results(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""Results should be truncated to the specified limit."""
|
|
rows = [{"symb": f"SYM{i}"} for i in range(20)]
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"output": rows})
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
|
|
result = await overseas_broker.fetch_overseas_rankings("NASD", limit=5)
|
|
assert len(result) == 5
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_network_error_raises(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""Network errors should raise ConnectionError."""
|
|
cm = MagicMock()
|
|
cm.__aenter__ = AsyncMock(side_effect=aiohttp.ClientError("timeout"))
|
|
cm.__aexit__ = AsyncMock(return_value=False)
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=cm)
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
|
|
with pytest.raises(ConnectionError, match="Network error"):
|
|
await overseas_broker.fetch_overseas_rankings("NASD")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_exchange_code_mapping_applied(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""All major exchanges should use mapped codes in API params."""
|
|
for original, mapped in [("NASD", "NAS"), ("NYSE", "NYS"), ("AMEX", "AMS")]:
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"output": [{"symb": "X"}]})
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
|
|
await overseas_broker.fetch_overseas_rankings(original)
|
|
|
|
call_params = mock_session.get.call_args[1]["params"]
|
|
assert call_params["EXCD"] == mapped, f"{original} should map to {mapped}"
|
|
|
|
|
|
class TestGetOverseasPrice:
|
|
"""Test get_overseas_price method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_success(self, overseas_broker: OverseasBroker) -> None:
|
|
"""Successful price fetch returns JSON data."""
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"output": {"last": "150.00"}})
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
overseas_broker._broker._auth_headers = AsyncMock(return_value={"authorization": "Bearer t"})
|
|
|
|
result = await overseas_broker.get_overseas_price("NASD", "AAPL")
|
|
assert result["output"]["last"] == "150.00"
|
|
|
|
call_args = mock_session.get.call_args
|
|
params = call_args[1]["params"]
|
|
assert params["EXCD"] == "NAS" # NASD → NAS via _PRICE_EXCHANGE_MAP
|
|
assert params["SYMB"] == "AAPL"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_http_error_raises(self, overseas_broker: OverseasBroker) -> None:
|
|
"""Non-200 response should raise ConnectionError."""
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 400
|
|
mock_resp.text = AsyncMock(return_value="Bad Request")
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
|
|
with pytest.raises(ConnectionError, match="get_overseas_price failed"):
|
|
await overseas_broker.get_overseas_price("NASD", "AAPL")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_network_error_raises(self, overseas_broker: OverseasBroker) -> None:
|
|
"""Network error should raise ConnectionError."""
|
|
cm = MagicMock()
|
|
cm.__aenter__ = AsyncMock(side_effect=aiohttp.ClientError("conn refused"))
|
|
cm.__aexit__ = AsyncMock(return_value=False)
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=cm)
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
|
|
with pytest.raises(ConnectionError, match="Network error"):
|
|
await overseas_broker.get_overseas_price("NASD", "AAPL")
|
|
|
|
|
|
class TestGetOverseasBalance:
|
|
"""Test get_overseas_balance method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_success(self, overseas_broker: OverseasBroker) -> None:
|
|
"""Successful balance fetch returns JSON data."""
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"output1": [{"pdno": "AAPL"}]})
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
|
|
result = await overseas_broker.get_overseas_balance("NASD")
|
|
assert result["output1"][0]["pdno"] == "AAPL"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_http_error_raises(self, overseas_broker: OverseasBroker) -> None:
|
|
"""Non-200 should raise ConnectionError."""
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 500
|
|
mock_resp.text = AsyncMock(return_value="Server Error")
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
|
|
with pytest.raises(ConnectionError, match="get_overseas_balance failed"):
|
|
await overseas_broker.get_overseas_balance("NASD")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_network_error_raises(self, overseas_broker: OverseasBroker) -> None:
|
|
"""Network error should raise ConnectionError."""
|
|
cm = MagicMock()
|
|
cm.__aenter__ = AsyncMock(side_effect=TimeoutError("timeout"))
|
|
cm.__aexit__ = AsyncMock(return_value=False)
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=cm)
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
|
|
with pytest.raises(ConnectionError, match="Network error"):
|
|
await overseas_broker.get_overseas_balance("NYSE")
|
|
|
|
|
|
class TestSendOverseasOrder:
|
|
"""Test send_overseas_order method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_buy_market_order(self, overseas_broker: OverseasBroker) -> None:
|
|
"""Market buy order should use VTTT1002U and ORD_DVSN=01."""
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"rt_cd": "0"})
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.post = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
overseas_broker._broker._get_hash_key = AsyncMock(return_value="hashval")
|
|
|
|
result = await overseas_broker.send_overseas_order("NASD", "AAPL", "BUY", 10)
|
|
assert result["rt_cd"] == "0"
|
|
|
|
# Verify BUY TR_ID
|
|
overseas_broker._broker._auth_headers.assert_called_with("VTTT1002U")
|
|
|
|
call_args = mock_session.post.call_args
|
|
body = call_args[1]["json"]
|
|
assert body["ORD_DVSN"] == "01" # market order
|
|
assert body["OVRS_ORD_UNPR"] == "0"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sell_limit_order(self, overseas_broker: OverseasBroker) -> None:
|
|
"""Limit sell order should use VTTT1001U and ORD_DVSN=00."""
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"rt_cd": "0"})
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.post = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
overseas_broker._broker._get_hash_key = AsyncMock(return_value="hashval")
|
|
|
|
result = await overseas_broker.send_overseas_order("NYSE", "MSFT", "SELL", 5, price=350.0)
|
|
assert result["rt_cd"] == "0"
|
|
|
|
overseas_broker._broker._auth_headers.assert_called_with("VTTT1001U")
|
|
|
|
call_args = mock_session.post.call_args
|
|
body = call_args[1]["json"]
|
|
assert body["ORD_DVSN"] == "00" # limit order
|
|
assert body["OVRS_ORD_UNPR"] == "350.0"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_order_http_error_raises(self, overseas_broker: OverseasBroker) -> None:
|
|
"""Non-200 should raise ConnectionError."""
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 400
|
|
mock_resp.text = AsyncMock(return_value="Bad Request")
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.post = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
overseas_broker._broker._get_hash_key = AsyncMock(return_value="hashval")
|
|
|
|
with pytest.raises(ConnectionError, match="send_overseas_order failed"):
|
|
await overseas_broker.send_overseas_order("NASD", "AAPL", "BUY", 1)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_order_network_error_raises(self, overseas_broker: OverseasBroker) -> None:
|
|
"""Network error should raise ConnectionError."""
|
|
cm = MagicMock()
|
|
cm.__aenter__ = AsyncMock(side_effect=aiohttp.ClientError("conn reset"))
|
|
cm.__aexit__ = AsyncMock(return_value=False)
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.post = MagicMock(return_value=cm)
|
|
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
overseas_broker._broker._get_hash_key = AsyncMock(return_value="hashval")
|
|
|
|
with pytest.raises(ConnectionError, match="Network error"):
|
|
await overseas_broker.send_overseas_order("NASD", "TSLA", "SELL", 2)
|
|
|
|
|
|
class TestGetCurrencyCode:
|
|
"""Test _get_currency_code mapping."""
|
|
|
|
def test_us_exchanges(self, overseas_broker: OverseasBroker) -> None:
|
|
assert overseas_broker._get_currency_code("NASD") == "USD"
|
|
assert overseas_broker._get_currency_code("NYSE") == "USD"
|
|
assert overseas_broker._get_currency_code("AMEX") == "USD"
|
|
|
|
def test_japan(self, overseas_broker: OverseasBroker) -> None:
|
|
assert overseas_broker._get_currency_code("TSE") == "JPY"
|
|
|
|
def test_hong_kong(self, overseas_broker: OverseasBroker) -> None:
|
|
assert overseas_broker._get_currency_code("SEHK") == "HKD"
|
|
|
|
def test_china(self, overseas_broker: OverseasBroker) -> None:
|
|
assert overseas_broker._get_currency_code("SHAA") == "CNY"
|
|
assert overseas_broker._get_currency_code("SZAA") == "CNY"
|
|
|
|
def test_vietnam(self, overseas_broker: OverseasBroker) -> None:
|
|
assert overseas_broker._get_currency_code("HNX") == "VND"
|
|
assert overseas_broker._get_currency_code("HSX") == "VND"
|
|
|
|
def test_unknown_defaults_usd(self, overseas_broker: OverseasBroker) -> None:
|
|
assert overseas_broker._get_currency_code("UNKNOWN") == "USD"
|
|
|
|
|
|
class TestExtractRankingRows:
|
|
"""Test _extract_ranking_rows helper."""
|
|
|
|
def test_output_key(self, overseas_broker: OverseasBroker) -> None:
|
|
data = {"output": [{"a": 1}, {"b": 2}]}
|
|
assert overseas_broker._extract_ranking_rows(data) == [{"a": 1}, {"b": 2}]
|
|
|
|
def test_output1_key(self, overseas_broker: OverseasBroker) -> None:
|
|
data = {"output1": [{"c": 3}]}
|
|
assert overseas_broker._extract_ranking_rows(data) == [{"c": 3}]
|
|
|
|
def test_output2_key(self, overseas_broker: OverseasBroker) -> None:
|
|
data = {"output2": [{"d": 4}]}
|
|
assert overseas_broker._extract_ranking_rows(data) == [{"d": 4}]
|
|
|
|
def test_no_list_returns_empty(self, overseas_broker: OverseasBroker) -> None:
|
|
data = {"output": "not a list"}
|
|
assert overseas_broker._extract_ranking_rows(data) == []
|
|
|
|
def test_empty_data(self, overseas_broker: OverseasBroker) -> None:
|
|
assert overseas_broker._extract_ranking_rows({}) == []
|
|
|
|
def test_filters_non_dict_rows(self, overseas_broker: OverseasBroker) -> None:
|
|
data = {"output": [{"a": 1}, "invalid", {"b": 2}]}
|
|
assert overseas_broker._extract_ranking_rows(data) == [{"a": 1}, {"b": 2}]
|
|
|
|
|
|
class TestPriceExchangeMap:
|
|
"""Test _PRICE_EXCHANGE_MAP is applied in get_overseas_price (issue #151)."""
|
|
|
|
def test_price_map_equals_ranking_map(self) -> None:
|
|
assert _PRICE_EXCHANGE_MAP is _RANKING_EXCHANGE_MAP
|
|
|
|
@pytest.mark.parametrize("original,expected", [
|
|
("NASD", "NAS"),
|
|
("NYSE", "NYS"),
|
|
("AMEX", "AMS"),
|
|
])
|
|
def test_us_exchange_code_mapping(self, original: str, expected: str) -> None:
|
|
assert _PRICE_EXCHANGE_MAP[original] == expected
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_overseas_price_sends_mapped_code(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""NASD → NAS must be sent to HHDFS00000300."""
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"output": {"last": "200.00"}})
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
|
|
await overseas_broker.get_overseas_price("NASD", "AAPL")
|
|
|
|
params = mock_session.get.call_args[1]["params"]
|
|
assert params["EXCD"] == "NAS"
|
|
|
|
|
|
class TestOrderRtCdCheck:
|
|
"""Test that send_overseas_order checks rt_cd and logs accordingly (issue #151)."""
|
|
|
|
@pytest.fixture
|
|
def overseas_broker(self, mock_settings: Settings) -> OverseasBroker:
|
|
broker = MagicMock(spec=KISBroker)
|
|
broker._settings = mock_settings
|
|
broker._account_no = "12345678"
|
|
broker._product_cd = "01"
|
|
broker._base_url = "https://openapivts.koreainvestment.com:9443"
|
|
broker._rate_limiter = AsyncMock()
|
|
broker._rate_limiter.acquire = AsyncMock()
|
|
broker._auth_headers = AsyncMock(return_value={"authorization": "Bearer t"})
|
|
broker._get_hash_key = AsyncMock(return_value="hashval")
|
|
return OverseasBroker(broker)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_success_rt_cd_returns_data(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""rt_cd='0' → order accepted, data returned."""
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"rt_cd": "0", "msg1": "완료"})
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.post = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
overseas_broker._broker._get_session = MagicMock(return_value=mock_session)
|
|
|
|
result = await overseas_broker.send_overseas_order("NASD", "AAPL", "BUY", 10, price=150.0)
|
|
assert result["rt_cd"] == "0"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_error_rt_cd_returns_data_with_msg(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""rt_cd != '0' → order rejected, data still returned (caller checks rt_cd)."""
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(
|
|
return_value={"rt_cd": "1", "msg1": "주문가능금액이 부족합니다."}
|
|
)
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.post = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
overseas_broker._broker._get_session = MagicMock(return_value=mock_session)
|
|
|
|
result = await overseas_broker.send_overseas_order("NASD", "AAPL", "BUY", 10, price=150.0)
|
|
assert result["rt_cd"] == "1"
|
|
assert "부족" in result["msg1"]
|
|
|
|
|
|
class TestPaperOverseasCash:
|
|
"""Test PAPER_OVERSEAS_CASH config setting (issue #151)."""
|
|
|
|
def test_default_value(self) -> None:
|
|
settings = Settings(
|
|
KIS_APP_KEY="k",
|
|
KIS_APP_SECRET="s",
|
|
KIS_ACCOUNT_NO="12345678-01",
|
|
GEMINI_API_KEY="g",
|
|
)
|
|
assert settings.PAPER_OVERSEAS_CASH == 50000.0
|
|
|
|
def test_env_override(self) -> None:
|
|
import os
|
|
os.environ["PAPER_OVERSEAS_CASH"] = "25000"
|
|
settings = Settings(
|
|
KIS_APP_KEY="k",
|
|
KIS_APP_SECRET="s",
|
|
KIS_ACCOUNT_NO="12345678-01",
|
|
GEMINI_API_KEY="g",
|
|
)
|
|
assert settings.PAPER_OVERSEAS_CASH == 25000.0
|
|
del os.environ["PAPER_OVERSEAS_CASH"]
|
|
|
|
def test_zero_disables_fallback(self) -> None:
|
|
import os
|
|
os.environ["PAPER_OVERSEAS_CASH"] = "0"
|
|
settings = Settings(
|
|
KIS_APP_KEY="k",
|
|
KIS_APP_SECRET="s",
|
|
KIS_ACCOUNT_NO="12345678-01",
|
|
GEMINI_API_KEY="g",
|
|
)
|
|
assert settings.PAPER_OVERSEAS_CASH == 0.0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TR_ID live/paper branching — overseas (issues #201, #203)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_overseas_broker_with_mode(mode: str) -> OverseasBroker:
|
|
s = Settings(
|
|
KIS_APP_KEY="k",
|
|
KIS_APP_SECRET="s",
|
|
KIS_ACCOUNT_NO="12345678-01",
|
|
GEMINI_API_KEY="g",
|
|
DB_PATH=":memory:",
|
|
MODE=mode,
|
|
)
|
|
kis = KISBroker(s)
|
|
kis._access_token = "tok"
|
|
kis._token_expires_at = float("inf")
|
|
kis._rate_limiter.acquire = AsyncMock()
|
|
return OverseasBroker(kis)
|
|
|
|
|
|
class TestOverseasTRIDBranching:
|
|
"""get_overseas_balance and send_overseas_order must use correct TR_ID."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_overseas_balance_paper_uses_vtts3012r(self) -> None:
|
|
broker = _make_overseas_broker_with_mode("paper")
|
|
captured: list[str] = []
|
|
|
|
async def mock_auth_headers(tr_id: str) -> dict:
|
|
captured.append(tr_id)
|
|
return {"tr_id": tr_id, "authorization": "Bearer tok"}
|
|
|
|
broker._broker._auth_headers = mock_auth_headers # type: ignore[method-assign]
|
|
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"output1": [], "output2": []})
|
|
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
|
|
mock_resp.__aexit__ = AsyncMock(return_value=False)
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=mock_resp)
|
|
broker._broker._get_session = MagicMock(return_value=mock_session)
|
|
|
|
await broker.get_overseas_balance("NASD")
|
|
assert "VTTS3012R" in captured
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_overseas_balance_live_uses_ttts3012r(self) -> None:
|
|
broker = _make_overseas_broker_with_mode("live")
|
|
captured: list[str] = []
|
|
|
|
async def mock_auth_headers(tr_id: str) -> dict:
|
|
captured.append(tr_id)
|
|
return {"tr_id": tr_id, "authorization": "Bearer tok"}
|
|
|
|
broker._broker._auth_headers = mock_auth_headers # type: ignore[method-assign]
|
|
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"output1": [], "output2": []})
|
|
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
|
|
mock_resp.__aexit__ = AsyncMock(return_value=False)
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=mock_resp)
|
|
broker._broker._get_session = MagicMock(return_value=mock_session)
|
|
|
|
await broker.get_overseas_balance("NASD")
|
|
assert "TTTS3012R" in captured
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_overseas_order_buy_paper_uses_vttt1002u(self) -> None:
|
|
broker = _make_overseas_broker_with_mode("paper")
|
|
captured: list[str] = []
|
|
|
|
async def mock_auth_headers(tr_id: str) -> dict:
|
|
captured.append(tr_id)
|
|
return {"tr_id": tr_id, "authorization": "Bearer tok"}
|
|
|
|
broker._broker._auth_headers = mock_auth_headers # type: ignore[method-assign]
|
|
broker._broker._get_hash_key = AsyncMock(return_value="h") # type: ignore[method-assign]
|
|
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"rt_cd": "0", "msg1": "OK"})
|
|
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
|
|
mock_resp.__aexit__ = AsyncMock(return_value=False)
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.post = MagicMock(return_value=mock_resp)
|
|
broker._broker._get_session = MagicMock(return_value=mock_session)
|
|
|
|
await broker.send_overseas_order("NASD", "AAPL", "BUY", 1)
|
|
assert "VTTT1002U" in captured
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_overseas_order_buy_live_uses_tttt1002u(self) -> None:
|
|
broker = _make_overseas_broker_with_mode("live")
|
|
captured: list[str] = []
|
|
|
|
async def mock_auth_headers(tr_id: str) -> dict:
|
|
captured.append(tr_id)
|
|
return {"tr_id": tr_id, "authorization": "Bearer tok"}
|
|
|
|
broker._broker._auth_headers = mock_auth_headers # type: ignore[method-assign]
|
|
broker._broker._get_hash_key = AsyncMock(return_value="h") # type: ignore[method-assign]
|
|
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"rt_cd": "0", "msg1": "OK"})
|
|
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
|
|
mock_resp.__aexit__ = AsyncMock(return_value=False)
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.post = MagicMock(return_value=mock_resp)
|
|
broker._broker._get_session = MagicMock(return_value=mock_session)
|
|
|
|
await broker.send_overseas_order("NASD", "AAPL", "BUY", 1)
|
|
assert "TTTT1002U" in captured
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_overseas_order_sell_paper_uses_vttt1001u(self) -> None:
|
|
broker = _make_overseas_broker_with_mode("paper")
|
|
captured: list[str] = []
|
|
|
|
async def mock_auth_headers(tr_id: str) -> dict:
|
|
captured.append(tr_id)
|
|
return {"tr_id": tr_id, "authorization": "Bearer tok"}
|
|
|
|
broker._broker._auth_headers = mock_auth_headers # type: ignore[method-assign]
|
|
broker._broker._get_hash_key = AsyncMock(return_value="h") # type: ignore[method-assign]
|
|
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"rt_cd": "0", "msg1": "OK"})
|
|
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
|
|
mock_resp.__aexit__ = AsyncMock(return_value=False)
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.post = MagicMock(return_value=mock_resp)
|
|
broker._broker._get_session = MagicMock(return_value=mock_session)
|
|
|
|
await broker.send_overseas_order("NASD", "AAPL", "SELL", 1)
|
|
assert "VTTT1001U" in captured
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_overseas_order_sell_live_uses_tttt1006u(self) -> None:
|
|
broker = _make_overseas_broker_with_mode("live")
|
|
captured: list[str] = []
|
|
|
|
async def mock_auth_headers(tr_id: str) -> dict:
|
|
captured.append(tr_id)
|
|
return {"tr_id": tr_id, "authorization": "Bearer tok"}
|
|
|
|
broker._broker._auth_headers = mock_auth_headers # type: ignore[method-assign]
|
|
broker._broker._get_hash_key = AsyncMock(return_value="h") # type: ignore[method-assign]
|
|
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"rt_cd": "0", "msg1": "OK"})
|
|
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
|
|
mock_resp.__aexit__ = AsyncMock(return_value=False)
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.post = MagicMock(return_value=mock_resp)
|
|
broker._broker._get_session = MagicMock(return_value=mock_session)
|
|
|
|
await broker.send_overseas_order("NASD", "AAPL", "SELL", 1)
|
|
assert "TTTT1006U" in captured
|
|
|
|
|
|
class TestGetOverseasPendingOrders:
|
|
"""Tests for get_overseas_pending_orders method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_paper_mode_returns_empty(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""Paper mode should immediately return [] without any API call."""
|
|
# Default mock_settings has MODE="paper"
|
|
overseas_broker._broker._settings = overseas_broker._broker._settings.model_copy(
|
|
update={"MODE": "paper"}
|
|
)
|
|
mock_session = MagicMock()
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
|
|
result = await overseas_broker.get_overseas_pending_orders("NASD")
|
|
|
|
assert result == []
|
|
mock_session.get.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_live_mode_calls_ttts3018r_with_correct_params(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""Live mode should call TTTS3018R with OVRS_EXCG_CD and return output list."""
|
|
overseas_broker._broker._settings = overseas_broker._broker._settings.model_copy(
|
|
update={"MODE": "live"}
|
|
)
|
|
captured_tr_id: list[str] = []
|
|
captured_params: list[dict] = []
|
|
|
|
async def mock_auth_headers(tr_id: str) -> dict:
|
|
captured_tr_id.append(tr_id)
|
|
return {}
|
|
|
|
overseas_broker._broker._auth_headers = mock_auth_headers # type: ignore[method-assign]
|
|
|
|
pending_orders = [
|
|
{"odno": "001", "pdno": "AAPL", "sll_buy_dvsn_cd": "02", "nccs_qty": "5"}
|
|
]
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"output": pending_orders})
|
|
|
|
mock_session = MagicMock()
|
|
|
|
def _capture_get(url: str, **kwargs: object) -> MagicMock:
|
|
captured_params.append(kwargs.get("params", {}))
|
|
return _make_async_cm(mock_resp)
|
|
|
|
mock_session.get = MagicMock(side_effect=_capture_get)
|
|
overseas_broker._broker._rate_limiter.acquire = AsyncMock()
|
|
overseas_broker._broker._get_session = MagicMock(return_value=mock_session)
|
|
|
|
result = await overseas_broker.get_overseas_pending_orders("NASD")
|
|
|
|
assert result == pending_orders
|
|
assert captured_tr_id == ["TTTS3018R"]
|
|
assert captured_params[0]["OVRS_EXCG_CD"] == "NASD"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_live_mode_connection_error(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""Network error in live mode should raise ConnectionError."""
|
|
overseas_broker._broker._settings = overseas_broker._broker._settings.model_copy(
|
|
update={"MODE": "live"}
|
|
)
|
|
cm = MagicMock()
|
|
cm.__aenter__ = AsyncMock(side_effect=aiohttp.ClientError("timeout"))
|
|
cm.__aexit__ = AsyncMock(return_value=False)
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.get = MagicMock(return_value=cm)
|
|
_setup_broker_mocks(overseas_broker, mock_session)
|
|
|
|
with pytest.raises(ConnectionError, match="Network error fetching pending orders"):
|
|
await overseas_broker.get_overseas_pending_orders("NASD")
|
|
|
|
|
|
class TestCancelOverseasOrder:
|
|
"""Tests for cancel_overseas_order method."""
|
|
|
|
def _setup_cancel_mocks(
|
|
self, overseas_broker: OverseasBroker, response: dict
|
|
) -> tuple[list[str], MagicMock]:
|
|
"""Wire up mocks for a successful cancel call; return captured TR_IDs and session."""
|
|
captured_tr_ids: list[str] = []
|
|
|
|
async def mock_auth_headers(tr_id: str) -> dict:
|
|
captured_tr_ids.append(tr_id)
|
|
return {}
|
|
|
|
overseas_broker._broker._auth_headers = mock_auth_headers # type: ignore[method-assign]
|
|
overseas_broker._broker._get_hash_key = AsyncMock(return_value="hash_val") # type: ignore[method-assign]
|
|
overseas_broker._broker._rate_limiter.acquire = AsyncMock()
|
|
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value=response)
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.post = MagicMock(return_value=_make_async_cm(mock_resp))
|
|
overseas_broker._broker._get_session = MagicMock(return_value=mock_session)
|
|
|
|
return captured_tr_ids, mock_session
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_us_live_uses_tttt1004u(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""US exchange in live mode should use TTTT1004U."""
|
|
overseas_broker._broker._settings = overseas_broker._broker._settings.model_copy(
|
|
update={"MODE": "live"}
|
|
)
|
|
captured, _ = self._setup_cancel_mocks(
|
|
overseas_broker, {"rt_cd": "0", "msg1": "OK"}
|
|
)
|
|
|
|
await overseas_broker.cancel_overseas_order("NASD", "AAPL", "ORD001", 5)
|
|
|
|
assert "TTTT1004U" in captured
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_us_paper_uses_vttt1004u(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""US exchange in paper mode should use VTTT1004U."""
|
|
# Default mock_settings has MODE="paper"
|
|
captured, _ = self._setup_cancel_mocks(
|
|
overseas_broker, {"rt_cd": "0", "msg1": "OK"}
|
|
)
|
|
|
|
await overseas_broker.cancel_overseas_order("NASD", "AAPL", "ORD001", 5)
|
|
|
|
assert "VTTT1004U" in captured
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_hk_live_uses_ttts1003u(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""SEHK exchange in live mode should use TTTS1003U."""
|
|
overseas_broker._broker._settings = overseas_broker._broker._settings.model_copy(
|
|
update={"MODE": "live"}
|
|
)
|
|
captured, _ = self._setup_cancel_mocks(
|
|
overseas_broker, {"rt_cd": "0", "msg1": "OK"}
|
|
)
|
|
|
|
await overseas_broker.cancel_overseas_order("SEHK", "0700", "ORD002", 10)
|
|
|
|
assert "TTTS1003U" in captured
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancel_sets_rvse_cncl_dvsn_cd_02(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""Cancel body must include RVSE_CNCL_DVSN_CD='02' and OVRS_ORD_UNPR='0'."""
|
|
captured_body: list[dict] = []
|
|
|
|
async def mock_auth_headers(tr_id: str) -> dict:
|
|
return {}
|
|
|
|
overseas_broker._broker._auth_headers = mock_auth_headers # type: ignore[method-assign]
|
|
overseas_broker._broker._get_hash_key = AsyncMock(return_value="h") # type: ignore[method-assign]
|
|
overseas_broker._broker._rate_limiter.acquire = AsyncMock()
|
|
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"rt_cd": "0"})
|
|
|
|
mock_session = MagicMock()
|
|
|
|
def _capture_post(url: str, **kwargs: object) -> MagicMock:
|
|
captured_body.append(kwargs.get("json", {}))
|
|
return _make_async_cm(mock_resp)
|
|
|
|
mock_session.post = MagicMock(side_effect=_capture_post)
|
|
overseas_broker._broker._get_session = MagicMock(return_value=mock_session)
|
|
|
|
await overseas_broker.cancel_overseas_order("NASD", "AAPL", "ORD003", 3)
|
|
|
|
assert captured_body[0]["RVSE_CNCL_DVSN_CD"] == "02"
|
|
assert captured_body[0]["OVRS_ORD_UNPR"] == "0"
|
|
assert captured_body[0]["ORGN_ODNO"] == "ORD003"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancel_sets_hashkey_header(
|
|
self, overseas_broker: OverseasBroker
|
|
) -> None:
|
|
"""hashkey must be set in the request headers."""
|
|
captured_headers: list[dict] = []
|
|
overseas_broker._broker._get_hash_key = AsyncMock(return_value="test_hash") # type: ignore[method-assign]
|
|
overseas_broker._broker._rate_limiter.acquire = AsyncMock()
|
|
|
|
async def mock_auth_headers(tr_id: str) -> dict:
|
|
return {"tr_id": tr_id}
|
|
|
|
overseas_broker._broker._auth_headers = mock_auth_headers # type: ignore[method-assign]
|
|
|
|
mock_resp = AsyncMock()
|
|
mock_resp.status = 200
|
|
mock_resp.json = AsyncMock(return_value={"rt_cd": "0"})
|
|
|
|
mock_session = MagicMock()
|
|
|
|
def _capture_post(url: str, **kwargs: object) -> MagicMock:
|
|
captured_headers.append(dict(kwargs.get("headers", {})))
|
|
return _make_async_cm(mock_resp)
|
|
|
|
mock_session.post = MagicMock(side_effect=_capture_post)
|
|
overseas_broker._broker._get_session = MagicMock(return_value=mock_session)
|
|
|
|
await overseas_broker.cancel_overseas_order("NASD", "AAPL", "ORD004", 2)
|
|
|
|
assert captured_headers[0].get("hashkey") == "test_hash"
|