Files
devplacepy/tests/api/nodes/ws.py
T
retoorandClaude Sonnet 5.5 42b81271b8 Add community node socket: devices, tickets, consent gate, kill switch
Phase 3 of plan.md. Device registration with one-time secrets, scoped single-use
socket tickets, the lock-owner WS /nodes/ws with handshake and heartbeat eviction,
a single batch ConsentGate, and NodesService which disconnects nodes within one tick
when consent, device or the nodes_enabled kill switch changes. A reconcile of 2000
sessions measured about 110 ms after replacing per-session queries with batches.
Devii may withdraw but never grant bandwidth_sharing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 09:14:53 +02:00

156 lines
5.5 KiB
Python

# retoor <retoor@molodetz.nl>
import json
import pytest
from starlette.websockets import WebSocketDisconnect
from devplacepy.services.manager import service_manager
from devplacepy.services.nodes.spec import (
CLOSE_CAPACITY,
CLOSE_KICKED,
CLOSE_POLICY,
CLOSE_PROTOCOL,
CLOSE_RETRY_OWNER,
SPEC,
)
from tests.api.nodes.devices import create
from tests.api.nodes.ticket import bearer
def hello(**overrides):
frame = {
"type": "hello",
"protocol": SPEC.version,
"client_version": "0.1.0",
"kind": "native",
"platform": "linux/amd64",
"modes": ["tunnel"],
}
frame.update(overrides)
return json.dumps(frame)
def ticket_for(client, member):
body = create(client, member).json()
ticket = client.post("/nodes/ticket", headers=bearer(body["secret"])).json()["ticket"]
return body["device"]["uid"], body["secret"], ticket
def closed_with(connection):
with pytest.raises(WebSocketDisconnect) as info:
connection.receive_text()
return info.value.code
def test_handshake_welcomes_with_the_published_numbers(client, member):
device_uid, _, ticket = ticket_for(client, member)
with client.websocket_connect(f"/nodes/ws?ticket={ticket}") as ws:
ws.send_text(hello())
welcome = json.loads(ws.receive_text())
assert welcome["type"] == "welcome"
assert welcome["device_uid"] == device_uid
assert welcome["limits"] == SPEC.limits.describe()
assert {item["suffix"] for item in welcome["destinations"]} == {
item.suffix for item in SPEC.destinations
}
assert welcome["settings"]["enabled"] is True
service = service_manager.get_service("nodes")
assert service.registry.get(device_uid) is not None
ws.send_text(json.dumps({"type": "pong"}))
ws.send_text(json.dumps({"type": "usage", "bytes_up": 5, "bytes_down": 7}))
assert service.registry.get(device_uid) is None
def test_a_ticket_works_once(client, member):
_, _, ticket = ticket_for(client, member)
with client.websocket_connect(f"/nodes/ws?ticket={ticket}") as ws:
ws.send_text(hello())
ws.receive_text()
with client.websocket_connect(f"/nodes/ws?ticket={ticket}") as again:
assert closed_with(again) == CLOSE_POLICY
def test_no_ticket_and_a_chat_ticket_are_refused(client, member):
with client.websocket_connect("/nodes/ws") as ws:
assert closed_with(ws) == CLOSE_POLICY
from devplacepy.services.messaging.tickets import issue_ticket
chat_ticket = issue_ticket(member["uid"])
with client.websocket_connect(f"/nodes/ws?ticket={chat_ticket}") as ws:
assert closed_with(ws) == CLOSE_POLICY
def test_a_non_owner_worker_says_retry(client, member):
_, _, ticket = ticket_for(client, member)
service_manager.set_lock_owner(False)
try:
with client.websocket_connect(f"/nodes/ws?ticket={ticket}") as ws:
assert closed_with(ws) == CLOSE_RETRY_OWNER
finally:
service_manager.set_lock_owner(True)
def test_protocol_violations_close_loudly(client, member):
cases = [
("not json", CLOSE_PROTOCOL),
(json.dumps({"type": "pong"}), CLOSE_PROTOCOL),
(hello(protocol=SPEC.version + 5), CLOSE_POLICY),
(hello(kind="extension"), CLOSE_POLICY),
(hello(modes=["bogus"]), CLOSE_PROTOCOL),
(hello(extra="x"), CLOSE_PROTOCOL),
]
for frame, expected in cases:
_, _, ticket = ticket_for(client, make_fresh(member))
with client.websocket_connect(f"/nodes/ws?ticket={ticket}") as ws:
ws.send_text(frame)
assert closed_with(ws) == expected, frame
def make_fresh(member):
from tests.api.nodes.conftest import make_member
return make_member()
def test_an_unsolicited_job_result_is_a_protocol_error(client, member):
_, _, ticket = ticket_for(client, member)
with client.websocket_connect(f"/nodes/ws?ticket={ticket}") as ws:
ws.send_text(hello())
ws.receive_text()
ws.send_text(
json.dumps(
{"type": "job_result", "job_id": "j", "status": 200, "body_b64": ""}
)
)
assert closed_with(ws) == CLOSE_PROTOCOL
def test_a_second_connection_of_the_same_device_replaces_the_first(client, member):
device_uid, secret, first_ticket = ticket_for(client, member)
second_ticket = client.post("/nodes/ticket", headers=bearer(secret)).json()["ticket"]
with client.websocket_connect(f"/nodes/ws?ticket={first_ticket}") as first:
first.send_text(hello())
first.receive_text()
with client.websocket_connect(f"/nodes/ws?ticket={second_ticket}") as second:
second.send_text(hello())
assert json.loads(second.receive_text())["type"] == "welcome"
kicked = json.loads(first.receive_text())
assert kicked == {"type": "kick", "reason": "replaced"}
assert closed_with(first) == CLOSE_KICKED
def test_capacity_is_a_hard_ceiling(client, member):
from devplacepy.database import get_setting, set_setting
service = service_manager.get_service("nodes")
_, _, ticket = ticket_for(client, member)
previous = get_setting("nodes_max_total", "")
set_setting("nodes_max_total", "0")
try:
with client.websocket_connect(f"/nodes/ws?ticket={ticket}") as ws:
assert closed_with(ws) == CLOSE_CAPACITY
assert service.counters.snapshot()["rejected_capacity"] >= 1
finally:
set_setting("nodes_max_total", previous)