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>
63 lines
1.7 KiB
Python
63 lines
1.7 KiB
Python
"""Shared test helpers: a mock ESI client backed by httpx.MockTransport."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
from collections.abc import Callable
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from familiar import esi
|
|
|
|
|
|
def make_jwt(claims: dict) -> str:
|
|
"""Build a fake JWT (header.payload.signature) whose payload is `claims`.
|
|
|
|
Only the payload segment is ever decoded by familiar, so the header and
|
|
signature can be arbitrary.
|
|
"""
|
|
payload = base64.urlsafe_b64encode(json.dumps(claims).encode()).rstrip(b"=").decode()
|
|
return f"header.{payload}.signature"
|
|
|
|
|
|
Handler = Callable[[httpx.Request], httpx.Response]
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_esi():
|
|
"""Install a mock ESI client driven by a request handler.
|
|
|
|
Usage::
|
|
|
|
def test_x(mock_esi):
|
|
def handler(request):
|
|
return httpx.Response(200, json={...})
|
|
mock_esi(handler)
|
|
...
|
|
|
|
The shared client is reset after the test.
|
|
"""
|
|
|
|
def _make(handler: Handler) -> esi.EsiClient:
|
|
client = esi.EsiClient(transport=httpx.MockTransport(handler))
|
|
esi.set_client(client)
|
|
return client
|
|
|
|
yield _make
|
|
esi.set_client(None)
|
|
|
|
|
|
def route(request: httpx.Request, fixtures: dict[str, object]) -> httpx.Response:
|
|
"""Match a request against ``{substring_in_path: json_body}`` and respond.
|
|
|
|
The special key ``"/universe/ids/"`` is matched for the name-resolution POST.
|
|
A missing match returns 404 so tests fail loudly.
|
|
"""
|
|
path = request.url.path
|
|
for needle, body in fixtures.items():
|
|
if needle in path:
|
|
return httpx.Response(200, json=body)
|
|
return httpx.Response(404, json={"error": f"no fixture for {path}"})
|