"""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)