forked from Rockachopa/Timmy-time-dashboard
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local> Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
148 lines
5.3 KiB
Python
148 lines
5.3 KiB
Python
"""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
|