"""Authenticated account tools (auth mocked, ESI mocked), multi-character.""" import asyncio import httpx import pytest from conftest import route from familiar import account, auth from familiar.server import mcp CHARS = [ {"character_id": 99, "character_name": "Alpha", "scopes": []}, {"character_id": 100, "character_name": "Beta", "scopes": []}, ] @pytest.fixture def fake_auth(monkeypatch): monkeypatch.setattr(auth, "list_characters", lambda: list(CHARS)) async def get_auth(cid, **kw): return f"token-{cid}", cid monkeypatch.setattr(auth, "get_auth", get_auth) def resolve(identifier): for c in CHARS: if ( str(c["character_id"]) == str(identifier) or (c["character_name"] or "").lower() == str(identifier).lower() ): return c["character_id"] raise auth.NotAuthenticated("no such character") monkeypatch.setattr(auth, "resolve_character", resolve) return CHARS def test_wallet_all_characters(mock_esi, fake_auth): mock_esi(lambda req: httpx.Response(200, json=1000.0)) result = asyncio.run(account.get_wallet()) assert len(result["characters"]) == 2 assert {c["character_name"] for c in result["characters"]} == {"Alpha", "Beta"} assert all(c["balance"] == 1000.0 for c in result["characters"]) def test_wallet_single_character_by_name(mock_esi, fake_auth): mock_esi(lambda req: httpx.Response(200, json=500.0)) result = asyncio.run(account.get_wallet(character="Alpha")) assert len(result["characters"]) == 1 assert result["characters"][0]["character_id"] == 99 def test_unknown_character_raises(mock_esi, fake_auth): mock_esi(lambda req: httpx.Response(200, json=1.0)) with pytest.raises(auth.NotAuthenticated): asyncio.run(account.get_wallet(character="Ghost")) def test_not_logged_in_raises(monkeypatch): monkeypatch.setattr(auth, "list_characters", lambda: []) with pytest.raises(auth.NotAuthenticated): asyncio.run(account.get_wallet()) def test_skills_summary(mock_esi, fake_auth): body = {"total_sp": 5_000_000, "unallocated_sp": 10_000, "skills": [{}, {}, {}]} mock_esi(lambda req: route(req, {"/skills/": body})) result = asyncio.run(account.get_skills(character=99)) assert result["characters"][0]["total_sp"] == 5_000_000 assert result["characters"][0]["skills_trained"] == 3 def test_skill_queue_limit(mock_esi, fake_auth): queue = [{"skill_id": i, "finished_level": 5, "queue_position": i} for i in range(5)] mock_esi(lambda req: route(req, {"/skillqueue/": queue})) result = asyncio.run(account.get_skill_queue(character="Beta", limit=2)) entry = result["characters"][0] assert entry["in_queue"] == 5 assert len(entry["upcoming"]) == 2 def test_location_merges_endpoints(mock_esi, fake_auth): fixtures = { "/location/": {"solar_system_id": 30000142, "station_id": 60003760}, "/ship/": {"ship_type_id": 670, "ship_name": "My Capsule"}, "/online/": {"online": True, "last_login": "2026-06-27T00:00:00Z"}, } mock_esi(lambda req: route(req, fixtures)) result = asyncio.run(account.get_location(character=99)) entry = result["characters"][0] assert entry["solar_system_id"] == 30000142 assert entry["ship_name"] == "My Capsule" assert entry["online"] is True def test_clones(mock_esi, fake_auth): fixtures = { "/clones/": {"home_location": {"location_id": 60003760}, "jump_clones": [{}, {}]}, "/implants/": [10000, 10001], } mock_esi(lambda req: route(req, fixtures)) result = asyncio.run(account.get_clones(character=99)) entry = result["characters"][0] assert entry["jump_clones"] == 2 assert entry["active_implants"] == [10000, 10001] def test_assets_paginates(mock_esi, fake_auth): def handler(req): page = req.url.params.get("page", "1") type_id = 34 if page == "1" else 35 return httpx.Response( 200, json=[{"type_id": type_id, "quantity": 1, "location_id": 1}], headers={"X-Pages": "2"}, ) mock_esi(handler) result = asyncio.run(account.get_assets(character=99, limit=20)) entry = result["characters"][0] assert entry["total_stacks"] == 2 assert {a["type_id"] for a in entry["sample"]} == {34, 35} def test_account_status_delegates(monkeypatch): monkeypatch.setattr(auth, "status", lambda: {"authenticated": True, "characters": CHARS}) result = asyncio.run(account.account_status()) assert result["authenticated"] is True assert len(result["characters"]) == 2 def test_account_tools_registered(): tools = {t.name for t in asyncio.run(mcp.list_tools())} assert {"get_wallet", "get_skills", "get_location", "get_assets", "account_status"} <= tools