Files
devplacepy/tests/unit/services/nodes/consent.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

145 lines
4.7 KiB
Python

# retoor <retoor@molodetz.nl>
import time
import pytest
from devplacepy.database import get_table, set_consent, set_setting
from devplacepy.database.nodes import save_node_settings
from devplacepy.services.nodes.consent import REASONS, ConsentGate
from devplacepy.utils import register_account
GATE = ConsentGate()
_counter = [0]
def fresh(age_band="adult"):
_counter[0] += 1
name = f"gate{int(time.time() * 1000)}{_counter[0]}"
uid, _, _ = register_account(
name, f"{name}@t.dev", "secret123", age_band=age_band, accepted_terms=True
)
return uid
@pytest.fixture
def switched_on(local_db):
set_setting("nodes_enabled", "1")
yield
set_setting("nodes_enabled", "0")
def ready():
uid = fresh()
set_consent("user", uid, "bandwidth_sharing", True)
save_node_settings(uid, {"enabled": True})
return uid
def test_a_fully_qualified_member_is_allowed(switched_on):
verdict = GATE.allows(ready())
assert verdict.allowed and verdict.reason == "" and verdict.message == ""
def test_the_kill_switch_beats_everything(local_db):
set_setting("nodes_enabled", "1")
uid = ready()
set_setting("nodes_enabled", "0")
assert GATE.allows(uid).reason == "kill_switch"
def test_default_is_off_without_the_setting(local_db):
get_table("site_settings").delete(key="nodes_enabled")
assert GATE.allows(ready()).reason == "kill_switch"
def test_unknown_account(switched_on):
assert GATE.allows("nobody").reason == "no_account"
assert GATE.allows("").reason == "no_account"
def test_each_missing_requirement_has_its_own_reason(switched_on):
no_consent = fresh()
save_node_settings(no_consent, {"enabled": True})
assert GATE.allows(no_consent).reason == "no_consent"
not_enabled = fresh()
set_consent("user", not_enabled, "bandwidth_sharing", True)
assert GATE.allows(not_enabled).reason == "not_enabled"
withdrawn = ready()
set_consent("user", withdrawn, "bandwidth_sharing", False)
assert GATE.allows(withdrawn).reason == "no_consent"
@pytest.mark.parametrize("band", ["", "under_min", "13_15", "16_17"])
def test_only_adults_pass(switched_on, band):
uid = fresh(age_band=band)
set_consent("user", uid, "bandwidth_sharing", True)
save_node_settings(uid, {"enabled": True})
assert GATE.allows(uid).reason == "not_adult"
def test_account_states_block(switched_on):
uid = ready()
users = get_table("users")
users.update({"uid": uid, "suspended_until": "2999-01-01T00:00:00+00:00"}, ["uid"])
assert GATE.allows(uid).reason == "suspended"
users.update({"uid": uid, "suspended_until": ""}, ["uid"])
users.update({"uid": uid, "is_active": False}, ["uid"])
assert GATE.allows(uid).reason == "account_inactive"
users.update({"uid": uid, "is_active": True}, ["uid"])
users.update({"uid": uid, "deletion_requested_at": "2026-01-01T00:00:00+00:00"}, ["uid"])
assert GATE.allows(uid).reason == "deletion_pending"
def test_every_reason_is_described():
for reason in (
"kill_switch",
"no_account",
"account_inactive",
"deletion_pending",
"suspended",
"not_adult",
"no_consent",
"not_enabled",
):
assert REASONS[reason]
def test_batch_decisions_equal_single_decisions(switched_on):
qualified = ready()
no_consent = fresh()
save_node_settings(no_consent, {"enabled": True})
minor = fresh(age_band="16_17")
set_consent("user", minor, "bandwidth_sharing", True)
save_node_settings(minor, {"enabled": True})
withdrawn = ready()
set_consent("user", withdrawn, "bandwidth_sharing", False)
uids = [qualified, no_consent, minor, withdrawn, "nobody"]
many = GATE.allows_many(uids)
assert list(many) == uids
for uid in uids:
assert many[uid] == GATE.allows(uid)
assert [many[uid].reason for uid in uids] == [
"", "no_consent", "not_adult", "no_consent", "no_account",
]
def test_batch_with_the_kill_switch_off_refuses_everyone(local_db):
set_setting("nodes_enabled", "0")
many = GATE.allows_many([ready(), ready()])
assert {verdict.reason for verdict in many.values()} == {"kill_switch"}
def test_batches_larger_than_the_chunk_size_are_all_decided(switched_on, monkeypatch):
monkeypatch.setattr("devplacepy.services.nodes.consent.BATCH_SIZE", 2)
uids = [ready() for _ in range(5)]
many = GATE.allows_many(uids)
assert len(many) == 5 and all(item.allowed for item in many.values())
def test_the_latest_consent_row_wins_in_a_batch(switched_on):
uid = ready()
set_consent("user", uid, "bandwidth_sharing", False)
set_consent("user", uid, "bandwidth_sharing", True)
assert GATE.allows_many([uid])[uid].allowed