forked from Rockachopa/Timmy-time-dashboard
This commit is contained in:
@@ -246,6 +246,131 @@ def test_world_ws_endpoint_accepts_connection(client):
|
||||
pass # Connection accepted — just close it
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WebSocket Authentication Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestWebSocketAuth:
|
||||
"""Tests for WebSocket token-based authentication."""
|
||||
|
||||
def test_ws_auth_disabled_when_token_unset(self, client):
|
||||
"""When matrix_ws_token is empty, auth is disabled (dev mode)."""
|
||||
with patch("dashboard.routes.world.settings") as mock_settings:
|
||||
mock_settings.matrix_ws_token = ""
|
||||
with client.websocket_connect("/api/world/ws") as ws:
|
||||
# Should receive world_state without auth
|
||||
msg = json.loads(ws.receive_text())
|
||||
assert msg["type"] == "world_state"
|
||||
|
||||
def test_ws_valid_token_via_query_param(self, client):
|
||||
"""Valid token via ?token= query param allows connection."""
|
||||
with patch("dashboard.routes.world.settings") as mock_settings:
|
||||
mock_settings.matrix_ws_token = "secret123"
|
||||
with client.websocket_connect("/api/world/ws?token=secret123") as ws:
|
||||
# Should receive connection_ack first
|
||||
ack = json.loads(ws.receive_text())
|
||||
assert ack["type"] == "connection_ack"
|
||||
# Then world_state
|
||||
msg = json.loads(ws.receive_text())
|
||||
assert msg["type"] == "world_state"
|
||||
|
||||
def test_ws_valid_token_via_first_message(self, client):
|
||||
"""Valid token via first auth message allows connection."""
|
||||
with patch("dashboard.routes.world.settings") as mock_settings:
|
||||
mock_settings.matrix_ws_token = "secret123"
|
||||
with client.websocket_connect("/api/world/ws") as ws:
|
||||
# Send auth message
|
||||
ws.send_text(json.dumps({"type": "auth", "token": "secret123"}))
|
||||
# Should receive connection_ack
|
||||
ack = json.loads(ws.receive_text())
|
||||
assert ack["type"] == "connection_ack"
|
||||
# Then world_state
|
||||
msg = json.loads(ws.receive_text())
|
||||
assert msg["type"] == "world_state"
|
||||
|
||||
def test_ws_invalid_token_via_query_param(self, client):
|
||||
"""Invalid token via ?token= closes connection with code 4001."""
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
with patch("dashboard.routes.world.settings") as mock_settings:
|
||||
mock_settings.matrix_ws_token = "secret123"
|
||||
# When auth fails with query param, accept() is called then close()
|
||||
# The test client raises WebSocketDisconnect on close
|
||||
with pytest.raises(WebSocketDisconnect) as exc_info:
|
||||
with client.websocket_connect("/api/world/ws?token=wrongtoken") as ws:
|
||||
# Try to receive - should trigger the close
|
||||
ws.receive_text()
|
||||
assert exc_info.value.code == 4001
|
||||
|
||||
def test_ws_invalid_token_via_first_message(self, client):
|
||||
"""Invalid token via first message closes connection with code 4001."""
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
with patch("dashboard.routes.world.settings") as mock_settings:
|
||||
mock_settings.matrix_ws_token = "secret123"
|
||||
with client.websocket_connect("/api/world/ws") as ws:
|
||||
# Send invalid auth message
|
||||
ws.send_text(json.dumps({"type": "auth", "token": "wrongtoken"}))
|
||||
# Connection should close with 4001
|
||||
with pytest.raises(WebSocketDisconnect) as exc_info:
|
||||
ws.receive_text()
|
||||
assert exc_info.value.code == 4001
|
||||
|
||||
def test_ws_no_token_when_auth_required(self, client):
|
||||
"""No token when auth is required closes connection with code 4001."""
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
with patch("dashboard.routes.world.settings") as mock_settings:
|
||||
mock_settings.matrix_ws_token = "secret123"
|
||||
with client.websocket_connect("/api/world/ws") as ws:
|
||||
# Send non-auth message without token
|
||||
ws.send_text(json.dumps({"type": "visitor_message", "text": "hello"}))
|
||||
# Connection should close with 4001
|
||||
with pytest.raises(WebSocketDisconnect) as exc_info:
|
||||
ws.receive_text()
|
||||
assert exc_info.value.code == 4001
|
||||
|
||||
def test_ws_non_json_first_message_when_auth_required(self, client):
|
||||
"""Non-JSON first message when auth required closes with 4001."""
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
with patch("dashboard.routes.world.settings") as mock_settings:
|
||||
mock_settings.matrix_ws_token = "secret123"
|
||||
with client.websocket_connect("/api/world/ws") as ws:
|
||||
# Send non-JSON message
|
||||
ws.send_text("not json")
|
||||
# Connection should close with 4001
|
||||
with pytest.raises(WebSocketDisconnect) as exc_info:
|
||||
ws.receive_text()
|
||||
assert exc_info.value.code == 4001
|
||||
|
||||
def test_ws_existing_behavior_unchanged_when_token_not_configured(self, client, tmp_path):
|
||||
"""Existing /api/world/ws behavior unchanged when token not configured."""
|
||||
f = tmp_path / "presence.json"
|
||||
f.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 1,
|
||||
"liveness": "2026-03-19T02:00:00Z",
|
||||
"mood": "exploring",
|
||||
"current_focus": "testing",
|
||||
"active_threads": [],
|
||||
"recent_events": [],
|
||||
"concerns": [],
|
||||
}
|
||||
)
|
||||
)
|
||||
with patch("dashboard.routes.world.settings") as mock_settings:
|
||||
mock_settings.matrix_ws_token = "" # Not configured
|
||||
with patch("dashboard.routes.world.PRESENCE_FILE", f):
|
||||
with client.websocket_connect("/api/world/ws") as ws:
|
||||
# Should receive world_state directly (no connection_ack)
|
||||
msg = json.loads(ws.receive_text())
|
||||
assert msg["type"] == "world_state"
|
||||
assert msg["timmyState"]["mood"] == "exploring"
|
||||
|
||||
|
||||
def test_world_ws_sends_snapshot_on_connect(client, tmp_path):
|
||||
"""WebSocket sends a world_state snapshot immediately on connect."""
|
||||
f = tmp_path / "presence.json"
|
||||
|
||||
Reference in New Issue
Block a user