mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-08-05 02:45:28 +00:00
Some checks failed
CI / Focused test guidance (report-only) (push) Has been cancelled
CI / Python syntax (compileall) (push) Has been cancelled
CI / JS syntax (node --check) (push) Has been cancelled
CI / Python tests (pytest) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Container scan / hadolint (Dockerfile lint) (push) Has been cancelled
Container scan (Trivy) / Trivy (image scan, advisory) (push) Has been cancelled
Container scan (Trivy) / Trivy (image scan + SARIF upload) (push) Has been cancelled
Dependency review / dependency-review (PR gate) (push) Has been cancelled
Dependency review / pip-audit (advisory) (push) Has been cancelled
ci / docker publish / build (amd64) (push) Has been cancelled
ci / docker publish / build (arm64) (push) Has been cancelled
Secret scan / gitleaks (push) Has been cancelled
Workflow security / actionlint (push) Has been cancelled
Workflow security / zizmor (Actions SAST) (push) Has been cancelled
ci / docker publish / merge manifest + tag (push) Has been cancelled
132 lines
3.6 KiB
Python
132 lines
3.6 KiB
Python
import asyncio
|
|
import json
|
|
from src import mcp_oauth
|
|
|
|
|
|
def test_registry_resolve_returns_code_and_state():
|
|
async def go():
|
|
fut = mcp_oauth.register_pending("st-1")
|
|
assert mcp_oauth.resolve_pending("st-1", "the-code") is True
|
|
return await asyncio.wait_for(fut, timeout=1)
|
|
code, state = asyncio.run(go())
|
|
assert code == "the-code"
|
|
assert state == "st-1"
|
|
|
|
|
|
def test_resolve_unknown_state_is_false():
|
|
assert mcp_oauth.resolve_pending("nope", "x") is False
|
|
|
|
|
|
def test_register_pending_prunes_abandoned_flows():
|
|
import time as _t
|
|
|
|
async def go():
|
|
mcp_oauth._pending.clear()
|
|
mcp_oauth._pending_ts.clear()
|
|
old = mcp_oauth.register_pending("old-state")
|
|
# Backdate the entry past the authorization window.
|
|
mcp_oauth._pending_ts["old-state"] = _t.monotonic() - (mcp_oauth.AUTH_WAIT_SECONDS + 1)
|
|
# A new registration triggers a prune of the stale one.
|
|
mcp_oauth.register_pending("new-state")
|
|
return old
|
|
|
|
old = asyncio.run(go())
|
|
assert "old-state" not in mcp_oauth._pending
|
|
assert "old-state" not in mcp_oauth._pending_ts
|
|
assert "new-state" in mcp_oauth._pending
|
|
assert old.cancelled()
|
|
|
|
|
|
def test_build_provider_has_odysseus_client_metadata():
|
|
p = mcp_oauth.build_provider("srv-1", "https://example.com/mcp")
|
|
md = p.context.client_metadata
|
|
assert md.client_name == "Odysseus"
|
|
assert "authorization_code" in md.grant_types
|
|
assert "refresh_token" in md.grant_types
|
|
assert str(md.redirect_uris[0]).rstrip("/") == mcp_oauth.REDIRECT_URI.rstrip("/")
|
|
|
|
|
|
def test_db_token_storage_round_trip():
|
|
from mcp.shared.auth import OAuthToken
|
|
|
|
class FakeSrv:
|
|
oauth_tokens = None
|
|
|
|
srv = FakeSrv()
|
|
|
|
class FakeQuery:
|
|
def filter(self, *a):
|
|
return self
|
|
|
|
def first(self):
|
|
return srv
|
|
|
|
class FakeSession:
|
|
def query(self, *a):
|
|
return FakeQuery()
|
|
|
|
def commit(self):
|
|
pass
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
storage = mcp_oauth.DbTokenStorage("srv-1", session_factory=lambda: FakeSession())
|
|
|
|
async def go():
|
|
await storage.set_tokens(OAuthToken(access_token="abc", token_type="Bearer"))
|
|
return await storage.get_tokens()
|
|
|
|
t = asyncio.run(go())
|
|
assert t.access_token == "abc"
|
|
assert srv.oauth_tokens is not None # persisted as JSON
|
|
|
|
|
|
def _fake_storage(oauth_tokens):
|
|
class FakeSrv:
|
|
pass
|
|
|
|
srv = FakeSrv()
|
|
srv.oauth_tokens = oauth_tokens
|
|
|
|
class FakeQuery:
|
|
def filter(self, *a):
|
|
return self
|
|
|
|
def first(self):
|
|
return srv
|
|
|
|
class FakeSession:
|
|
def query(self, *a):
|
|
return FakeQuery()
|
|
|
|
def commit(self):
|
|
pass
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
return srv, mcp_oauth.DbTokenStorage("srv-1", session_factory=lambda: FakeSession())
|
|
|
|
|
|
def test_load_falls_back_to_empty_dict_for_non_dict_json():
|
|
# A corrupted/migrated oauth_tokens column holding a JSON array, not an
|
|
# object, must not crash _load()'s callers with AttributeError.
|
|
_srv, storage = _fake_storage('["stale", "data"]')
|
|
assert storage._load() == {}
|
|
|
|
|
|
def test_get_tokens_returns_none_for_non_dict_oauth_tokens():
|
|
_srv, storage = _fake_storage("42")
|
|
|
|
async def go():
|
|
return await storage.get_tokens()
|
|
|
|
assert asyncio.run(go()) is None
|
|
|
|
|
|
def test_update_recovers_from_non_dict_oauth_tokens():
|
|
# _update() must not raise TypeError trying to item-assign into a list.
|
|
srv, storage = _fake_storage('["stale", "data"]')
|
|
storage._update("tokens", {"access_token": "new"})
|
|
assert json.loads(srv.oauth_tokens) == {"tokens": {"access_token": "new"}}
|