Files
devplacepy/tests/api/nodes/devices.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

106 lines
4.1 KiB
Python

# retoor <retoor@molodetz.nl>
from devplacepy.database import get_setting, set_setting
from devplacepy.database.nodes import LIMITS, find_device_by_secret
from tests.api.nodes.conftest import make_member
def create(client, who, **body):
payload = {"label": "Home laptop", "kind": "native", **body}
return client.post("/nodes/devices", json=payload, headers=who["headers"])
def test_guests_get_401(client):
assert client.get("/nodes/devices").status_code == 401
assert client.post("/nodes/devices", json={"label": "x"}).status_code == 401
assert client.post("/nodes/devices/x/rotate").status_code == 401
assert client.post("/nodes/devices/x/delete").status_code == 401
def test_registration_returns_the_secret_once_and_never_caches_it(client, member):
response = create(client, member)
assert response.status_code == 201
assert response.headers["cache-control"] == "no-store"
body = response.json()
assert body["secret"].startswith("dpn_")
assert body["device"]["label"] == "Home laptop"
assert "secret_hash" not in body["device"] and "ip_hash" not in body["device"]
assert find_device_by_secret(body["secret"])["uid"] == body["device"]["uid"]
listing = client.get("/nodes/devices", headers=member["headers"]).json()
assert [item["uid"] for item in listing["devices"]] == [body["device"]["uid"]]
assert listing["max_devices"] == LIMITS.max_devices_per_user
assert "secret" not in str(listing)
def test_registration_needs_consent_and_enablement(client):
no_consent = make_member(consent=False)
refused = create(client, no_consent)
assert refused.status_code == 403
assert refused.json()["error"]["reason"] == "no_consent"
not_enabled = make_member(enabled=False)
assert create(client, not_enabled).json()["error"]["reason"] == "not_enabled"
minor = make_member(age_band="16_17")
assert create(client, minor).json()["error"]["reason"] == "not_adult"
def test_kill_switch_blocks_registration(client, member):
previous = get_setting("nodes_enabled", "0")
set_setting("nodes_enabled", "0")
try:
response = create(client, member)
assert response.status_code == 403
assert response.json()["error"]["reason"] == "kill_switch"
finally:
set_setting("nodes_enabled", previous)
def test_device_cap_is_a_409(client, member):
for _ in range(LIMITS.max_devices_per_user):
assert create(client, member).status_code == 201
assert create(client, member).status_code == 409
def test_unknown_kind_is_a_validation_error(client, member):
assert create(client, member, kind="browser").status_code == 422
def test_rotate_replaces_the_secret_and_is_owner_only(client, member):
created = create(client, member).json()
uid, old = created["device"]["uid"], created["secret"]
other = make_member()
assert (
client.post(f"/nodes/devices/{uid}/rotate", headers=other["headers"]).status_code
== 404
)
rotated = client.post(f"/nodes/devices/{uid}/rotate", headers=member["headers"])
assert rotated.status_code == 200
new = rotated.json()["secret"]
assert new != old
assert find_device_by_secret(old) is None
assert find_device_by_secret(new)["uid"] == uid
def test_delete_is_owner_or_admin(client, member):
uid = create(client, member).json()["device"]["uid"]
other = make_member()
assert (
client.post(f"/nodes/devices/{uid}/delete", headers=other["headers"]).status_code
== 403
)
admin = make_member(role="Admin")
assert (
client.post(f"/nodes/devices/{uid}/delete", headers=admin["headers"]).status_code
== 200
)
assert client.get("/nodes/devices", headers=member["headers"]).json()["devices"] == []
assert (
client.post(f"/nodes/devices/{uid}/delete", headers=member["headers"]).status_code
== 404
)
def test_owner_can_delete_their_own_device(client, member):
uid = create(client, member).json()["device"]["uid"]
response = client.post(f"/nodes/devices/{uid}/delete", headers=member["headers"])
assert response.status_code == 200 and response.json() == {"ok": True}