mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-03 07:21:54 +00:00
feat(dashboard-auth): define DashboardAuthProvider ABC + Session dataclass
Phase 1, Task 1.1. New package hermes_cli/dashboard_auth/ contains:
base.py - DashboardAuthProvider ABC with 5 abstract methods
(start_login, complete_login, verify_session,
refresh_session, revoke_session), Session + LoginStart
frozen dataclasses, three exception types
(ProviderError / InvalidCodeError / RefreshExpiredError),
and assert_protocol_compliance() for plugins to call
in their own tests.
registry.py - Module-level register/get/list/clear with a lock.
Nothing reads the registry yet — Phase 2 adds the StubAuthProvider and
Phase 3 wires the gate middleware. The plugin hook lands in Task 1.3.
This commit is contained in:
parent
949ad95e4b
commit
2dc6d03a3d
4 changed files with 373 additions and 0 deletions
117
tests/hermes_cli/test_dashboard_auth_provider_base.py
Normal file
117
tests/hermes_cli/test_dashboard_auth_provider_base.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
"""Contract test for DashboardAuthProvider implementations.
|
||||
|
||||
Every provider plugin should call ``assert_protocol_compliance`` on its
|
||||
provider class in its own unit test. This module tests the abstract base
|
||||
itself: dataclass fields, ABC rejection of partial impls, and the
|
||||
protocol-compliance helper.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.dashboard_auth.base import (
|
||||
DashboardAuthProvider,
|
||||
Session,
|
||||
LoginStart,
|
||||
assert_protocol_compliance,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dataclasses
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_session_has_required_fields():
|
||||
s = Session(
|
||||
user_id="u1",
|
||||
email="a@b.com",
|
||||
display_name="A",
|
||||
org_id="org_1",
|
||||
provider="test",
|
||||
expires_at=1234567890,
|
||||
access_token="at",
|
||||
refresh_token="rt",
|
||||
)
|
||||
assert s.user_id == "u1"
|
||||
assert s.provider == "test"
|
||||
assert s.expires_at == 1234567890
|
||||
|
||||
|
||||
def test_login_start_has_redirect_and_state():
|
||||
ls = LoginStart(
|
||||
redirect_url="https://portal/authorize?...",
|
||||
cookie_payload={"hermes_session_pkce": "verifier=abc;state=xyz"},
|
||||
)
|
||||
assert ls.redirect_url.startswith("https://")
|
||||
assert "hermes_session_pkce" in ls.cookie_payload
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ABC enforcement
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_abstract_provider_cannot_be_instantiated():
|
||||
with pytest.raises(TypeError):
|
||||
DashboardAuthProvider() # type: ignore[abstract]
|
||||
|
||||
|
||||
class _BrokenProvider(DashboardAuthProvider):
|
||||
name = "broken"
|
||||
display_name = "Broken"
|
||||
# Deliberately missing all the methods.
|
||||
|
||||
|
||||
def test_assert_protocol_compliance_rejects_partial_impl():
|
||||
with pytest.raises(TypeError):
|
||||
assert_protocol_compliance(_BrokenProvider)
|
||||
|
||||
|
||||
class _CompliantProvider(DashboardAuthProvider):
|
||||
name = "ok"
|
||||
display_name = "OK"
|
||||
|
||||
def start_login(self, *, redirect_uri: str) -> LoginStart:
|
||||
return LoginStart(redirect_url="x", cookie_payload={})
|
||||
|
||||
def complete_login(self, *, code, state, code_verifier, redirect_uri) -> Session:
|
||||
return Session(
|
||||
user_id="u", email="x", display_name="x", org_id="o",
|
||||
provider=self.name, expires_at=0,
|
||||
access_token="a", refresh_token="r",
|
||||
)
|
||||
|
||||
def verify_session(self, *, access_token: str):
|
||||
return None
|
||||
|
||||
def refresh_session(self, *, refresh_token: str) -> Session:
|
||||
return Session(
|
||||
user_id="u", email="x", display_name="x", org_id="o",
|
||||
provider=self.name, expires_at=0,
|
||||
access_token="a", refresh_token="r",
|
||||
)
|
||||
|
||||
def revoke_session(self, *, refresh_token: str) -> None:
|
||||
return None
|
||||
|
||||
|
||||
def test_assert_protocol_compliance_accepts_full_impl():
|
||||
# Returns None on success; the helper raises on failure.
|
||||
assert assert_protocol_compliance(_CompliantProvider) is None
|
||||
|
||||
|
||||
def test_assert_protocol_compliance_rejects_missing_name_attr():
|
||||
class NoName(_CompliantProvider):
|
||||
name = "" # empty is treated as missing
|
||||
|
||||
with pytest.raises(TypeError, match="name"):
|
||||
assert_protocol_compliance(NoName)
|
||||
|
||||
|
||||
def test_assert_protocol_compliance_rejects_missing_display_name():
|
||||
class NoDisplay(_CompliantProvider):
|
||||
display_name = ""
|
||||
|
||||
with pytest.raises(TypeError, match="display_name"):
|
||||
assert_protocol_compliance(NoDisplay)
|
||||
Loading…
Add table
Add a link
Reference in a new issue