""" Tests for meshbay_node.hub_client — uses httpx.MockTransport to avoid network. """ import time import httpx import jwt import pytest from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey from meshbay_node.hub_client import HubClient, HubConfig from meshbay_node.keystore import NodeKeys # ── Test fixtures ────────────────────────────────────────────────────────────── @pytest.fixture def hub_keys(): """Fake hub Ed25519 keypair for signing test JWTs.""" sk = Ed25519PrivateKey.generate() sk_pem = sk.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption(), ) pk_pem = sk.public_key().public_bytes( serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo, ) return sk, sk_pem, pk_pem @pytest.fixture def node_keys(): sk_ed = Ed25519PrivateKey.generate() sk_x = X25519PrivateKey.generate() return NodeKeys(sk_ed25519=sk_ed, sk_x25519=sk_x) @pytest.fixture def hub_config(tmp_path): return HubConfig( hub_url="http://fake-hub", username="testuser", cache_dir=tmp_path, ) def make_node_token(sk_pem, user_id, pk_user_b64, hub_id="fake-hub", ttl=3600): now = int(time.time()) return jwt.encode({ "iss": hub_id, "sub": user_id, "pk_user": pk_user_b64, "hub_id": hub_id, "jti": "test-jti", "scope": "node", "iat": now, "exp": now + ttl, }, sk_pem, algorithm="EdDSA") # ── Tests ───────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_login_verifies_jwt_offline(hub_keys, node_keys, hub_config): sk_hub, sk_hub_pem, pk_hub_pem = hub_keys user_id = "user-uuid-001" token = make_node_token(sk_hub_pem, user_id, node_keys.pk_ed25519_b64) def handler(request): if request.url.path == "/v1/hub/pubkey": return httpx.Response(200, json={"pk_hub_pem": pk_hub_pem.decode()}) if request.url.path == "/v1/nodes/auth": return httpx.Response(200, json={ "access_token": token, "token_type": "bearer", "expires_in": 3600}) return httpx.Response(404) transport = httpx.MockTransport(handler) client = HubClient(hub_config, node_keys) client._http = httpx.AsyncClient(transport=transport, base_url="http://fake-hub") session = await client.login() assert session.user_id == user_id assert session.access_token == token assert session.hub_pk_pem == pk_hub_pem assert session.token_expires_in > 3500 assert not session.token_needs_refresh @pytest.mark.asyncio async def test_login_rejects_missing_jti(hub_keys, node_keys, hub_config): sk_hub, sk_hub_pem, pk_hub_pem = hub_keys bad_token = jwt.encode({ "iss": "fake-hub", "sub": "uid", "pk_user": node_keys.pk_ed25519_b64, "hub_id": "fake-hub", "scope": "node", "iat": int(time.time()), "exp": int(time.time()) + 3600, }, sk_hub_pem, algorithm="EdDSA") def handler(request): if request.url.path == "/v1/hub/pubkey": return httpx.Response(200, json={"pk_hub_pem": pk_hub_pem.decode()}) if request.url.path == "/v1/nodes/auth": return httpx.Response(200, json={ "access_token": bad_token, "token_type": "bearer", "expires_in": 3600}) return httpx.Response(404) transport = httpx.MockTransport(handler) client = HubClient(hub_config, node_keys) client._http = httpx.AsyncClient(transport=transport, base_url="http://fake-hub") with pytest.raises(AssertionError, match="jti"): await client.login() @pytest.mark.asyncio async def test_token_needs_refresh(hub_keys, node_keys, hub_config): sk_hub, sk_hub_pem, pk_hub_pem = hub_keys short_token = make_node_token(sk_hub_pem, "uid", node_keys.pk_ed25519_b64, ttl=60) def handler(request): if request.url.path == "/v1/hub/pubkey": return httpx.Response(200, json={"pk_hub_pem": pk_hub_pem.decode()}) if request.url.path == "/v1/nodes/auth": return httpx.Response(200, json={ "access_token": short_token, "token_type": "bearer", "expires_in": 60}) return httpx.Response(404) transport = httpx.MockTransport(handler) client = HubClient(hub_config, node_keys) client._http = httpx.AsyncClient(transport=transport, base_url="http://fake-hub") session = await client.login() assert session.token_needs_refresh @pytest.mark.asyncio async def test_hub_pk_cached(hub_keys, node_keys, hub_config, tmp_path): _, _, pk_hub_pem = hub_keys call_count = {"n": 0} def handler(request): if request.url.path == "/v1/hub/pubkey": call_count["n"] += 1 return httpx.Response(200, json={"pk_hub_pem": pk_hub_pem.decode()}) return httpx.Response(404) transport = httpx.MockTransport(handler) client = HubClient(hub_config, node_keys) client._http = httpx.AsyncClient(transport=transport, base_url="http://fake-hub") await client._fetch_hub_pk() await client._fetch_hub_pk() # second call should use cache assert call_count["n"] == 1, "Hub PK should be fetched only once"