Add admin port support with other major refactorations
Some checks failed
Continuous Integration / lint-and-security (pull_request) Failing after 39s
Continuous Integration / tests-and-coverage (pull_request) Successful in 24s

This commit is contained in:
2026-06-29 19:47:48 +02:00
parent df629b2922
commit aebd5f4ef5
22 changed files with 1746 additions and 67 deletions

12
tests/conftest.py Normal file
View File

@@ -0,0 +1,12 @@
import pytest
import os
@pytest.fixture(scope="session")
def server_config():
"""Provides server connection parameters from environment variables with defaults."""
return {
"host": os.getenv("OPENTTD_HOST", "127.0.0.1"),
"game_port": int(os.getenv("OPENTTD_GAME_PORT", "3979")),
"admin_port": int(os.getenv("OPENTTD_ADMIN_PORT", "3977")),
"password": os.getenv("OPENTTD_PASSWORD", "asd")
}

170
tests/test_admin.py Normal file
View File

@@ -0,0 +1,170 @@
import pytest
import asyncio
import os
import monocypher
from openttd import OpenTTDAdminClient
from openttd.protocol import PacketAdminType, AdminUpdateType, AdminUpdateFrequency
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)
def test_admin_packet_types():
assert PacketAdminType.AdminJoin == 0
assert PacketAdminType.ServerWelcome == 104
assert PacketAdminType.ServerAuthRequest == 128
@pytest.mark.asyncio
async def test_admin_client_connect_and_actions(monkeypatch):
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
assert client.host == "127.0.0.1"
assert client.port == 3977
assert client.admin_name == "TestAdmin"
# 1. Connect (secure=False)
proto = MockProtocol()
transport = MockTransport()
async def mock_connect(*args, **kwargs):
return transport, proto
monkeypatch.setattr(asyncio.get_running_loop(), "create_connection", mock_connect)
await client.connect(admin_password="asd", secure=False)
assert len(proto.sent) == 1
# 2. Connect (secure=True)
proto.sent.clear()
await client.connect(admin_password="asd", secure=True)
assert len(proto.sent) == 1
# 3. Connect Exception
async def mock_fail(*args, **kwargs):
raise Exception("Connection Failed")
monkeypatch.setattr(asyncio.get_running_loop(), "create_connection", mock_fail)
with pytest.raises(Exception, match="Connection Failed"):
await client.connect()
# Reset connection mock for further tests
client._protocol = proto
client._transport = transport
# 4. Actions
proto.sent.clear()
await client.send_rcon("help")
await client.send_chat("hello")
await client.update_frequency(AdminUpdateType.Chat, AdminUpdateFrequency.Automatic)
await client.poll(AdminUpdateType.ClientInfo)
await client.poll_clients(1)
await client.poll_companies(2)
await client.poll_economy(3)
await client.poll_stats(4)
await client.send_gamescript({"cmd": "test"})
assert len(proto.sent) == 9
# 5. Protocol callbacks
client.connected(None)
# X25519 authentication handshake callback
server_pub = monocypher.x25519_public_key(monocypher.generate_key())
nonce = os.urandom(24)
await client.receive_ServerAuthRequest(None, 1, server_pub + nonce)
assert client._session_key_send is not None
await client.receive_ServerEnableEncryption(None, os.urandom(24))
assert client.encryption_enabled is True
await client.receive_ServerProtocol(None, 3, {})
await client.receive_ServerWelcome(None, server_name="JGRPP")
assert client.joined.is_set()
# Chat callback with and without handler
chat_events = []
client.on_chat = lambda **k: chat_events.append(k)
await client.receive_ServerChat(None, client_id=1, message="hello")
assert chat_events == [{"client_id": 1, "message": "hello"}]
client.on_chat = None
await client.receive_ServerChat(None, client_id=1, message="hello")
# Console callback with and without handler
console_events = []
client.on_console = lambda **k: console_events.append(k)
await client.receive_ServerConsole(None, origin="server", text="welcome")
assert console_events == [{"origin": "server", "text": "welcome"}]
client.on_console = None
await client.receive_ServerConsole(None, origin="server", text="welcome")
# Other callbacks
await client.receive_ServerRcon(None, text="res")
await client.receive_ServerRconEnd(None, command="help")
await client.receive_ServerClientJoin(None, client_id=1)
await client.receive_ServerClientInfo(None, client_id=1, name="user", network_address="127.0.0.1")
await client.receive_ServerClientUpdate(None, client_id=1, name="user2")
await client.receive_ServerClientQuit(None, client_id=1)
await client.receive_ServerClientError(None, client_id=1, error_code=3)
await client.receive_ServerCompanyNew(None, company_id=1)
await client.receive_ServerCompanyInfo(None, company_id=1, name="company")
await client.receive_ServerCompanyUpdate(None, company_id=1)
await client.receive_ServerCompanyRemove(None, company_id=1)
await client.receive_ServerCompanyEconomy(None, company_id=1, money=1000, loan=100)
await client.receive_ServerCompanyStats(None, company_id=1, vehicles={}, stations={})
# Gamescript callback with and without handler
gs_events = []
client.on_gamescript = lambda data: gs_events.append(data)
await client.receive_ServerGamescript(None, data={"a": 1})
assert gs_events == [{"a": 1}]
client.on_gamescript = None
await client.receive_ServerGamescript(None, data={"a": 1})
await client.receive_ServerDate(None)
# Error callbacks that shut down the client
client.shutdown_event.clear()
await client.receive_ServerError(None, 5)
assert client.shutdown_event.is_set()
client.shutdown_event.clear()
await client.receive_ServerFull(None)
assert client.shutdown_event.is_set()
client.shutdown_event.clear()
await client.receive_ServerBanned(None)
assert client.shutdown_event.is_set()
client.shutdown_event.clear()
await client.receive_ServerShutdown(None)
assert client.shutdown_event.is_set()
await client.receive_ServerNewGame(None)
await client.receive_ServerPong(None, payload=123)
# 6. Disconnect
client.shutdown_event.clear()
client.disconnect(None)
assert client.shutdown_event.is_set()
# 7. Quit
client._transport = MockTransport()
await client.quit()
assert client.shutdown_event.is_set()
# 8. Quit Exception
class BadProtocol:
async def send_packet(self, data):
raise Exception("Fail")
client._transport = MockTransport()
client._protocol = BadProtocol()
await client.quit()
assert client.shutdown_event.is_set()

