# retoor 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 assert welcome["device_label"] == "Home laptop" 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) def test_the_donor_address_is_classed_through_the_geo_database(client, member, tmp_path, monkeypatch): from devplacepy import config from devplacepy.database.nodes import get_device from devplacepy.services.nodes import geo monkeypatch.setattr(geo, "SPEC", geo.GeoSpec(min_asn_rows=1, min_country_rows=1, checks=())) target = tmp_path / "geo.db" built = geo.build( ["8.8.8.0,8.8.8.255,15169,Google LLC", "20.0.0.0,20.0.0.255,64500,Home Fiber BV"], ["8.8.8.0,8.8.8.255,US", "20.0.0.0,20.0.0.255,NL"], target, "2026-10", ) geo.install(built, target) monkeypatch.setattr(config, "NODE_GEO_DB", target) for address, expected in (("8.8.8.8", ("US", "datacenter")), ("20.0.0.9", ("NL", "residential")), ("9.9.9.9", ("", "unknown"))): device_uid, _, ticket = ticket_for(client, make_fresh(member)) with client.websocket_connect(f"/nodes/ws?ticket={ticket}", headers={"x-real-ip": address}) as ws: ws.send_text(hello()) ws.receive_text() session = service_manager.get_service("nodes").registry.get(device_uid) assert session.asn_class == expected[1] row = get_device(device_uid) assert (row["country"], row["asn_class"]) == expected assert "x-real-ip" not in str(row) and address not in str(row.values()) def test_a_missing_geo_database_is_counted_and_classes_unknown(client, member, tmp_path, monkeypatch): from devplacepy import config monkeypatch.setattr(config, "NODE_GEO_DB", tmp_path / "absent.db") device_uid, _, ticket = ticket_for(client, member) service = service_manager.get_service("nodes") before = service.counters.snapshot()["geo_unavailable"] with client.websocket_connect(f"/nodes/ws?ticket={ticket}", headers={"x-real-ip": "8.8.8.8"}) as ws: ws.send_text(hello()) ws.receive_text() assert service.registry.get(device_uid).asn_class == "unknown" assert service.counters.snapshot()["geo_unavailable"] == before + 1