from __future__ import annotations
import asyncio
import json
import os
import pytest
pytest.importorskip("net")
pytest.importorskip("net_sdk")
import net
_ROOT_SEED_HEX = "11" * 32
def _run(coro) -> dict:
return json.loads(asyncio.run(coro))
def _set_env(**vals):
saved = {k: os.environ.get(k) for k in vals}
for k, v in vals.items():
if v is None:
os.environ.pop(k, None)
else:
os.environ[k] = v
return saved
def _restore_env(saved):
for k, v in saved.items():
if v is None:
os.environ.pop(k, None)
else:
os.environ[k] = v
@pytest.fixture()
def rooted_node(plugin, tmp_path):
node = plugin.node
saved = _set_env(
NET_MESH_IDENTITY_SEED=_ROOT_SEED_HEX,
NET_MESH_DEVICE_STORE=str(tmp_path / "devices.json"),
NET_MESH_REVOCATION_STORE=str(tmp_path / "revocations.json"),
NET_MESH_PIN_STORE=str(tmp_path / "pins.json"),
NET_MESH_PSK=None,
NET_MESH_PEERS=None,
)
node.shutdown() try:
yield node
finally:
node.shutdown()
_restore_env(saved)
node.shutdown()
def test_invite_mints_a_shareable_string(rooted_node, plugin):
res = _run(plugin.tools.handle_net_mesh_invite({"ttl_seconds": 300}))
assert res["status"] == "ok"
assert res["invite"].startswith("net-invite:")
root = net.Identity.from_seed(bytes.fromhex(_ROOT_SEED_HEX))
parsed = net.InviteToken.decode(res["invite"])
assert parsed.root == root.entity_id
assert res["root_fingerprint"] == net.fingerprint(root.entity_id)
def test_devices_and_revoke_over_the_facade(rooted_node, plugin):
node = plugin.node
operator = node.operator()
invite = operator.invite(node.mesh().rendezvous_string(), 300)
device = net.Identity.generate()
req = net.JoinRequest.create(device, "pc", ["region:office"], invite)
operator.approve(req, 3600)
res = _run(plugin.tools.handle_net_mesh_devices({}))
assert res["status"] == "ok"
assert len(res["devices"]) == 1
rec = res["devices"][0]
assert rec["name"] == "pc"
assert rec["device_id"] == device.entity_id.hex()
assert rec["tags"] == ["region:office"]
assert rec["revoked"] is False
assert rec["expires_in_days"] >= 360
assert rec["renewal_recommended"] is False
assert "warning" not in res
res = _run(plugin.tools.handle_net_mesh_revoke({"device_id": device.entity_id.hex()}))
assert res["status"] == "ok"
res = _run(plugin.tools.handle_net_mesh_devices({}))
assert res["devices"][0]["revoked"] is True
def test_devices_expiry_warning_surfaces(rooted_node, plugin, tmp_path):
import time
node = plugin.node
operator = node.operator()
invite = operator.invite(node.mesh().rendezvous_string(), 300)
device = net.Identity.generate()
operator.approve(net.JoinRequest.create(device, "pc", [], invite), 3600)
store = tmp_path / "devices.json"
data = json.loads(store.read_text())
data["devices"][0]["enrolled_at"] = int(time.time()) - (365 - 20) * 86400
store.write_text(json.dumps(data))
res = _run(plugin.tools.handle_net_mesh_devices({}))
assert res["status"] == "ok"
assert res["devices"][0]["renewal_recommended"] is True
assert res["devices"][0]["expires_in_days"] <= 21
assert "warning" in res
def test_revoke_rejects_a_bad_device_id(rooted_node, plugin):
res = _run(plugin.tools.handle_net_mesh_revoke({"device_id": "not-hex"}))
assert res["status"] == "error"
res = _run(plugin.tools.handle_net_mesh_revoke({"device_id": ""}))
assert res["status"] == "error"
res = _run(plugin.tools.handle_net_mesh_revoke({"device_id": "ab12"}))
assert res["status"] == "error"
@pytest.mark.parametrize("set_var", ["NET_MESH_DEVICE_STORE", "NET_MESH_REVOCATION_STORE"])
def test_half_store_override_fails_loudly(plugin, tmp_path, monkeypatch, set_var):
other = (
"NET_MESH_REVOCATION_STORE"
if set_var == "NET_MESH_DEVICE_STORE"
else "NET_MESH_DEVICE_STORE"
)
monkeypatch.setenv("NET_MESH_IDENTITY_SEED", _ROOT_SEED_HEX)
monkeypatch.setenv(set_var, str(tmp_path / "store.json"))
monkeypatch.delenv(other, raising=False)
with pytest.raises(RuntimeError, match=other):
plugin.node._build_operator(object())
def test_mesh_tools_registered_in_toolset(plugin):
names = {name for name, _schema, _handler, _emoji in plugin.tools.TOOLS}
assert {"net_mesh_invite", "net_mesh_devices", "net_mesh_revoke"} <= names
def test_mesh_tools_error_without_a_root_identity(plugin, tmp_path):
node = plugin.node
saved = _set_env(
NET_MESH_IDENTITY_SEED=None, NET_MESH_DEVICE_STORE=str(tmp_path / "devices.json"),
NET_MESH_REVOCATION_STORE=str(tmp_path / "revocations.json"),
NET_MESH_PIN_STORE=str(tmp_path / "pins.json"),
NET_MESH_PSK=None,
NET_MESH_PEERS=None,
)
node.shutdown()
try:
res = _run(plugin.tools.handle_net_mesh_invite({}))
assert res["status"] == "error"
assert "root" in res["error"].lower()
res = _run(plugin.tools.handle_net_mesh_devices({}))
assert res["status"] == "error"
finally:
node.shutdown()
_restore_env(saved)
node.shutdown()