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>
106 lines
4.1 KiB
Python
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}
|