Files
Commonwealth-Online-Server/server/tests/test_runtime_portability.py
T

201 lines
6.2 KiB
Python

from __future__ import annotations
import json
import socket
import threading
import time
from pathlib import Path
import pytest
from admin_server import DEFAULT_ADMIN_TOKEN_PATH, send_admin_command
from lan_discovery import LanDiscoveryResponder
from server_core import FalloutTogetherServer, get_lan_addresses
from server_service import ServerConfig, ServerService
def _free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(("127.0.0.1", 0))
return int(sock.getsockname()[1])
def test_get_lan_addresses_never_returns_wildcard() -> None:
addresses = get_lan_addresses()
assert "0.0.0.0" not in addresses
for address in addresses:
assert not address.startswith("127.")
def test_discovery_failure_does_not_stop_game_server(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
game_port = _free_port()
admin_port = _free_port()
def _fail_start(self: LanDiscoveryResponder) -> None:
raise OSError("Could not bind LAN discovery to 0.0.0.0:7778. simulated failure")
monkeypatch.setattr(LanDiscoveryResponder, "start", _fail_start)
service = ServerService(
ServerConfig(
host="127.0.0.1",
port=game_port,
admin_port=admin_port,
bans_path=str(tmp_path / "bans.json"),
)
)
thread = threading.Thread(target=service.serve_forever, daemon=True)
thread.start()
deadline = time.time() + 5.0
while time.time() < deadline and not service.is_running():
time.sleep(0.05)
assert service.is_running()
with socket.create_connection(("127.0.0.1", game_port), timeout=2.0) as conn:
data = conn.recv(4096)
assert b"welcome" in data
service.stop()
thread.join(timeout=3.0)
assert not service.is_running()
def test_stop_is_idempotent(tmp_path: Path) -> None:
game_port = _free_port()
admin_port = _free_port()
service = ServerService(
ServerConfig(
host="127.0.0.1",
port=game_port,
admin_port=admin_port,
bans_path=str(tmp_path / "bans.json"),
)
)
service.start()
deadline = time.time() + 5.0
while time.time() < deadline and not service.is_running():
time.sleep(0.05)
assert service.is_running()
service.stop()
service.stop()
assert not service.is_running()
def test_admin_and_game_bind_errors_include_address() -> None:
occupied = _free_port()
holder = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
holder.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
holder.bind(("127.0.0.1", occupied))
holder.listen()
server = FalloutTogetherServer(host="127.0.0.1", port=occupied)
with pytest.raises(OSError, match=rf"127\.0\.0\.1:{occupied}"):
server._prepare_server_socket()
finally:
holder.close()
def test_headless_admin_roundtrip(tmp_path: Path) -> None:
game_port = _free_port()
admin_port = _free_port()
service = ServerService(
ServerConfig(
host="127.0.0.1",
port=game_port,
admin_port=admin_port,
bans_path=str(tmp_path / "bans.json"),
)
)
thread = threading.Thread(target=service.serve_forever, daemon=True)
thread.start()
deadline = time.time() + 5.0
while time.time() < deadline and not service.is_running():
time.sleep(0.05)
assert service.is_running()
response = send_admin_command({"cmd": "ping"}, port=admin_port)
assert response.get("ok") is True
assert response.get("data", {}).get("pong") is True
service.stop()
thread.join(timeout=3.0)
def test_admin_rejects_unauthenticated_requests_and_does_not_disclose_token(tmp_path: Path) -> None:
game_port = _free_port()
admin_port = _free_port()
service = ServerService(
ServerConfig(
host="127.0.0.1",
port=game_port,
admin_port=admin_port,
bans_path=str(tmp_path / "bans.json"),
)
)
thread = threading.Thread(target=service.serve_forever, daemon=True)
thread.start()
deadline = time.time() + 5.0
while time.time() < deadline and not service.is_running():
time.sleep(0.05)
assert service.is_running()
with socket.create_connection(("127.0.0.1", admin_port), timeout=2.0) as conn:
conn.sendall(b'{"cmd":"ping"}\n')
raw = conn.recv(4096)
unauthorized = json.loads(raw.split(b"\n", 1)[0].decode("utf-8"))
assert unauthorized.get("ok") is False
assert unauthorized.get("error") == "Unauthorized admin request."
authenticated = send_admin_command({"cmd": "status"}, port=admin_port)
assert authenticated.get("ok") is True
token = DEFAULT_ADMIN_TOKEN_PATH.read_text(encoding="utf-8").strip()
assert token
assert token not in json.dumps(authenticated, sort_keys=True)
service.stop()
thread.join(timeout=3.0)
def test_lan_discovery_sets_broadcast_option() -> None:
class DummyServer:
def get_stats(self):
return {
"connectedClients": 0,
"port": 7777,
"serverName": "Test",
"serverDescription": "",
"maxPlayers": 16,
}
def _log(self, message: str, *, level: str = "info") -> None:
return None
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("127.0.0.1", 0))
port = int(sock.getsockname()[1])
sock.close()
responder = LanDiscoveryResponder(DummyServer(), discovery_port=port)
responder.start()
try:
assert responder._socket is not None
value = responder._socket.getsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST)
assert value in (0, 1)
probe = {
"type": "discover",
"protocol": "commonwealth-online",
}
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
client.settimeout(2.0)
client.sendto(json.dumps(probe).encode("utf-8"), ("127.0.0.1", port))
data, _addr = client.recvfrom(2048)
packet = json.loads(data.decode("utf-8"))
assert packet["type"] == "discoverResponse"
assert packet["port"] == 7777
finally:
responder.stop()