View File

@@ -1,54 +1,329 @@
import asyncio
import pytest
import pytest_asyncio
import sys
import os
import random
from unittest.mock import MagicMock, AsyncMock
# Add lib to path
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'lib'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'lib'))
from openttd import OpenTTDClient
from openttd import OpenTTDClient, OpenTTDAdminClient
from openttd.protocol import (
OpenTTDProtocol,
OpenTTDAdminProtocol,
AdminUpdateType,
AdminUpdateFrequency,
PacketGameType,
PacketAdminType
)
@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)
# --- Pytest Fixtures ---
# 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
@pytest_asyncio.fixture
async def connected_admin(server_config):
"""Fixture to yield a connected and authenticated OpenTTDAdminClient."""
admin_name = f"E2E_Admin_{random.randint(1000, 9999)}"
admin = OpenTTDAdminClient(
host=server_config["host"],
port=server_config["admin_port"],
admin_name=admin_name
)
await admin.connect(admin_password=server_config["password"], secure=True)
await asyncio.wait_for(admin.joined.wait(), timeout=10.0)
yield admin
if hasattr(admin, '_transport') and not admin.shutdown_event.is_set():
await admin.quit()
@pytest_asyncio.fixture
async def connected_client(server_config):
"""Fixture to yield a connected and joined spectator OpenTTDClient."""
client_name = f"E2E_Player_{random.randint(1000, 9999)}"
client = OpenTTDClient(
host=server_config["host"],
port=server_config["game_port"],
username=client_name
)
await client.connect(server_password=server_config["password"])
await client.join_company(company_id=255, company_password="")
await asyncio.wait_for(client.joined.wait(), timeout=15.0)
yield client
if hasattr(client, '_transport') and not client.shutdown_event.is_set():
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()
# ==============================================================================
# --- End-to-End Tests (Covering all public functions with multiple inputs) ---
# ==============================================================================
# --- Game Client Public Functions ---
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_client_init_and_connect_multiple_inputs(server_config):
# Public function: __init__()
# Input 1: Custom port & default username
client1 = OpenTTDClient(host=server_config["host"], port=server_config["game_port"])
assert client1.host == server_config["host"]
assert client1.port == server_config["game_port"]
assert client1.username == "GeminiUser"
# Input 2: Custom port & custom username
client2 = OpenTTDClient(host=server_config["host"], port=server_config["game_port"], username="E2E_Player_Custom")
assert client2.username == "E2E_Player_Custom"
# Public function: connect()
# Input 1: Correct server password
await client1.connect(server_password=server_config["password"])
assert client1._transport is not None
await client1.quit()
# Input 2: Incorrect server password
await client2.connect(server_password="wrong_password")
await asyncio.sleep(0.5)
assert client2.shutdown_event.is_set()
await client2.quit()
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_client_join_company_multiple_inputs(server_config):
# Public function: join_company()
# Input 1: Join as spectator (company_id=255)
client1 = OpenTTDClient(host=server_config["host"], port=server_config["game_port"], username="E2E_Spectator")
await client1.connect(server_password=server_config["password"])
await client1.join_company(company_id=255, company_password="")
await asyncio.wait_for(client1.joined.wait(), timeout=10.0)
assert client1.joined.is_set()
await client1.quit()
# Input 2: Join specific company ID (company_id=1)
client2 = OpenTTDClient(host=server_config["host"], port=server_config["game_port"], username="E2E_Player_Join_1")
await client2.connect(server_password=server_config["password"])
await client2.join_company(company_id=1, company_password="comp_password")
await asyncio.sleep(0.5)
await client2.quit()
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_client_quit_and_disconnect_multiple_inputs(server_config):
# Public function: quit() and disconnect()
client = OpenTTDClient(host=server_config["host"], port=server_config["game_port"])
await client.connect(server_password=server_config["password"])
# Public function: disconnect()
# Input 1: disconnect callback with None
client.disconnect(None)
assert client.shutdown_event.is_set()
# Input 2: disconnect callback with custom string
client.disconnect("network_lost")
# Public function: quit()
client2 = OpenTTDClient(host=server_config["host"], port=server_config["game_port"])
await client2.connect(server_password=server_config["password"])
# Input 1: quit active connection
await client2.quit()
assert client2.shutdown_event.is_set()
# Input 2: quit already inactive client
await client2.quit()
# --- Admin Client Public Functions ---
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_admin_init_and_connect_multiple_inputs(server_config):
# Public function: __init__()
# Input 1: Custom admin name
admin1 = OpenTTDAdminClient(host=server_config["host"], port=server_config["admin_port"], admin_name="E2E_Admin_1")
assert admin1.admin_name == "E2E_Admin_1"
# Input 2: Alternative admin name
admin2 = OpenTTDAdminClient(host=server_config["host"], port=server_config["admin_port"], admin_name="E2E_Admin_2")
assert admin2.admin_name == "E2E_Admin_2"
# Public function: connect()
# Input 1: secure=True (PAKE auth)
await admin1.connect(admin_password=server_config["password"], secure=True)
await asyncio.wait_for(admin1.joined.wait(), timeout=10.0)
assert admin1.joined.is_set()
await admin1.quit()
# Input 2: secure=False (plaintext auth, rejected by server)
await admin2.connect(admin_password=server_config["password"], secure=False)
await asyncio.sleep(0.5)
assert admin2.shutdown_event.is_set()
await admin2.quit()
# Input 3: incorrect password
admin3 = OpenTTDAdminClient(host=server_config["host"], port=server_config["admin_port"])
await admin3.connect(admin_password="wrong_password", secure=True)
await asyncio.sleep(0.5)
assert admin3.shutdown_event.is_set()
await admin3.quit()
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_admin_quit_and_disconnect_multiple_inputs(server_config):
# Public function: quit() and disconnect()
admin = OpenTTDAdminClient(host=server_config["host"], port=server_config["admin_port"])
await admin.connect(admin_password=server_config["password"], secure=True)
# Public function: disconnect()
# Input 1: disconnect callback with None
admin.disconnect(None)
assert admin.shutdown_event.is_set()
# Input 2: disconnect callback with custom string
admin.disconnect("admin_shutdown")
# Public function: quit()
admin2 = OpenTTDAdminClient(host=server_config["host"], port=server_config["admin_port"])
await admin2.connect(admin_password=server_config["password"], secure=True)
# Input 1: quit active connection
await admin2.quit()
assert admin2.shutdown_event.is_set()
# Input 2: quit already inactive client
await admin2.quit()
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_admin_send_rcon_multiple_inputs(connected_admin):
# Public function: send_rcon()
# Input 1: command "help"
await connected_admin.send_rcon("help")
# Input 2: command "setting max_clients"
await connected_admin.send_rcon("setting max_clients")
await asyncio.sleep(0.5)
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_admin_send_chat_multiple_inputs(connected_admin, connected_client):
# Public function: send_chat()
# Input 1: ChatBroadcast (action=1, dest_type=0, dest_id=0)
await connected_admin.send_chat("Hello from E2E Broadcast!", action=1, dest_type=0, dest_id=0)
# Input 2: Chat direct to client (action=1, dest_type=1, dest_id=client_id)
client_id = connected_client.client_id if connected_client.client_id is not None else 1
await connected_admin.send_chat("Hello private", action=1, dest_type=1, dest_id=client_id)
# Input 3: ChatBroadcast action (action=3, dest_type=0)
await connected_admin.send_chat("wave", action=3, dest_type=0, dest_id=0)
await asyncio.sleep(0.5)
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_admin_update_frequency_multiple_inputs(connected_admin):
# Public function: update_frequency()
# Input 1: Chat update to Automatic
await connected_admin.update_frequency(AdminUpdateType.Chat, AdminUpdateFrequency.Automatic)
# Input 2: Console update to Poll
await connected_admin.update_frequency(AdminUpdateType.Console, AdminUpdateFrequency.Poll)
await asyncio.sleep(0.5)
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_admin_poll_and_helpers_multiple_inputs(connected_admin):
# Public functions: poll(), poll_clients(), poll_companies(), poll_economy(), poll_stats()
# Input 1 for poll(): ClientInfo poll with 0xFFFFFFFF
await connected_admin.poll(AdminUpdateType.ClientInfo, 0xFFFFFFFF)
# Input 2 for poll(): CompanyInfo poll with 0
await connected_admin.poll(AdminUpdateType.CompanyInfo, 0)
# Input 1 for poll_clients(): 0xFFFFFFFF
await connected_admin.poll_clients(0xFFFFFFFF)
# Input 2 for poll_clients(): specific client ID 1
await connected_admin.poll_clients(1)
# Input 1 for poll_companies(): 0xFFFFFFFF
await connected_admin.poll_companies(0xFFFFFFFF)
# Input 2 for poll_companies(): specific company ID 0
await connected_admin.poll_companies(0)
# Input 1 for poll_economy(): 0xFFFFFFFF
await connected_admin.poll_economy(0xFFFFFFFF)
# Input 2 for poll_economy(): specific company ID 0
await connected_admin.poll_economy(0)
# Input 1 for poll_stats(): 0xFFFFFFFF
await connected_admin.poll_stats(0xFFFFFFFF)
# Input 2 for poll_stats(): specific company ID 0
await connected_admin.poll_stats(0)
await asyncio.sleep(0.5)
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_admin_send_gamescript_multiple_inputs(connected_admin):
# Public function: send_gamescript()
# Input 1: healthcheck dict
await connected_admin.send_gamescript({"command": "healthcheck"})
# Input 2: alternative command dict
await connected_admin.send_gamescript({"command": "ping", "sequence": 1})
await asyncio.sleep(0.5)
# --- Protocol Public Functions ---
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_protocol_public_functions_multiple_inputs(server_config):
# Public functions: __init__(), receive_packet(), send_packet()
client_alt = OpenTTDClient(host=server_config["host"], port=server_config["game_port"])
admin_alt = OpenTTDAdminClient(host=server_config["host"], port=server_config["admin_port"])
# 1. Test __init__() with multiple inputs (handlers)
proto_game = OpenTTDProtocol(client_alt)
proto_admin = OpenTTDAdminProtocol(admin_alt)
assert proto_game.handler == client_alt
assert proto_admin.handler == admin_alt
client_alt2 = OpenTTDClient(host=server_config["host"], port=server_config["game_port"])
admin_alt2 = OpenTTDAdminClient(host=server_config["host"], port=server_config["admin_port"])
proto_game2 = OpenTTDProtocol(client_alt2)
proto_admin2 = OpenTTDAdminProtocol(admin_alt2)
assert proto_game2.handler == client_alt2
assert proto_admin2.handler == admin_alt2
# 2. Test receive_packet() with multiple inputs
# Input 1: Valid packet structure (length >= 3)
res_type1, res_data1 = proto_game.receive_packet(None, memoryview(b"\x03\x00\x05")) # type 5 is ServerUnused
assert res_type1 == PacketGameType.ServerUnused
# Input 2: Invalid/short packet structure (length < 3)
res_type2, res_data2 = proto_game.receive_packet(None, memoryview(b"\x01"))
assert res_type2 == PacketGameType.ServerUnused # Falls back to ServerUnused on error
# 3. Test send_packet() with multiple inputs (using mock transports)
class FakeTransport:
def __init__(self):
self.written = []
self.closed = False
def write(self, data):
self.written.append(data)
def is_closing(self):
return self.closed
transport = FakeTransport()
proto_game.transport = transport
proto_game._can_write.set()
# Input 1: send_packet with encryption disabled
client_alt.encryption_enabled = False
await proto_game.send_packet(b"\x03\x00\x04") # ClientUnused packet
assert len(transport.written) == 1
# Input 2: send_packet with encryption enabled
client_alt.encryption_enabled = True
client_alt._session_key_send = b"\x00" * 32
client_alt._encryption_nonce = b"\x00" * 24
await proto_game.send_packet(b"\x03\x00\x05")
assert len(transport.written) == 2

