import httpx import pytest import respx from arbitrade.config.settings import Settings from arbitrade.exchange.kraken_rest import KrakenRestClient @pytest.mark.asyncio async def test_server_time_success() -> None: settings = Settings(_env_file=None) client = KrakenRestClient(settings) with respx.mock(base_url=settings.kraken_rest_url) as mock_router: mock_router.get("/0/public/Time").respond( 200, json={"error": [], "result": {"unixtime": 1}}, ) payload = await client.server_time() await client.close() assert payload["unixtime"] == 1 @pytest.mark.asyncio async def test_retry_then_success() -> None: settings = Settings( _env_file=None, kraken_retry_attempts=2, kraken_retry_base_delay_seconds=0.0, ) client = KrakenRestClient(settings) with respx.mock(base_url=settings.kraken_rest_url) as mock_router: route = mock_router.get("/0/public/Time") route.side_effect = [ httpx.ConnectError("boom"), httpx.Response(200, json={"error": [], "result": {"unixtime": 2}}), ] payload = await client.server_time() await client.close() assert payload["unixtime"] == 2 @pytest.mark.asyncio async def test_balances_private_call_uses_headers() -> None: settings = Settings( _env_file=None, KRAKEN_API_KEY="key", KRAKEN_API_SECRET="c2VjcmV0", # base64("secret") kraken_private_rate_limit_seconds=0.0, ) client = KrakenRestClient(settings) with respx.mock(base_url=settings.kraken_rest_url) as mock_router: route = mock_router.post("/0/private/Balance").respond( 200, json={"error": [], "result": {"ZUSD": "10.0"}}, ) payload = await client.balances() await client.close() request = route.calls.last.request assert request.headers.get("API-Key") == "key" assert request.headers.get("API-Sign") assert payload["ZUSD"] == "10.0" @pytest.mark.asyncio async def test_balances_requires_credentials() -> None: settings = Settings( _env_file=None, KRAKEN_API_KEY=None, KRAKEN_API_SECRET=None, kraken_private_rate_limit_seconds=0.0, ) client = KrakenRestClient(settings) with pytest.raises(RuntimeError, match="Missing Kraken API credentials"): await client.balances() await client.close() def test_compliance_default_ok() -> None: settings = Settings(_env_file=None) client = KrakenRestClient(settings) issues = client.validate_compliance() assert issues == [] def test_compliance_detects_insecure_config() -> None: settings = Settings( _env_file=None, KRAKEN_REST_URL="http://api.kraken.com", KRAKEN_PRIVATE_RATE_LIMIT_SECONDS=0.0, KRAKEN_RETRY_ATTEMPTS=0, KRAKEN_RETRY_BASE_DELAY_SECONDS=-1.0, ) client = KrakenRestClient(settings) issues = client.validate_compliance() assert any("https://" in issue for issue in issues) assert any("below 1.0" in issue for issue in issues) assert any("ATTEMPTS" in issue for issue in issues) assert any("BASE_DELAY" in issue for issue in issues) @pytest.mark.asyncio async def test_place_market_order_posts_add_order_payload() -> None: settings = Settings( _env_file=None, KRAKEN_API_KEY="key", KRAKEN_API_SECRET="c2VjcmV0", # base64("secret") kraken_private_rate_limit_seconds=0.0, ) client = KrakenRestClient(settings) with respx.mock(base_url=settings.kraken_rest_url) as mock_router: route = mock_router.post("/0/private/AddOrder").respond( 200, json={"error": [], "result": {"txid": ["m1"]}}, ) payload = await client.place_market_order( pair="XBTUSD", side="buy", volume=0.05, ) await client.close() request_body = route.calls.last.request.content.decode() assert "pair=XBTUSD" in request_body assert "type=buy" in request_body assert "ordertype=market" in request_body assert "volume=0.05" in request_body assert payload["txid"] == ["m1"] @pytest.mark.asyncio async def test_place_limit_order_posts_add_order_payload() -> None: settings = Settings( _env_file=None, KRAKEN_API_KEY="key", KRAKEN_API_SECRET="c2VjcmV0", # base64("secret") kraken_private_rate_limit_seconds=0.0, ) client = KrakenRestClient(settings) with respx.mock(base_url=settings.kraken_rest_url) as mock_router: route = mock_router.post("/0/private/AddOrder").respond( 200, json={"error": [], "result": {"txid": ["l1"]}}, ) payload = await client.place_limit_order( pair="ETHUSD", side="sell", volume=1.5, price=3500.0, ) await client.close() request_body = route.calls.last.request.content.decode() assert "pair=ETHUSD" in request_body assert "type=sell" in request_body assert "ordertype=limit" in request_body assert "price=3500.0" in request_body assert "volume=1.5" in request_body assert payload["txid"] == ["l1"] @pytest.mark.asyncio async def test_place_order_validates_inputs() -> None: settings = Settings( _env_file=None, KRAKEN_API_KEY="key", KRAKEN_API_SECRET="c2VjcmV0", # base64("secret") kraken_private_rate_limit_seconds=0.0, ) client = KrakenRestClient(settings) with pytest.raises(ValueError, match="side"): await client.place_market_order(pair="XBTUSD", side="hold", volume=0.1) with pytest.raises(ValueError, match="volume"): await client.place_market_order(pair="XBTUSD", side="buy", volume=0.0) with pytest.raises(ValueError, match="price"): await client.place_limit_order( pair="XBTUSD", side="buy", volume=0.1, price=0.0, ) await client.close() @pytest.mark.asyncio async def test_query_order_posts_query_orders_payload() -> None: settings = Settings( _env_file=None, KRAKEN_API_KEY="key", KRAKEN_API_SECRET="c2VjcmV0", # base64("secret") kraken_private_rate_limit_seconds=0.0, ) client = KrakenRestClient(settings) with respx.mock(base_url=settings.kraken_rest_url) as mock_router: route = mock_router.post("/0/private/QueryOrders").respond( 200, json={"error": [], "result": {"order-1": {"status": "closed"}}}, ) payload = await client.query_order(order_id="order-1", include_trades=False) await client.close() request_body = route.calls.last.request.content.decode() assert "txid=order-1" in request_body assert "trades=false" in request_body assert payload["order-1"]["status"] == "closed" @pytest.mark.asyncio async def test_cancel_order_posts_cancel_order_payload() -> None: settings = Settings( _env_file=None, KRAKEN_API_KEY="key", KRAKEN_API_SECRET="c2VjcmV0", # base64("secret") kraken_private_rate_limit_seconds=0.0, ) client = KrakenRestClient(settings) with respx.mock(base_url=settings.kraken_rest_url) as mock_router: route = mock_router.post("/0/private/CancelOrder").respond( 200, json={"error": [], "result": {"count": 1}}, ) payload = await client.cancel_order(order_id="order-1") await client.close() request_body = route.calls.last.request.content.decode() assert "txid=order-1" in request_body assert payload["count"] == 1