Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions spond/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,17 @@ async def wrapper(self, *args, **kwargs):
return wrapper

async def login(self) -> None:
login_url = f"{self.api_url}login"
login_url = f"{self.api_url}auth2/login"
data = {"email": self.username, "password": self.password}
async with self.clientsession.post(login_url, json=data) as r:
login_result = await r.json()
self.token = login_result.get("loginToken")
if self.token is None:
err_msg = f"Login failed. Response received: {login_result}"
raise AuthenticationError(err_msg)
self.token = self._extract_access_token(login_result)

@staticmethod
def _extract_access_token(login_result: dict) -> str:
access = login_result.get("accessToken")
if isinstance(access, dict):
token = access.get("token")
if isinstance(token, str) and token:
return token
raise AuthenticationError(f"Login failed. Response received: {login_result}")
62 changes: 62 additions & 0 deletions tests/test_spond.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import pytest

from spond import AuthenticationError
from spond.base import _SpondBase
from spond.spond import Spond

Expand Down Expand Up @@ -340,3 +341,64 @@ async def test_get_posts__api_error_raises(self, mock_get, mock_token) -> None:

with pytest.raises(ValueError, match="401"):
await s.get_posts()


class TestLogin:
@pytest.mark.parametrize(
("login_result", "expected"),
[
(
{"accessToken": {"token": "ABC", "expiration": "2026-05-14T12:00:00Z"}},
"ABC",
),
],
)
def test_extract_access_token__happy_path(self, login_result, expected) -> None:
assert _SpondBase._extract_access_token(login_result) == expected

@pytest.mark.parametrize(
"login_result",
[
{"error": "Invalid credentials"},
{"accessToken": None},
{"accessToken": {}},
{"accessToken": {"token": ""}},
{"accessToken": {"token": None}},
],
)
def test_extract_access_token__bad_shape_raises(self, login_result) -> None:
with pytest.raises(AuthenticationError):
_SpondBase._extract_access_token(login_result)

@pytest.mark.asyncio
@patch("aiohttp.ClientSession.post")
async def test_login__happy_path(self, mock_post) -> None:
mock_response = {
"accessToken": {"token": "ABC", "expiration": "2026-05-14T12:00:00Z"},
"refreshToken": {"token": "REF", "expiration": "2026-08-11T12:00:00Z"},
"passwordToken": {"token": "PWD", "expiration": "2026-05-13T13:00:00Z"},
}
mock_post.return_value.__aenter__.return_value.json = AsyncMock(
return_value=mock_response
)

s = Spond(MOCK_USERNAME, MOCK_PASSWORD)
await s.login()

mock_post.assert_called_once_with(
"https://api.spond.com/core/v1/auth2/login",
json={"email": MOCK_USERNAME, "password": MOCK_PASSWORD},
)
assert s.token == "ABC"

@pytest.mark.asyncio
@patch("aiohttp.ClientSession.post")
async def test_login__error_response_raises(self, mock_post) -> None:
mock_post.return_value.__aenter__.return_value.json = AsyncMock(
return_value={"error": "Invalid credentials"}
)

s = Spond(MOCK_USERNAME, MOCK_PASSWORD)
with pytest.raises(AuthenticationError):
await s.login()
assert s.token is None
Loading