View File

@@ -77,8 +77,9 @@ async def test_client_error_handling():
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
await client.receive_ServerError(None, 9) # NameInUse
await client.receive_ServerError(None, 11) # CompanyMismatch
await client.receive_ServerError(None, 17) # TimeoutComputer
@pytest.mark.asyncio
async def test_client_server_full_banned():
@@ -165,3 +166,75 @@ async def test_client_full_handshake_flow():
client._transport.close()
await client.quit()
# --- Game Client Edge Cases & Callback Unit Tests ---
@pytest.mark.asyncio
async def test_unit_client_connection_failure(server_config):
client = OpenTTDClient(host=server_config["host"], port=9999)
with pytest.raises(OSError):
await client.connect()
@pytest.mark.asyncio
async def test_unit_client_already_joined_warning(server_config):
client = OpenTTDClient(host=server_config["host"], port=server_config["game_port"])
client.joined.set()
await client.join_company(255)
assert client.joined.is_set()
@pytest.mark.asyncio
async def test_unit_client_disconnect(server_config):
client = OpenTTDClient(host=server_config["host"], port=server_config["game_port"])
client.disconnect(None)
assert client.shutdown_event.is_set()
@pytest.mark.asyncio
async def test_unit_client_receive_chat_no_callback(server_config):
client = OpenTTDClient(host=server_config["host"], port=server_config["game_port"])
client.on_chat = None
await client.receive_ServerChat(None, 1, "hello")
assert client.on_chat is None
@pytest.mark.asyncio
async def test_unit_client_receive_chat_with_callback(server_config):
client = OpenTTDClient(host=server_config["host"], port=server_config["game_port"])
chats = []
client.on_chat = lambda cid, msg: chats.append((cid, msg))
await client.receive_ServerChat(None, 2, "world")
assert chats == [(2, "world")]
@pytest.mark.asyncio
async def test_unit_client_receive_auth_request_invalid_type(server_config):
client = OpenTTDClient(host=server_config["host"], port=server_config["game_port"])
await client.receive_ServerAuthenticationRequest(None, 2, b"")
assert not client.encryption_enabled
@pytest.mark.asyncio
async def test_unit_client_noop_callbacks(server_config):
client = OpenTTDClient(host=server_config["host"], port=server_config["game_port"])
client.connected(None)
await client.receive_ServerUnused(None)
await client.receive_ServerCompanyUpdate(None)
await client.receive_ServerClientInfo(None)
await client.receive_ServerSync(None)
await client.receive_ServerClientJoined(None)
await client.receive_ServerMapBegin(None)
await client.receive_ServerMapSize(None)
await client.receive_ServerMapData(None)
await client.receive_ServerConfigurationUpdate(None)
await client.receive_ServerExternalChat(None)
await client.receive_ServerCommand(None)
await client.receive_ServerFull(None)
await client.receive_ServerBanned(None)
await client.receive_ClientAck(None)
await client.receive_ClientIdentify(None)
assert not client.shutdown_event.is_set()
assert not client.joined.is_set()
def test_unit_exclude_call_check_decorator():
from openttd import exclude_call_check
@exclude_call_check
def dummy(): pass
assert dummy.__exclude_call_check__ is True

