test: prove the revocation wiring end-to-end (resolve_passport -> decide_trust)
Some checks failed
check / gate (push) Has been cancelled
Some checks failed
check / gate (push) Has been cancelled
Monkeypatched httpx so resolve_passport sees a real revoked trust body
(status=revoked, band=unproven, allowed=[]) and must raise
PassportNotInGoodStanding — proving the WIRING, not just the decision. This is
the path that stops a validly-signed EPT that outlived its passport's
revocation (~365-day tokens).
Live-confirmed alongside: Eternitas refuses to mint EPTs for revoked bots
("credentials are not issued for non-active bots"), so the only exposure was a
pre-existing token — exactly what this now catches. The active agent's EPT still
returns 200. A fully-live revoked test would require revoking a real fleet
passport (destructive), so the wiring is proven deterministically instead.
79 tests green.
Co-Authored-By: Claude (Fable 5) <noreply@anthropic.com>
This commit is contained in:
@@ -197,3 +197,66 @@ def test_unknown_or_missing_status_fails_closed():
|
||||
{"status": ""}, {}):
|
||||
with pytest.raises(PassportNotInGoodStanding):
|
||||
decide_trust(body, "ET26-X")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_passport_raises_on_revoked_status_wiring(monkeypatch):
|
||||
"""Proves the WIRING, not just the decision: resolve_passport must feed the
|
||||
real trust body through decide_trust and propagate the refusal. A validly
|
||||
minted EPT can outlive the passport by ~a year, so this is the path that
|
||||
stops a revoked-but-still-signed token."""
|
||||
|
||||
from api.app import auth
|
||||
from api.app.auth import PassportNotInGoodStanding, resolve_passport
|
||||
from api.app.config import Settings
|
||||
|
||||
class _Resp:
|
||||
status_code = 200
|
||||
|
||||
def json(self):
|
||||
return {"status": "revoked", "band": "unproven", "allowed_actions": []}
|
||||
|
||||
class _Client:
|
||||
def __init__(self, *a, **k):
|
||||
pass
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *a):
|
||||
return False
|
||||
|
||||
async def get(self, *a, **k):
|
||||
return _Resp()
|
||||
|
||||
monkeypatch.setattr(auth.httpx, "AsyncClient", _Client)
|
||||
settings = Settings(eternitas_platform_api_key="x", eternitas_base_url="https://api.eternitas.ai")
|
||||
|
||||
with pytest.raises(PassportNotInGoodStanding):
|
||||
await resolve_passport(settings, "ET26-NJQT-QMR0")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_passport_returns_band_on_active_wiring(monkeypatch):
|
||||
import httpx # noqa: F401
|
||||
|
||||
from api.app import auth
|
||||
from api.app.auth import resolve_passport
|
||||
from api.app.config import Settings
|
||||
|
||||
class _Resp:
|
||||
status_code = 200
|
||||
|
||||
def json(self):
|
||||
return {"status": "active", "band": "gold", "allowed_actions": ["read"]}
|
||||
|
||||
class _Client:
|
||||
def __init__(self, *a, **k): pass
|
||||
async def __aenter__(self): return self
|
||||
async def __aexit__(self, *a): return False
|
||||
async def get(self, *a, **k): return _Resp()
|
||||
|
||||
monkeypatch.setattr(auth.httpx, "AsyncClient", _Client)
|
||||
settings = Settings(eternitas_platform_api_key="x")
|
||||
band, actions = await resolve_passport(settings, "ET26-1EF9-VJAN")
|
||||
assert band == "gold" and actions == ("read",)
|
||||
|
||||
Reference in New Issue
Block a user