diff --git a/sdks/implementations/python/src/stack_auth/_token_store.py b/sdks/implementations/python/src/stack_auth/_token_store.py index f9dbb9dfb..1f2415a00 100644 --- a/sdks/implementations/python/src/stack_auth/_token_store.py +++ b/sdks/implementations/python/src/stack_auth/_token_store.py @@ -197,8 +197,9 @@ def resolve_token_store(init: TokenStoreInit, project_id: str) -> TokenStore | N access_token=init.get("access_token"), refresh_token=init.get("refresh_token"), ) - # Must be RequestLike (has .headers attribute) - return RequestTokenStore(init) # type: ignore[arg-type] + if isinstance(init, RequestLike): + return RequestTokenStore(init) + raise TypeError(f"Invalid token store initializer: {type(init)}") # --------------------------------------------------------------------------- diff --git a/sdks/implementations/python/src/stack_auth/_types.py b/sdks/implementations/python/src/stack_auth/_types.py index db6bb5fd3..601140301 100644 --- a/sdks/implementations/python/src/stack_auth/_types.py +++ b/sdks/implementations/python/src/stack_auth/_types.py @@ -1,8 +1,9 @@ from __future__ import annotations -from typing import Mapping, Protocol +from typing import Mapping, Protocol, runtime_checkable +@runtime_checkable class RequestLike(Protocol): @property def headers(self) -> Mapping[str, str]: ... diff --git a/sdks/implementations/python/tests/test_token_store.py b/sdks/implementations/python/tests/test_token_store.py index dfa566f9c..5539fa42c 100644 --- a/sdks/implementations/python/tests/test_token_store.py +++ b/sdks/implementations/python/tests/test_token_store.py @@ -215,6 +215,39 @@ class TestTokenStoreRegistry: def test_resolve_none(self) -> None: assert resolve_token_store(None, "proj") is None + def test_resolve_raises_type_error_for_invalid_input(self) -> None: + with pytest.raises(TypeError, match="Invalid token store initializer"): + resolve_token_store(12345, "proj") # type: ignore[arg-type] + + +# --------------------------------------------------------------------------- +# RequestLike runtime_checkable isinstance checks +# --------------------------------------------------------------------------- + +class TestRequestLikeProtocol: + def test_isinstance_works_on_conforming_object(self) -> None: + """RequestLike should be runtime_checkable so isinstance works.""" + from stack_auth._types import RequestLike + + request = _FakeRequest({"x-stack-auth": "{}"}) + assert isinstance(request, RequestLike) + + def test_isinstance_rejects_non_conforming_object(self) -> None: + """Objects without a .headers property should not pass isinstance.""" + from stack_auth._types import RequestLike + + assert not isinstance("a string", RequestLike) + assert not isinstance(42, RequestLike) + assert not isinstance({}, RequestLike) + + def test_resolve_token_store_returns_request_token_store_for_request_like(self) -> None: + """resolve_token_store should detect RequestLike via isinstance and return RequestTokenStore.""" + header_value = json.dumps({"accessToken": "at", "refreshToken": "rt"}) + request = _FakeRequest({"x-stack-auth": header_value}) + store = resolve_token_store(request, "proj") + assert isinstance(store, RequestTokenStore) + assert store.get_stored_access_token() == "at" + # --------------------------------------------------------------------------- # Helper functions: _is_fresh_enough, _is_expired, _decode_jwt_payload