Files
familiar/tests/test_account.py
T
LatteandClaude Opus 4.8 eb6440db40
CI / lint (pull_request) Successful in 51s
CI / lint (push) Successful in 50s
CI / test (pull_request) Successful in 57s
CI / publish (pull_request) Has been skipped
CI / test (push) Successful in 57s
CI / publish (push) Successful in 50s
Phase 2: EVE SSO auth + read-only account tools
Add PKCE OAuth2 login against EVE SSO with an encrypted, auto-refreshing
token store, driven by `familiar login` / `status` / `logout` CLI commands.
The MCP server gains authenticated account tools:

- account_status, get_wallet, get_skills, get_skill_queue
- get_location, get_clones, get_assets (paginated)

ESI client extended with Bearer auth and X-Pages pagination. 51 tests
(auth + account mocked), coverage 88%. Client id/callback/scopes are
env-overridable; refresh token Fernet-encrypted on disk.

Closes #7

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 18:32:47 +02:00

103 lines
3.3 KiB
Python

"""Authenticated account tools (auth mocked, ESI mocked)."""
import asyncio
import httpx
import pytest
from familiar import account, auth
from familiar.server import mcp
from conftest import route
@pytest.fixture
def fake_auth(monkeypatch):
async def _stub():
return "fake-token", 99
monkeypatch.setattr(auth, "get_auth", _stub)
return 99
def test_get_wallet(mock_esi, fake_auth):
mock_esi(lambda req: httpx.Response(200, json=1234567.89))
result = asyncio.run(account.get_wallet())
assert result["character_id"] == 99
assert result["balance"] == 1234567.89
def test_get_skills_summarizes(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())
assert result["total_sp"] == 5_000_000
assert result["skills_trained"] == 3
def test_get_skill_queue_limits(mock_esi, fake_auth):
queue = [
{"skill_id": i, "finished_level": 5, "queue_position": i, "finish_date": "2026"}
for i in range(5)
]
mock_esi(lambda req: route(req, {"/skillqueue/": queue}))
result = asyncio.run(account.get_skill_queue(limit=2))
assert result["in_queue"] == 5
assert len(result["upcoming"]) == 2
def test_get_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())
assert result["solar_system_id"] == 30000142
assert result["ship_name"] == "My Capsule"
assert result["online"] is True
def test_get_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())
assert result["jump_clones"] == 2
assert result["active_implants"] == [10000, 10001]
def test_get_assets_paginates(mock_esi, fake_auth):
# Two pages of assets; the client should follow X-Pages and concatenate.
def handler(req):
page = req.url.params.get("page", "1")
if page == "1":
return httpx.Response(
200,
json=[{"type_id": 34, "quantity": 1, "location_id": 1}],
headers={"X-Pages": "2"},
)
return httpx.Response(
200,
json=[{"type_id": 35, "quantity": 2, "location_id": 1}],
headers={"X-Pages": "2"},
)
mock_esi(handler)
result = asyncio.run(account.get_assets(limit=20))
assert result["total_stacks"] == 2
assert {a["type_id"] for a in result["sample"]} == {34, 35}
def test_account_status_delegates(monkeypatch):
monkeypatch.setattr(auth, "status", lambda: {"authenticated": True, "character_id": 1})
result = asyncio.run(account.account_status())
assert result["authenticated"] is True
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