# retoor import asyncio import time import pytest from devplacepy.database import get_table, set_consent, set_setting from devplacepy.database.nodes import ( create_device, revoke_device, save_node_settings, suspend_device, ) from devplacepy.services.nodes.service import NodesService from devplacepy.utils import register_account from tests.conftest import run_async from tests.unit.services.nodes.session import FakeSocket from devplacepy.services.nodes.session import NodeSession _counter = [0] def member(): _counter[0] += 1 name = f"svc{int(time.time() * 1000)}{_counter[0]}" uid, _, _ = register_account( name, f"{name}@t.dev", "secret123", age_band="adult", accepted_terms=True ) set_consent("user", uid, "bandwidth_sharing", True) save_node_settings(uid, {"enabled": True}) return uid def connected(service, uid): device, _ = create_device(uid, "x", "native") socket = FakeSocket() session = NodeSession(socket, device) service.registry.admit(session) return device, socket @pytest.fixture def service(local_db): set_setting("nodes_enabled", "1") yield NodesService() set_setting("nodes_enabled", "0") def reconcile(service): return run_async(service.reconcile()) def test_healthy_nodes_stay_connected(service): _, socket = connected(service, member()) assert reconcile(service) == 0 assert socket.closed is None def test_the_kill_switch_disconnects_everyone_in_one_tick(service): sockets = [connected(service, member())[1] for _ in range(4)] set_setting("nodes_enabled", "0") assert reconcile(service) == 4 assert service.registry.count() == 0 assert all(socket.sent[-1] == {"type": "kick", "reason": "kill_switch"} for socket in sockets) def test_withdrawn_consent_disconnects_that_owner_only(service): leaving, staying = member(), member() _, gone = connected(service, leaving) _, kept = connected(service, staying) set_consent("user", leaving, "bandwidth_sharing", False) assert reconcile(service) == 1 assert gone.sent[-1]["reason"] == "no_consent" assert kept.closed is None and service.registry.count() == 1 def test_a_revoked_device_is_kicked(service): uid = member() device, socket = connected(service, uid) revoke_device(device["uid"], uid) assert reconcile(service) == 1 assert socket.sent[-1]["reason"] == "revoked" def test_a_suspended_device_is_kicked(service): uid = member() device, socket = connected(service, uid) suspend_device(device["uid"], "test", 30) assert reconcile(service) == 1 assert socket.sent[-1]["reason"] == "suspended" def test_donation_turned_off_by_the_member_disconnects(service): uid = member() _, socket = connected(service, uid) save_node_settings(uid, {"enabled": False}) assert reconcile(service) == 1 assert socket.sent[-1]["reason"] == "not_enabled" def test_a_deleted_account_is_disconnected(service): uid = member() _, socket = connected(service, uid) get_table("users").update( {"uid": uid, "deletion_requested_at": "2026-01-01T00:00:00+00:00"}, ["uid"] ) assert reconcile(service) == 1 assert socket.sent[-1]["reason"] == "deletion_pending" def test_the_gate_is_asked_once_for_all_owners_per_tick(service, monkeypatch): uid, other = member(), member() for owner in (uid, uid, uid, other): connected(service, owner) calls = [] original = service.gate.allows_many def spy(uids): uids = list(uids) calls.append(sorted(set(uids))) return original(uids) monkeypatch.setattr(service.gate, "allows_many", spy) reconcile(service) assert calls == [sorted({uid, other})] def test_an_empty_registry_costs_nothing(service, monkeypatch): monkeypatch.setattr(service.gate, "allows_many", lambda uids: 1 / 0) assert reconcile(service) == 0 def test_stopping_the_service_kicks_all(service): _, socket = connected(service, member()) run_async(service.on_disable()) assert socket.sent[-1]["reason"] == "service_stopped" assert service.registry.count() == 0 def test_metrics_report_connected_and_capacity(service): connected(service, member()) labels = {item["label"]: item["value"] for item in service.collect_metrics()["stats"]} assert labels["connected"] == 1 and labels["capacity"] > 0 def test_the_service_has_the_kill_switch_as_a_config_field(service): keys = [field.key for field in service.all_fields()] assert "nodes_enabled" in keys and "nodes_max_total" in keys def test_changed_settings_are_pushed_to_the_node_once(service): uid = member() device, socket = connected(service, uid) session = service.registry.get(device["uid"]) session.settings = {"stale": True} assert reconcile(service) == 0 pushed = [frame for frame in socket.sent if frame["type"] == "settings_update"] assert len(pushed) == 1 and pushed[0]["settings"]["enabled"] is True assert session.settings == pushed[0]["settings"] reconcile(service) assert len([frame for frame in socket.sent if frame["type"] == "settings_update"]) == 1 save_node_settings(uid, {"bandwidth_mb_day": 9}) reconcile(service) assert session.settings["bandwidth_mb_day"] == 9 def test_the_dispatch_loop_starts_once_and_stops_with_the_service(service): async def scenario(): service.ensure_dispatching() first = service._dispatch_task service.ensure_dispatching() assert service._dispatch_task is first and not first.done() await service.on_disable() await asyncio.sleep(0) assert service._dispatch_task is None and first.cancelled() run_async(scenario())