Initialized openttd-client repo
This commit is contained in:
65
tests/test_coverage.py
Normal file
65
tests/test_coverage.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
import struct
|
||||
from openttd.protocol import OpenTTDProtocol, PacketGameType
|
||||
from openttd.client import OpenTTDClient
|
||||
|
||||
class MockTransport:
|
||||
def __init__(self): self._closing = False
|
||||
def is_closing(self): return self._closing
|
||||
def close(self): self._closing = True
|
||||
def write(self, data): return len(data)
|
||||
|
||||
class MockProtocol:
|
||||
async def send_packet(self, data):
|
||||
raise Exception("Send failed")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_connect_exception(monkeypatch):
|
||||
# Coverage for client.py:51-53
|
||||
client = OpenTTDClient(host="127.0.0.1")
|
||||
async def mock_fail(*args, **kwargs):
|
||||
raise Exception("Async Failure")
|
||||
monkeypatch.setattr(asyncio.get_running_loop(), "create_connection", mock_fail)
|
||||
with pytest.raises(Exception, match="Async Failure"):
|
||||
await client.connect()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_quit_exception():
|
||||
# Coverage for client.py:76
|
||||
client = OpenTTDClient(host="127.0.0.1")
|
||||
client._transport = MockTransport()
|
||||
client._protocol = MockProtocol() # send_packet raises
|
||||
await client.quit() # Should hit the except: pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_no_company_password():
|
||||
# Coverage for client.py:126-127
|
||||
client = OpenTTDClient(host="127.0.0.1")
|
||||
# _company_password is empty by default
|
||||
await client.receive_ServerNeedCompanyPassword(None, 0, "srv")
|
||||
# Should log error and return
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_chat_no_callback():
|
||||
# Coverage for client.py:162
|
||||
client = OpenTTDClient(host="127.0.0.1")
|
||||
# on_chat is None
|
||||
await client.receive_ServerChat(None, 1, "Hello")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_protocol_receive_exception():
|
||||
# Coverage for protocol.py:74-75
|
||||
# Trigger exception in receive_packet loop
|
||||
class BadHandler:
|
||||
encryption_enabled = False
|
||||
proto = OpenTTDProtocol(BadHandler())
|
||||
# data too short for read_uint16
|
||||
res = proto.receive_packet(None, memoryview(b"\x01"))
|
||||
assert res == (PacketGameType.ServerUnused, {})
|
||||
|
||||
def test_protocol_welcome_parser():
|
||||
# Coverage for protocol.py:110-112
|
||||
data = memoryview(struct.pack("<I", 42))
|
||||
res = OpenTTDProtocol.receive_ServerWelcome(None, data)
|
||||
assert res == {"client_id": 42}
|
||||
54
tests/test_e2e.py
Normal file
54
tests/test_e2e.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import asyncio
|
||||
import pytest
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add lib to path
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'lib'))
|
||||
|
||||
from openttd import OpenTTDClient
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_connection_and_join():
|
||||
# Configuration matches your local server
|
||||
SERVER_IP = "127.0.0.1"
|
||||
SERVER_PW = "asd"
|
||||
COMPANY_ID = 0
|
||||
COMPANY_PW = "asd123"
|
||||
|
||||
client = OpenTTDClient(host=SERVER_IP, username="TestRunner")
|
||||
|
||||
# Track chat for coverage
|
||||
chat_received = asyncio.Event()
|
||||
def chat_handler(cid, msg):
|
||||
chat_received.set()
|
||||
client.on_chat = chat_handler
|
||||
|
||||
try:
|
||||
# 1. Connect
|
||||
await client.connect(server_password=SERVER_PW)
|
||||
|
||||
# 2. Join company
|
||||
await client.join_company(company_id=COMPANY_ID, company_password=COMPANY_PW)
|
||||
|
||||
# 3. Wait for join (timeout after 15s to be safe)
|
||||
await asyncio.wait_for(client.joined.wait(), timeout=15.0)
|
||||
|
||||
assert client.joined.is_set()
|
||||
assert client.client_id is not None
|
||||
|
||||
# 4. Stay briefly to ensure keep-alive/frames work
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# 5. Graceful Quit
|
||||
await client.quit()
|
||||
|
||||
# 6. Wait for shutdown event
|
||||
await asyncio.wait_for(client.shutdown_event.wait(), timeout=5.0)
|
||||
assert client.shutdown_event.is_set()
|
||||
|
||||
except Exception as e:
|
||||
pytest.fail(f"E2E Test failed: {e}")
|
||||
finally:
|
||||
if not client.shutdown_event.is_set():
|
||||
await client.quit()
|
||||
167
tests/test_logic.py
Normal file
167
tests/test_logic.py
Normal file
@@ -0,0 +1,167 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
import hashlib
|
||||
from openttd.protocol import PacketGameType
|
||||
from openttd.client import OpenTTDClient
|
||||
|
||||
def test_packet_game_type_values():
|
||||
assert PacketGameType.ServerFull == 0
|
||||
assert PacketGameType.ClientJoin == 2
|
||||
assert PacketGameType.ServerWelcome == 21
|
||||
assert PacketGameType.ClientQuit == 47
|
||||
|
||||
def test_company_password_hashing():
|
||||
password = "asd123"
|
||||
server_id = "c14cf984cecd354df72ccdcb338cf547"
|
||||
seed = 2064088478
|
||||
|
||||
salted = bytearray()
|
||||
p_bytes = password.encode('utf-8')
|
||||
s_bytes = server_id.encode('utf-8')
|
||||
for i in range(32):
|
||||
p_char = p_bytes[i] if i < len(p_bytes) else 0
|
||||
s_char = s_bytes[i] if i < len(s_bytes) else 0
|
||||
seed_char = (seed >> (i % 32)) & 0xFF
|
||||
salted.append(p_char ^ s_char ^ seed_char)
|
||||
expected_hash = hashlib.md5(salted, usedforsecurity=False).hexdigest()
|
||||
assert len(expected_hash) == 32
|
||||
|
||||
class MockTransport:
|
||||
def __init__(self): self._closing = False
|
||||
def is_closing(self): return self._closing
|
||||
def close(self): self._closing = True
|
||||
def write(self, data): return len(data)
|
||||
|
||||
class MockProtocol:
|
||||
def __init__(self): self.sent = []
|
||||
async def send_packet(self, data):
|
||||
self.sent.append(data)
|
||||
return len(data)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_connect_success(monkeypatch):
|
||||
# Coverage for client.py:48-50
|
||||
client = OpenTTDClient(host="127.0.0.1")
|
||||
|
||||
class FakeProto:
|
||||
def __init__(self): self.sent = []
|
||||
async def send_packet(self, data): self.sent.append(data)
|
||||
|
||||
proto = FakeProto()
|
||||
async def mock_success(*args, **kwargs):
|
||||
return MockTransport(), proto
|
||||
|
||||
monkeypatch.setattr(asyncio.get_running_loop(), "create_connection", mock_success)
|
||||
|
||||
await client.connect()
|
||||
assert len(proto.sent) == 1 # ClientGameInfo sent
|
||||
assert client._protocol == proto
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_connect_failure(monkeypatch):
|
||||
client = OpenTTDClient(host="127.0.0.1")
|
||||
async def mock_fail(*args, **kwargs):
|
||||
raise Exception("Async Failure")
|
||||
monkeypatch.setattr(asyncio.get_running_loop(), "create_connection", mock_fail)
|
||||
with pytest.raises(Exception, match="Async Failure"):
|
||||
await client.connect()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_error_handling():
|
||||
client = OpenTTDClient(host="127.0.0.1")
|
||||
client._transport = MockTransport()
|
||||
client._protocol = MockProtocol()
|
||||
|
||||
await client.receive_ServerError(None, 8) # WrongRevision
|
||||
assert client.shutdown_event.is_set()
|
||||
|
||||
client.shutdown_event.clear()
|
||||
await client.receive_ServerError(None, 10) # WrongPassword
|
||||
await client.receive_ServerError(None, 11) # NameInUse
|
||||
await client.receive_ServerError(None, 17) # Timeout
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_server_full_banned():
|
||||
client = OpenTTDClient(host="127.0.0.1")
|
||||
client._transport = MockTransport()
|
||||
client._protocol = MockProtocol()
|
||||
|
||||
await client.receive_ServerFull(None)
|
||||
await client.receive_ServerBanned(None)
|
||||
client.connected(None)
|
||||
client.disconnect(None)
|
||||
assert client.shutdown_event.is_set()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chat_callback():
|
||||
client = OpenTTDClient(host="127.0.0.1")
|
||||
received = []
|
||||
def on_chat(cid, msg):
|
||||
received.append((cid, msg))
|
||||
client.on_chat = on_chat
|
||||
|
||||
await client.receive_ServerChat(None, 42, "Hello World")
|
||||
assert received == [(42, "Hello World")]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fallback_handlers():
|
||||
client = OpenTTDClient(host="127.0.0.1")
|
||||
client.log.setLevel(100)
|
||||
|
||||
await client.receive_ServerUnused(None)
|
||||
await client.receive_ServerSync(None)
|
||||
await client.receive_ServerClientJoined(None)
|
||||
await client.receive_ServerMapBegin(None)
|
||||
await client.receive_ServerMapSize(None, size=100)
|
||||
await client.receive_ServerMapData(None, data=b"data")
|
||||
await client.receive_ServerConfigurationUpdate(None)
|
||||
await client.receive_ServerClientInfo(None)
|
||||
await client.receive_ServerExternalChat(None)
|
||||
await client.receive_ServerCommand(None)
|
||||
await client.receive_ClientAck(None)
|
||||
await client.receive_ClientIdentify(None)
|
||||
await client.receive_ServerCompanyUpdate(None)
|
||||
|
||||
client.joined.set()
|
||||
await client.join_company(0)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_full_handshake_flow():
|
||||
client = OpenTTDClient(host="127.0.0.1", username="TestUser")
|
||||
client._protocol = MockProtocol()
|
||||
client._transport = MockTransport()
|
||||
|
||||
await client.receive_ServerGameInfo(None, name="TestSrv", openttd_version="14.0")
|
||||
assert len(client._protocol.sent) == 1
|
||||
|
||||
pake_data = b"S" * 32 + b"N" * 24
|
||||
await client.receive_ServerAuthenticationRequest(None, 1, pake_data)
|
||||
assert len(client._protocol.sent) == 2
|
||||
|
||||
await client.receive_ServerEnableEncryption(None, b"E" * 24)
|
||||
assert client.encryption_enabled
|
||||
assert len(client._protocol.sent) == 3
|
||||
|
||||
await client.receive_ServerCheckNewGRFs(None)
|
||||
assert len(client._protocol.sent) == 4
|
||||
|
||||
await client.join_company(0, "comp_pw")
|
||||
await client.receive_ServerNeedCompanyPassword(None, 1234, "srv_id")
|
||||
assert len(client._protocol.sent) == 5
|
||||
|
||||
await client.receive_ServerWelcome(None, client_id=42)
|
||||
assert client.client_id == 42
|
||||
assert len(client._protocol.sent) == 6
|
||||
|
||||
await client.receive_ServerMapDone(None)
|
||||
assert len(client._protocol.sent) == 7
|
||||
assert client.joined.is_set()
|
||||
|
||||
await client.receive_ServerFrame(None, 100, 7)
|
||||
assert len(client._protocol.sent) == 8
|
||||
|
||||
await client.quit()
|
||||
assert len(client._protocol.sent) == 9
|
||||
|
||||
client._transport.close()
|
||||
await client.quit()
|
||||
128
tests/test_protocol.py
Normal file
128
tests/test_protocol.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import pytest
|
||||
import struct
|
||||
import monocypher
|
||||
from openttd.protocol import OpenTTDProtocol, PacketGameType
|
||||
from openttd_protocol.wire.exceptions import SocketClosed
|
||||
|
||||
class MockTransport:
|
||||
def __init__(self): self._closing = False
|
||||
def is_closing(self): return self._closing
|
||||
def close(self): self._closing = True
|
||||
def write(self, data): return len(data)
|
||||
|
||||
class MockHandler:
|
||||
def __init__(self):
|
||||
self.encryption_enabled = False
|
||||
self._recv_aead = None
|
||||
self._send_aead = None
|
||||
self._session_key_recv = b"A" * 32
|
||||
self._session_key_send = b"B" * 32
|
||||
self._encryption_nonce = b"C" * 24
|
||||
|
||||
async def receive_ServerUnused(self, source, **kwargs): pass
|
||||
async def receive_ClientAck(self, source, **kwargs): pass
|
||||
|
||||
def test_protocol_static_parsers():
|
||||
data = memoryview(struct.pack("<BI B", 1, 42, 0) + b"Hello\x00")
|
||||
res = OpenTTDProtocol.receive_ServerChat(None, data)
|
||||
assert res["client_id"] == 42
|
||||
assert res["message"] == "Hello"
|
||||
|
||||
data = memoryview(struct.pack("<I", 123))
|
||||
res = OpenTTDProtocol.receive_ServerWelcome(None, data)
|
||||
assert res["client_id"] == 123
|
||||
|
||||
data = memoryview(struct.pack("<II", 1000, 2000) + b"\x00" * 12 + b"\x07")
|
||||
res = OpenTTDProtocol.receive_ServerFrame(None, data)
|
||||
assert res["frame"] == 1000
|
||||
assert res["token"] == 7
|
||||
|
||||
assert OpenTTDProtocol.receive_ServerExternalChat(None, b"") == {}
|
||||
assert OpenTTDProtocol.receive_ServerCommand(None, b"") == {}
|
||||
assert OpenTTDProtocol.receive_ServerFull(None, b"") == {}
|
||||
assert OpenTTDProtocol.receive_ServerBanned(None, b"") == {}
|
||||
assert OpenTTDProtocol.receive_ClientIdentify(None, b"") == {}
|
||||
assert OpenTTDProtocol.receive_ClientAck(None, struct.pack("<IB", 1, 2)) == {"frame": 1, "token": 2}
|
||||
assert OpenTTDProtocol.receive_ServerEnableEncryption(None, b"data") == {"data": b"data"}
|
||||
assert OpenTTDProtocol.receive_ServerCheckNewGRFs(None, b"") == {}
|
||||
assert OpenTTDProtocol.receive_ServerUnused(None, b"") == {}
|
||||
assert OpenTTDProtocol.receive_ServerMapDone(None, b"") == {}
|
||||
assert OpenTTDProtocol.receive_ServerClientInfo(None, b"") == {}
|
||||
assert OpenTTDProtocol.receive_ServerSync(None, b"") == {}
|
||||
assert OpenTTDProtocol.receive_ServerClientJoined(None, b"") == {}
|
||||
assert OpenTTDProtocol.receive_ServerMapBegin(None, b"") == {}
|
||||
assert OpenTTDProtocol.receive_ServerMapSize(None, b"") == {"size": 0}
|
||||
assert OpenTTDProtocol.receive_ServerMapData(None, b"data") == {"data": b"data"}
|
||||
assert OpenTTDProtocol.receive_ServerConfigurationUpdate(None, b"") == {}
|
||||
assert OpenTTDProtocol.receive_ServerAuthenticationRequest(None, struct.pack("<B", 1) + b"data") == {"auth_type": 1, "data": b"data"}
|
||||
assert OpenTTDProtocol.receive_ServerError(None, b"\x08") == {"error_code": 8}
|
||||
assert OpenTTDProtocol.receive_ServerCompanyUpdate(None, b"\x01\x00") == {"passworded_mask": 1}
|
||||
assert OpenTTDProtocol.receive_ServerNeedCompanyPassword(None, memoryview(struct.pack("<I", 1234) + b"sid\x00")) == {"seed": 1234, "server_id": "sid"}
|
||||
|
||||
# Coverage for receive_ServerGameInfo
|
||||
try:
|
||||
OpenTTDProtocol.receive_ServerGameInfo(None, memoryview(b"\x00" * 200))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_protocol_exception_handling():
|
||||
handler = MockHandler()
|
||||
proto = OpenTTDProtocol(handler)
|
||||
proto.transport = MockTransport()
|
||||
|
||||
# Passing data that causes struct.unpack to fail (too short for uint16)
|
||||
ptype, kwargs = proto.receive_packet(None, memoryview(b"\x01"))
|
||||
assert ptype == PacketGameType.ServerUnused
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_protocol_encryption_logic():
|
||||
handler = MockHandler()
|
||||
handler.encryption_enabled = True
|
||||
proto = OpenTTDProtocol(handler)
|
||||
proto.transport = MockTransport()
|
||||
proto._can_write.set()
|
||||
|
||||
# Send test
|
||||
payload = b"\x03\x00\x0e"
|
||||
written_len = await proto.send_packet(payload)
|
||||
# len is 19 because [len 2] + [mac 16] + [data 1]
|
||||
assert written_len == 19
|
||||
|
||||
# Decryption test:
|
||||
# Use ClientAck (32) as inner payload: [uint8 type] [uint32 frame] [uint8 token]
|
||||
inner_payload = struct.pack("<B I B", 32, 1234, 7)
|
||||
|
||||
locker = monocypher.IncrementalAuthenticatedEncryption(handler._session_key_recv, handler._encryption_nonce)
|
||||
mac, ciphertext = locker.lock(inner_payload)
|
||||
|
||||
handler._recv_aead = monocypher.IncrementalAuthenticatedEncryption(handler._session_key_recv, handler._encryption_nonce)
|
||||
wire_data = memoryview(struct.pack("<H", len(mac) + len(ciphertext) + 2) + mac + ciphertext)
|
||||
|
||||
ptype, kwargs = proto.receive_packet(None, wire_data)
|
||||
assert ptype == PacketGameType.ClientAck
|
||||
assert kwargs["frame"] == 1234
|
||||
assert kwargs["token"] == 7
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_protocol_decryption_failure():
|
||||
handler = MockHandler()
|
||||
handler.encryption_enabled = True
|
||||
proto = OpenTTDProtocol(handler)
|
||||
proto.transport = MockTransport()
|
||||
|
||||
# Needs to be at least 18 bytes for read_uint16 + mac
|
||||
wire_data = memoryview(b"\x14\x00" + b"X" * 16 + b"junk")
|
||||
ptype, kwargs = proto.receive_packet(None, wire_data)
|
||||
assert ptype == PacketGameType.ServerUnused
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_protocol_is_closing_failure():
|
||||
handler = MockHandler()
|
||||
proto = OpenTTDProtocol(handler)
|
||||
proto.transport = MockTransport()
|
||||
proto.transport.close()
|
||||
proto._can_write.set()
|
||||
|
||||
with pytest.raises(SocketClosed):
|
||||
await proto.send_packet(b"\x02\x00")
|
||||
Reference in New Issue
Block a user