"""Dashboard user accounts: registration, login, roles, API keys, usage. Unit tests for AccountStore plus HTTP integration on the tracker: register/login/logout, per-account balance and usage, API-key lifecycle (revoked keys rejected by the OpenAI proxy), and the admin listing. """ import http.cookies import json import urllib.error import urllib.request import pytest from meshnet_tracker.accounts import AccountStore from meshnet_tracker.auth import sign_hive_request from meshnet_tracker.billing import BillingLedger from meshnet_tracker.server import TrackerServer HIVE_SECRET = "test-hive-secret" # ---------------------------------------------------------------- unit tests def test_first_account_is_admin_then_users(): store = AccountStore() first = store.register(email="admin@example.com", password="secret-123") second = store.register(email="user@example.com", password="secret-123") assert first["role"] == "admin" assert second["role"] == "user" def test_register_requires_email_or_wallet_and_password_length(): store = AccountStore() with pytest.raises(ValueError, match="email or a wallet"): store.register(password="secret-123") with pytest.raises(ValueError, match="invalid email"): store.register(email="not-an-email", password="secret-123") with pytest.raises(ValueError, match="at least 8"): store.register(email="a@b.co", password="short") def test_register_rejects_duplicate_identifiers(): store = AccountStore() store.register(email="dup@example.com", password="secret-123") with pytest.raises(ValueError, match="already exists"): store.register(email="DUP@example.com", password="other-secret") def test_register_and_update_nickname(): store = AccountStore() account = store.register( email="nick@example.com", password="secret-123", nickname=" Alpha ", ) assert account["nickname"] == "Alpha" updated = store.update_profile(account["account_id"], nickname="Beta") assert updated["nickname"] == "Beta" cleared = store.update_profile(account["account_id"], nickname=None) assert cleared["nickname"] is None def test_nickname_replicates_across_stores(): leader = AccountStore() follower = AccountStore() account = leader.register( email="nick@example.com", password="secret-123", nickname="HiveNick", ) leader.update_profile(account["account_id"], nickname="Renamed") events, _ = leader.events_since(0) follower.apply_events(events) view = follower.get_account(account["account_id"]) assert view is not None assert view["nickname"] == "Renamed" def test_login_by_email_or_wallet(): store = AccountStore() account = store.register( email="both@example.com", wallet="WalletXYZ", password="secret-123" ) assert store.verify_login("both@example.com", "secret-123")["account_id"] == account["account_id"] assert store.verify_login("WalletXYZ", "secret-123")["account_id"] == account["account_id"] assert store.verify_login("both@example.com", "wrong-password") is None assert store.verify_login("nobody@example.com", "secret-123") is None def test_sessions_resolve_and_destroy(): store = AccountStore() account = store.register(email="s@example.com", password="secret-123") token = store.create_session(account["account_id"]) assert store.session_account(token)["account_id"] == account["account_id"] store.destroy_session(token) assert store.session_account(token) is None assert store.session_account("bogus") is None def test_sessions_persist_across_restart(tmp_path): db = str(tmp_path / "accounts.db") store = AccountStore(db_path=db) account = store.register(email="cookie@example.com", password="secret-123") token = store.create_session(account["account_id"]) store.save_to_db() reloaded = AccountStore(db_path=db) assert reloaded.session_account(token)["account_id"] == account["account_id"] def test_api_key_lifecycle(): store = AccountStore() account = store.register(email="k@example.com", password="secret-123") other = store.register(email="other@example.com", password="secret-123") key = store.create_api_key(account["account_id"]) assert key.startswith("sk-mesh-") assert store.keys_for(account["account_id"]) == [key] # someone else's account cannot revoke it assert store.revoke_api_key(other["account_id"], key) is False assert store.revoke_api_key(account["account_id"], key) is True assert store.keys_for(account["account_id"]) == [] assert store.is_key_revoked(key) def test_accounts_persist_across_restart(tmp_path): db = str(tmp_path / "accounts.db") store = AccountStore(db_path=db) account = store.register(email="p@example.com", password="secret-123") key = store.create_api_key(account["account_id"]) store.save_to_db() reloaded = AccountStore(db_path=db) assert reloaded.verify_login("p@example.com", "secret-123") is not None assert reloaded.keys_for(account["account_id"]) == [key] def test_account_events_replicate_and_dedupe(): leader = AccountStore() follower = AccountStore() account = leader.register(email="r@example.com", password="secret-123") key = leader.create_api_key(account["account_id"]) leader.revoke_api_key(account["account_id"], key) events, cursor = leader.events_since(0) assert follower.apply_events(events) == len(events) assert follower.apply_events(events) == 0 # replay is a no-op assert follower.verify_login("r@example.com", "secret-123") is not None assert follower.is_key_revoked(key) more, _ = leader.events_since(cursor) assert more == [] # ---------------------------------------------------------- HTTP integration def _call(url, method="GET", body=None, token=None): headers = {"Content-Type": "application/json"} if token: headers["Authorization"] = f"Bearer {token}" data = json.dumps(body).encode() if body is not None else None req = urllib.request.Request(url, data=data, headers=headers, method=method) with urllib.request.urlopen(req) as r: return json.loads(r.read()) @pytest.fixture def account_tracker(): """Tracker with credit features pinned OFF (defaults are devnet-friendly 1.0).""" ledger = BillingLedger(starting_credit=0.0, default_price_per_1k=0.02) tracker = TrackerServer( billing=ledger, accounts=AccountStore(), hive_secret=HIVE_SECRET, starting_credit=0.0, devnet_topup_amount=0.0, ) port = tracker.start() yield f"http://127.0.0.1:{port}", ledger tracker.stop() def test_register_login_and_account_view(account_tracker): url, _ = account_tracker reg = _call(f"{url}/v1/auth/register", "POST", {"email": "admin@example.com", "password": "secret-123"}) assert reg["account"]["role"] == "admin" assert reg["api_key"].startswith("sk-mesh-") assert reg["session_token"] login = _call(f"{url}/v1/auth/login", "POST", {"identifier": "admin@example.com", "password": "secret-123"}) me = _call(f"{url}/v1/account", token=login["session_token"]) assert me["account"]["email"] == "admin@example.com" assert me["api_keys"] == [reg["api_key"]] assert me["total_balance"] == pytest.approx(0.0) assert me["usage"]["requests"] == 0 assert "records" not in me["usage"] assert "recent" not in me["usage"] def test_account_usage_endpoint_returns_records(account_tracker): url, ledger = account_tracker reg = _call(f"{url}/v1/auth/register", "POST", {"email": "usage@example.com", "password": "secret-123"}) ledger.charge_request(reg["api_key"], "test-model", total_tokens=42, node_work=[("wallet-1", 1)]) usage = _call(f"{url}/v1/account/usage", token=reg["session_token"]) assert usage["requests"] == 1 assert usage["total_tokens"] == 42 assert len(usage["records"]) == 1 assert usage["records"][0]["model"] == "test-model" def test_account_nickname_register_and_profile_update(account_tracker): url, _ = account_tracker reg = _call(f"{url}/v1/auth/register", "POST", { "email": "nick@example.com", "password": "secret-123", "nickname": "Operator", }) assert reg["account"]["nickname"] == "Operator" updated = _call( f"{url}/v1/account/profile", "POST", {"nickname": "Renamed"}, token=reg["session_token"], ) assert updated["account"]["nickname"] == "Renamed" me = _call(f"{url}/v1/account", token=reg["session_token"]) assert me["account"]["nickname"] == "Renamed" def test_login_sets_cookie_and_cookie_auth_survives_tracker_restart(tmp_path): accounts_db = str(tmp_path / "accounts.db") tracker = TrackerServer( billing=BillingLedger(starting_credit=0.0, default_price_per_1k=0.02), accounts_db=accounts_db, starting_credit=0.0, devnet_topup_amount=0.0, ) port = tracker.start() url = f"http://127.0.0.1:{port}" try: _call(f"{url}/v1/auth/register", "POST", {"email": "cookie-http@example.com", "password": "secret-123"}) req = urllib.request.Request( f"{url}/v1/auth/login", data=json.dumps({ "identifier": "cookie-http@example.com", "password": "secret-123", }).encode(), headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req) as r: assert json.loads(r.read())["session_token"] cookie_header = r.headers["Set-Cookie"] finally: tracker.stop() cookie = http.cookies.SimpleCookie(cookie_header) session_cookie = cookie["meshnet_session"].OutputString() restarted = TrackerServer( billing=BillingLedger(starting_credit=0.0, default_price_per_1k=0.02), accounts_db=accounts_db, starting_credit=0.0, devnet_topup_amount=0.0, ) restarted_port = restarted.start() restarted_url = f"http://127.0.0.1:{restarted_port}" try: req = urllib.request.Request( f"{restarted_url}/v1/account", headers={"Cookie": session_cookie}, method="GET", ) with urllib.request.urlopen(req) as r: me = json.loads(r.read()) finally: restarted.stop() assert me["account"]["email"] == "cookie-http@example.com" def test_bad_credentials_and_missing_session_are_401(account_tracker): url, _ = account_tracker _call(f"{url}/v1/auth/register", "POST", {"email": "a@example.com", "password": "secret-123"}) with pytest.raises(urllib.error.HTTPError) as exc_info: _call(f"{url}/v1/auth/login", "POST", {"identifier": "a@example.com", "password": "wrong-pass"}) assert exc_info.value.code == 401 with pytest.raises(urllib.error.HTTPError) as exc_info: _call(f"{url}/v1/account") assert exc_info.value.code == 401 def test_key_create_revoke_and_revoked_key_rejected_by_proxy(account_tracker): url, _ = account_tracker reg = _call(f"{url}/v1/auth/register", "POST", {"email": "k@example.com", "password": "secret-123"}) token = reg["session_token"] new_key = _call(f"{url}/v1/account/keys", "POST", {}, token=token)["api_key"] me = _call(f"{url}/v1/account", token=token) assert sorted(me["api_keys"]) == sorted([reg["api_key"], new_key]) _call(f"{url}/v1/account/keys/revoke", "POST", {"api_key": new_key}, token=token) with pytest.raises(urllib.error.HTTPError) as exc_info: _call(f"{url}/v1/chat/completions", "POST", {"model": "any", "messages": []}, token=new_key) assert exc_info.value.code == 401 assert "revoked" in exc_info.value.read().decode() def test_admin_listing_requires_admin_role(account_tracker): url, _ = account_tracker admin = _call(f"{url}/v1/auth/register", "POST", {"email": "admin@example.com", "password": "secret-123"}) user = _call(f"{url}/v1/auth/register", "POST", {"wallet": "WalletUser1", "password": "secret-123"}) with pytest.raises(urllib.error.HTTPError) as exc_info: _call(f"{url}/v1/admin/accounts", token=user["session_token"]) assert exc_info.value.code == 403 listing = _call(f"{url}/v1/admin/accounts", token=admin["session_token"]) accounts = listing["accounts"] assert len(accounts) == 2 assert accounts[0]["role"] == "admin" assert accounts[1]["wallet"] == "WalletUser1" assert "balances" in accounts[0] def test_accounts_gossip_endpoint_applies_events(account_tracker): url, _ = account_tracker peer = AccountStore() peer.register(email="remote@example.com", password="secret-123") events, _ = peer.events_since(0) body = json.dumps({"events": events}).encode() req = urllib.request.Request( f"{url}/v1/accounts/gossip", data=body, headers={"Content-Type": "application/json", **sign_hive_request(HIVE_SECRET, body)}, method="POST", ) with urllib.request.urlopen(req) as r: result = json.loads(r.read()) assert result["applied"] == len(events) login = _call(f"{url}/v1/auth/login", "POST", {"identifier": "remote@example.com", "password": "secret-123"}) assert login["account"]["email"] == "remote@example.com" def test_accounts_endpoints_404_when_disabled(): tracker = TrackerServer() # no accounts, no billing port = tracker.start() try: with pytest.raises(urllib.error.HTTPError) as exc_info: _call(f"http://127.0.0.1:{port}/v1/auth/register", "POST", {"email": "x@example.com", "password": "secret-123"}) assert exc_info.value.code == 404 finally: tracker.stop() # ------------------------------------------- US-039/US-040: credit and top-up @pytest.fixture def funded_tracker(): """Tracker with Caller Credit and the devnet top-up faucet enabled.""" ledger = BillingLedger(starting_credit=0.0, default_price_per_1k=0.02) tracker = TrackerServer( billing=ledger, accounts=AccountStore(), hive_secret=HIVE_SECRET, starting_credit=1.0, devnet_topup_amount=10.0, ) port = tracker.start() yield f"http://127.0.0.1:{port}", ledger tracker.stop() def test_caller_credit_granted_once_per_account(funded_tracker): url, ledger = funded_tracker reg = _call(f"{url}/v1/auth/register", "POST", {"email": "c@example.com", "password": "secret-123"}) token = reg["session_token"] first_key = reg["api_key"] assert ledger.get_client_balance(first_key) == pytest.approx(1.0) # A second key never re-grants — not even after revoking the first. second = _call(f"{url}/v1/account/keys", "POST", {}, token=token) assert second["caller_credit_granted"] is False assert ledger.get_client_balance(second["api_key"]) == pytest.approx(0.0) _call(f"{url}/v1/account/keys/revoke", "POST", {"api_key": first_key}, token=token) third = _call(f"{url}/v1/account/keys", "POST", {}, token=token) assert third["caller_credit_granted"] is False assert ledger.get_client_balance(third["api_key"]) == pytest.approx(0.0) def test_unknown_bearer_key_rejected_by_proxy(funded_tracker): url, ledger = funded_tracker with pytest.raises(urllib.error.HTTPError) as exc_info: _call(f"{url}/v1/chat/completions", "POST", {"model": "any", "messages": []}, token="sk-mesh-made-up-key") assert exc_info.value.code == 401 assert "unknown API key" in exc_info.value.read().decode() # The invented key must not have become a billable client. assert ledger.get_client_balance("sk-mesh-made-up-key") == pytest.approx(0.0) def test_devnet_topup_credits_own_key_only(funded_tracker): url, ledger = funded_tracker owner = _call(f"{url}/v1/auth/register", "POST", {"email": "own@example.com", "password": "secret-123"}) other = _call(f"{url}/v1/auth/register", "POST", {"email": "oth@example.com", "password": "secret-123"}) me = _call(f"{url}/v1/account", token=owner["session_token"]) assert me["topup_amount"] == pytest.approx(10.0) result = _call(f"{url}/v1/account/topup", "POST", {"api_key": owner["api_key"]}, token=owner["session_token"]) assert result["credited"] == pytest.approx(10.0) assert result["balance"] == pytest.approx(11.0) # 1.0 caller credit + 10.0 with pytest.raises(urllib.error.HTTPError) as exc_info: _call(f"{url}/v1/account/topup", "POST", {"api_key": owner["api_key"]}, token=other["session_token"]) assert exc_info.value.code == 403 assert ledger.get_client_balance(owner["api_key"]) == pytest.approx(11.0) def test_topup_404_when_disabled(account_tracker): url, _ = account_tracker reg = _call(f"{url}/v1/auth/register", "POST", {"email": "t@example.com", "password": "secret-123"}) me = _call(f"{url}/v1/account", token=reg["session_token"]) assert me["topup_amount"] == pytest.approx(0.0) with pytest.raises(urllib.error.HTTPError) as exc_info: _call(f"{url}/v1/account/topup", "POST", {"api_key": reg["api_key"]}, token=reg["session_token"]) assert exc_info.value.code == 404