View File

@@ -126,3 +126,169 @@ async def test_protocol_is_closing_failure():
with pytest.raises(SocketClosed):
await proto.send_packet(b"\x02\x00")
def test_admin_protocol_static_receives():
from openttd.protocol import OpenTTDAdminProtocol
import struct
# 1. receive_ServerProtocol
data = memoryview(struct.pack("<B B H H B", 3, 1, 10, 100, 0))
res = OpenTTDAdminProtocol.receive_ServerProtocol(None, data)
assert res == {"version": 3, "updates": {10: 100}}
# 2. receive_ServerWelcome
data = memoryview(b"srv_name\x00" + b"1.0\x00" + struct.pack("<B", 1) + b"map_name\x00" + struct.pack("<I B I H H", 1234, 2, 5678, 100, 200))
res = OpenTTDAdminProtocol.receive_ServerWelcome(None, data)
assert res["server_name"] == "srv_name"
assert res["openttd_version"] == "1.0"
assert res["dedicated"] is True
assert res["map_name"] == "map_name"
assert res["generation_seed"] == 1234
assert res["landscape"] == 2
assert res["start_date"] == 5678
assert res["map_width"] == 100
assert res["map_height"] == 200
# 3. receive_ServerDate
data = memoryview(struct.pack("<I", 12345))
res = OpenTTDAdminProtocol.receive_ServerDate(None, data)
assert res == {"date": 12345}
# 4. receive_ServerChat
data = memoryview(struct.pack("<B B I", 1, 2, 3) + b"msg\x00" + struct.pack("<Q", 100))
res = OpenTTDAdminProtocol.receive_ServerChat(None, data)
assert res["action"] == 1
assert res["dest_type"] == 2
assert res["client_id"] == 3
assert res["message"] == "msg"
assert res["money"] == 100
# 5. receive_ServerConsole
data = memoryview(b"origin\x00" + b"text\x00")
res = OpenTTDAdminProtocol.receive_ServerConsole(None, data)
assert res == {"origin": "origin", "text": "text"}
# 6. receive_ServerRcon
data = memoryview(struct.pack("<H", 7) + b"rcon_text\x00")
res = OpenTTDAdminProtocol.receive_ServerRcon(None, data)
assert res == {"color": 7, "text": "rcon_text"}
# 7. receive_ServerRconEnd
data = memoryview(b"cmd\x00")
res = OpenTTDAdminProtocol.receive_ServerRconEnd(None, data)
assert res == {"command": "cmd"}
# 8. receive_ServerAuthRequest
data = memoryview(struct.pack("<B", 1) + b"auth_data")
res = OpenTTDAdminProtocol.receive_ServerAuthRequest(None, data)
assert res == {"auth_type": 1, "data": b"auth_data"}
# 9. receive_ServerEnableEncryption
res = OpenTTDAdminProtocol.receive_ServerEnableEncryption(None, memoryview(b"enc_nonce"))
assert res == {"data": b"enc_nonce"}
# 10. receive_ServerError
data = memoryview(struct.pack("<B", 10))
res = OpenTTDAdminProtocol.receive_ServerError(None, data)
assert res == {"error_code": 10}
# 11. receive_ServerFull, receive_ServerBanned, receive_ServerShutdown, receive_ServerNewGame
assert OpenTTDAdminProtocol.receive_ServerFull(None, memoryview(b"")) == {}
assert OpenTTDAdminProtocol.receive_ServerBanned(None, memoryview(b"")) == {}
assert OpenTTDAdminProtocol.receive_ServerShutdown(None, memoryview(b"")) == {}
assert OpenTTDAdminProtocol.receive_ServerNewGame(None, memoryview(b"")) == {}
# 12. receive_ServerClientJoin
data = memoryview(struct.pack("<I", 12))
res = OpenTTDAdminProtocol.receive_ServerClientJoin(None, data)
assert res == {"client_id": 12}
# 13. receive_ServerClientInfo
data = memoryview(struct.pack("<I", 1) + b"127.0.0.1\x00" + b"clientname\x00" + struct.pack("<B I B", 2, 3456, 3))
res = OpenTTDAdminProtocol.receive_ServerClientInfo(None, data)
assert res["client_id"] == 1
assert res["network_address"] == "127.0.0.1"
assert res["name"] == "clientname"
assert res["language"] == 2
assert res["join_date"] == 3456
assert res["play_as"] == 3
# 14. receive_ServerClientUpdate
data = memoryview(struct.pack("<I", 1) + b"newname\x00" + struct.pack("<B", 2))
res = OpenTTDAdminProtocol.receive_ServerClientUpdate(None, data)
assert res == {"client_id": 1, "name": "newname", "play_as": 2}
# 15. receive_ServerClientQuit
data = memoryview(struct.pack("<I", 1))
res = OpenTTDAdminProtocol.receive_ServerClientQuit(None, data)
assert res == {"client_id": 1}
# 16. receive_ServerClientError
data = memoryview(struct.pack("<I B", 1, 2))
res = OpenTTDAdminProtocol.receive_ServerClientError(None, data)
assert res == {"client_id": 1, "error_code": 2}
# 17. receive_ServerCompanyNew
data = memoryview(struct.pack("<B", 1))
res = OpenTTDAdminProtocol.receive_ServerCompanyNew(None, data)
assert res == {"company_id": 1}
# 18. receive_ServerCompanyInfo
data = memoryview(struct.pack("<B", 1) + b"companyname\x00" + b"managername\x00" + struct.pack("<B B I B", 2, 1, 1990, 0))
res = OpenTTDAdminProtocol.receive_ServerCompanyInfo(None, data)
assert res["company_id"] == 1
assert res["name"] == "companyname"
assert res["manager_name"] == "managername"
assert res["color"] == 2
assert res["password_protected"] is True
assert res["inaugurated_year"] == 1990
assert res["is_ai"] is False
# 19. receive_ServerCompanyUpdate
data = memoryview(struct.pack("<B", 1) + b"companyname\x00" + b"managername\x00" + struct.pack("<B B B B B B B", 2, 1, 0, 255, 255, 255, 255))
res = OpenTTDAdminProtocol.receive_ServerCompanyUpdate(None, data)
assert res["company_id"] == 1
assert res["name"] == "companyname"
assert res["manager_name"] == "managername"
assert res["color"] == 2
assert res["password_protected"] is True
assert res["quarters_of_bankruptcy"] == 0
assert res["share_owners"] == [255, 255, 255, 255]
# 20. receive_ServerCompanyRemove
data = memoryview(struct.pack("<B B", 1, 2))
res = OpenTTDAdminProtocol.receive_ServerCompanyRemove(None, data)
assert res == {"company_id": 1, "reason": 2}
# 21. receive_ServerCompanyEconomy
data = memoryview(struct.pack("<B Q Q q H Q H H Q H H", 1, 1000, 200, -50, 10, 1200, 8, 9, 1100, 7, 8))
res = OpenTTDAdminProtocol.receive_ServerCompanyEconomy(None, data)
assert res["company_id"] == 1
assert res["money"] == 1000
assert res["loan"] == 200
assert res["income"] == -50
assert res["delivered_cargo"] == 10
assert res["value_last_quarter"] == 1200
assert res["performance_last_quarter"] == 8
assert res["delivered_cargo_last_quarter"] == 9
assert res["value_previous_quarter"] == 1100
assert res["performance_previous_quarter"] == 7
assert res["delivered_cargo_previous_quarter"] == 8
# 22. receive_ServerCompanyStats
data = memoryview(struct.pack("<B H H H H H H H H H H", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
res = OpenTTDAdminProtocol.receive_ServerCompanyStats(None, data)
assert res["company_id"] == 1
assert res["vehicles"] == {"trains": 2, "lorries": 3, "buses": 4, "planes": 5, "ships": 6}
assert res["stations"] == {"train": 7, "lorry": 8, "bus": 9, "airport": 10, "harbour": 11}
# 23. receive_ServerGamescript (valid and invalid JSON)
res = OpenTTDAdminProtocol.receive_ServerGamescript(None, memoryview(b'{"a": 1}\x00'))
assert res == {"data": {"a": 1}}
res = OpenTTDAdminProtocol.receive_ServerGamescript(None, memoryview(b'invalid_json\x00'))
assert res == {"raw_data": "invalid_json"}
# 24. receive_ServerPong
data = memoryview(struct.pack("<I", 999))
res = OpenTTDAdminProtocol.receive_ServerPong(None, data)
assert res == {"payload": 999}