diff --git a/decart/tokens/client.py b/decart/tokens/client.py index d4daa17..8421b87 100644 --- a/decart/tokens/client.py +++ b/decart/tokens/client.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import aiohttp @@ -20,6 +20,9 @@ class TokensClient: client = DecartClient(api_key=os.getenv("DECART_API_KEY")) token = await client.tokens.create() # Returns: CreateTokenResponse(api_key="ek_...", expires_at="...") + + # With metadata: + token = await client.tokens.create(metadata={"role": "viewer"}) ``` """ @@ -29,10 +32,17 @@ def __init__(self, parent: "DecartClient") -> None: async def _get_session(self) -> aiohttp.ClientSession: return await self._parent._get_session() - async def create(self) -> CreateTokenResponse: + async def create( + self, + *, + metadata: dict[str, Any] | None = None, + ) -> CreateTokenResponse: """ Create a client token. + Args: + metadata: Optional custom key-value pairs to attach to the token. + Returns: A short-lived API key safe for client-side use. @@ -40,6 +50,9 @@ async def create(self) -> CreateTokenResponse: ```python token = await client.tokens.create() # Returns: CreateTokenResponse(api_key="ek_...", expires_at="...") + + # With metadata: + token = await client.tokens.create(metadata={"role": "viewer"}) ``` Raises: @@ -48,12 +61,17 @@ async def create(self) -> CreateTokenResponse: session = await self._get_session() endpoint = f"{self._parent.base_url}/v1/client/tokens" + headers = { + "X-API-KEY": self._parent.api_key, + "User-Agent": build_user_agent(self._parent.integration), + } + + body = {"metadata": metadata} if metadata is not None else {} + async with session.post( endpoint, - headers={ - "X-API-KEY": self._parent.api_key, - "User-Agent": build_user_agent(self._parent.integration), - }, + headers=headers, + json=body, ) as response: if not response.ok: error_text = await response.text() diff --git a/tests/test_tokens.py b/tests/test_tokens.py index ccaa40e..a77cb5f 100644 --- a/tests/test_tokens.py +++ b/tests/test_tokens.py @@ -66,3 +66,51 @@ async def test_create_token_403_error() -> None: with patch.object(client, "_get_session", AsyncMock(return_value=mock_session)): with pytest.raises(TokenCreateError, match="Failed to create token"): await client.tokens.create() + + +@pytest.mark.asyncio +async def test_create_token_with_metadata() -> None: + """Sends metadata as JSON body when provided.""" + client = DecartClient(api_key="test-api-key") + + mock_response = AsyncMock() + mock_response.ok = True + mock_response.json = AsyncMock( + return_value={"apiKey": "ek_test123", "expiresAt": "2024-12-15T12:10:00Z"} + ) + + mock_session = MagicMock() + mock_session.post = MagicMock( + return_value=AsyncMock(__aenter__=AsyncMock(return_value=mock_response)) + ) + + with patch.object(client, "_get_session", AsyncMock(return_value=mock_session)): + result = await client.tokens.create(metadata={"role": "viewer"}) + + assert result.api_key == "ek_test123" + assert result.expires_at == "2024-12-15T12:10:00Z" + call_kwargs = mock_session.post.call_args + assert call_kwargs.kwargs["json"] == {"metadata": {"role": "viewer"}} + + +@pytest.mark.asyncio +async def test_create_token_without_metadata_sends_null() -> None: + """Sends JSON body with null metadata when none provided.""" + client = DecartClient(api_key="test-api-key") + + mock_response = AsyncMock() + mock_response.ok = True + mock_response.json = AsyncMock( + return_value={"apiKey": "ek_test123", "expiresAt": "2024-12-15T12:10:00Z"} + ) + + mock_session = MagicMock() + mock_session.post = MagicMock( + return_value=AsyncMock(__aenter__=AsyncMock(return_value=mock_response)) + ) + + with patch.object(client, "_get_session", AsyncMock(return_value=mock_session)): + await client.tokens.create() + + call_kwargs = mock_session.post.call_args + assert call_kwargs.kwargs["json"] == {}