forked from retoor/devplacepy
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>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user