"""Unit tests for bannerlord.gabs_client — TCP JSON-RPC client.""" import asyncio import json from unittest.mock import AsyncMock, MagicMock, patch import pytest from bannerlord.gabs_client import GABSClient, GABSError, GABSUnavailable # ── Connection ──────────────────────────────────────────────────────────────── class TestGABSClientConnection: async def test_connect_success(self): mock_reader = AsyncMock() mock_writer = MagicMock() mock_writer.close = MagicMock() mock_writer.wait_closed = AsyncMock() with patch( "bannerlord.gabs_client.asyncio.open_connection", return_value=(mock_reader, mock_writer), ): client = GABSClient() await client.connect() assert client.connected is True await client.close() async def test_connect_failure_degrades_gracefully(self): with patch( "bannerlord.gabs_client.asyncio.open_connection", side_effect=OSError("Connection refused"), ): client = GABSClient() await client.connect() # must not raise assert client.connected is False async def test_connect_timeout_degrades_gracefully(self): with patch( "bannerlord.gabs_client.asyncio.open_connection", side_effect=asyncio.TimeoutError(), ): client = GABSClient() await client.connect() assert client.connected is False async def test_context_manager(self): mock_reader = AsyncMock() mock_writer = MagicMock() mock_writer.close = MagicMock() mock_writer.wait_closed = AsyncMock() with patch( "bannerlord.gabs_client.asyncio.open_connection", return_value=(mock_reader, mock_writer), ): async with GABSClient() as client: assert client.connected is True assert client.connected is False # ── RPC ─────────────────────────────────────────────────────────────────────── class TestGABSClientRPC: def _make_connected_client(self, response_data: dict): """Return a client with mocked reader/writer.""" client = GABSClient() client._connected = True raw_response = json.dumps(response_data) + "\n" client._reader = AsyncMock() client._reader.readline = AsyncMock(return_value=raw_response.encode()) client._writer = MagicMock() client._writer.write = MagicMock() client._writer.drain = AsyncMock() return client async def test_call_returns_result(self): client = self._make_connected_client({"jsonrpc": "2.0", "id": 1, "result": {"foo": "bar"}}) result = await client.call("game.getState") assert result == {"foo": "bar"} async def test_call_raises_on_error(self): client = self._make_connected_client( {"jsonrpc": "2.0", "id": 1, "error": {"code": -32601, "message": "Method not found"}} ) with pytest.raises(GABSError, match="Method not found"): await client.call("game.nonexistent") async def test_call_raises_unavailable_when_not_connected(self): client = GABSClient() assert client.connected is False with pytest.raises(GABSUnavailable): await client.call("game.getState") async def test_sequence_increments(self): client = self._make_connected_client({"jsonrpc": "2.0", "id": 1, "result": {}}) await client.call("game.getState") assert client._seq == 1 client._reader.readline = AsyncMock( return_value=(json.dumps({"jsonrpc": "2.0", "id": 2, "result": {}}) + "\n").encode() ) await client.call("game.getState") assert client._seq == 2 async def test_get_state_calls_correct_method(self): client = self._make_connected_client( {"jsonrpc": "2.0", "id": 1, "result": {"campaign_day": 10}} ) result = await client.get_state() written = client._writer.write.call_args[0][0].decode() payload = json.loads(written.strip()) assert payload["method"] == "game.getState" assert result == {"campaign_day": 10} async def test_move_party_sends_target(self): client = self._make_connected_client( {"jsonrpc": "2.0", "id": 1, "result": {"moving": True}} ) await client.move_party("Epicrotea") written = client._writer.write.call_args[0][0].decode() payload = json.loads(written.strip()) assert payload["method"] == "party.move" assert payload["params"]["target"] == "Epicrotea" async def test_connection_lost_marks_disconnected(self): client = GABSClient() client._connected = True client._reader = AsyncMock() client._reader.readline = AsyncMock(side_effect=OSError("connection reset")) client._writer = MagicMock() client._writer.write = MagicMock() client._writer.drain = AsyncMock() with pytest.raises(GABSUnavailable): await client.call("game.getState") assert client.connected is False