Initial commit

This commit is contained in:
Jage9
2026-02-20 08:16:43 -05:00
commit b246c9a7fd
53 changed files with 9538 additions and 0 deletions

0
server/tests/__init__.py Normal file
View File

View File

@@ -0,0 +1,24 @@
from pathlib import Path
import pytest
from app.config import load_config
def test_load_config_defaults_when_path_none() -> None:
cfg = load_config(None)
assert cfg.server.bind_ip == "127.0.0.1"
assert cfg.network.allow_insecure_ws is True
assert cfg.storage.state_file == "runtime/items.json"
def test_load_config_requires_tls_when_insecure_disabled(tmp_path: Path) -> None:
config_path = tmp_path / "config.toml"
config_path.write_text(
"""
[network]
allow_insecure_ws = false
""".strip()
)
with pytest.raises(ValueError):
load_config(config_path)

View File

@@ -0,0 +1,35 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import cast
from websockets.asyncio.server import ServerConnection
from app.client import ClientConnection
from app.item_service import ItemService
def _fake_ws() -> ServerConnection:
return cast(ServerConnection, object())
def test_item_persistence_omits_global_type_properties(tmp_path: Path) -> None:
state_file = tmp_path / "items.json"
service = ItemService(state_file=state_file)
client = ClientConnection(websocket=_fake_ws(), id="u1", x=3, y=4)
item = service.default_item(client, "dice")
service.add_item(item)
service.save_state()
saved = json.loads(state_file.read_text(encoding="utf-8"))
assert isinstance(saved, list)
assert len(saved) == 1
assert "capabilities" not in saved[0]
assert "useSound" not in saved[0]
reloaded = ItemService(state_file=state_file)
loaded_item = reloaded.items[item.id]
assert loaded_item.useSound == "sounds/roll.ogg"
assert "usable" in loaded_item.capabilities

View File

@@ -0,0 +1,18 @@
from pydantic import ValidationError, TypeAdapter
from app.models import ClientPacket
def test_update_position_validates() -> None:
adapter = TypeAdapter(ClientPacket)
packet = adapter.validate_python({"type": "update_position", "x": 10, "y": 12})
assert packet.type == "update_position"
def test_unknown_type_rejected() -> None:
adapter = TypeAdapter(ClientPacket)
try:
adapter.validate_python({"type": "unknown"})
except ValidationError:
return
assert False, "validation should fail"

View File

@@ -0,0 +1,28 @@
from __future__ import annotations
from typing import cast
from websockets.asyncio.server import ServerConnection
from app.server import ClientConnection, SignalingServer
def _fake_ws() -> ServerConnection:
return cast(ServerConnection, object())
def test_nickname_taken_is_case_insensitive() -> None:
server = SignalingServer("127.0.0.1", 8765, None, None)
first_ws = _fake_ws()
second_ws = _fake_ws()
server.clients[first_ws] = ClientConnection(websocket=first_ws, id="1", nickname="Jage")
server.clients[second_ws] = ClientConnection(websocket=second_ws, id="2", nickname="Alice")
assert server._is_nickname_taken("jage", exclude_client_id="2")
assert server._is_nickname_taken("JAGE", exclude_client_id="2")
assert not server._is_nickname_taken("jage", exclude_client_id="1")
def test_nickname_key_uses_casefold() -> None:
server = SignalingServer("127.0.0.1", 8765, None, None)
assert server._nickname_key("Jage") == server._nickname_key("jage")

View File

@@ -0,0 +1,73 @@
from __future__ import annotations
import json
from typing import cast
import pytest
from websockets.asyncio.server import ServerConnection
from app.models import BroadcastChatMessagePacket, BroadcastNicknamePacket, NicknameResultPacket
from app.server import ClientConnection, SignalingServer
def _fake_ws() -> ServerConnection:
return cast(ServerConnection, object())
@pytest.mark.asyncio
async def test_same_nickname_same_case_is_noop(monkeypatch: pytest.MonkeyPatch) -> None:
server = SignalingServer("127.0.0.1", 8765, None, None)
ws = _fake_ws()
client = ClientConnection(websocket=ws, id="1", nickname="Jage")
server.clients[ws] = client
sent_packets: list[object] = []
broadcast_packets: list[object] = []
async def fake_send(websocket: ServerConnection, packet: object) -> None:
sent_packets.append(packet)
async def fake_broadcast(packet: object, exclude: ServerConnection | None = None) -> None:
broadcast_packets.append(packet)
monkeypatch.setattr(server, "_send", fake_send)
monkeypatch.setattr(server, "_broadcast", fake_broadcast)
await server._handle_message(client, json.dumps({"type": "update_nickname", "nickname": "Jage"}))
assert client.nickname == "Jage"
assert broadcast_packets == []
assert any(
isinstance(packet, NicknameResultPacket) and packet.accepted and packet.effectiveNickname == "Jage"
for packet in sent_packets
)
@pytest.mark.asyncio
async def test_case_only_change_is_allowed_and_broadcast(monkeypatch: pytest.MonkeyPatch) -> None:
server = SignalingServer("127.0.0.1", 8765, None, None)
ws = _fake_ws()
client = ClientConnection(websocket=ws, id="1", nickname="jage")
server.clients[ws] = client
sent_packets: list[object] = []
broadcast_packets: list[object] = []
async def fake_send(websocket: ServerConnection, packet: object) -> None:
sent_packets.append(packet)
async def fake_broadcast(packet: object, exclude: ServerConnection | None = None) -> None:
broadcast_packets.append(packet)
monkeypatch.setattr(server, "_send", fake_send)
monkeypatch.setattr(server, "_broadcast", fake_broadcast)
await server._handle_message(client, json.dumps({"type": "update_nickname", "nickname": "Jage"}))
assert client.nickname == "Jage"
assert any(
isinstance(packet, NicknameResultPacket) and packet.accepted and packet.effectiveNickname == "Jage"
for packet in sent_packets
)
assert any(isinstance(packet, BroadcastNicknamePacket) for packet in broadcast_packets)
assert any(isinstance(packet, BroadcastChatMessagePacket) for packet in broadcast_packets)