Skip to content
Open
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
4 changes: 2 additions & 2 deletions .secrets.baseline

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions src/api/hive_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,21 @@ def __init__( # pylint: disable=too-many-positional-arguments # noqa: PLR0913
aws_session_token="SESSION_TOKEN",
)

@property
def username(self) -> str:
"""Return the Hive username, normalised for Cognito."""
return self._username

@username.setter
def username(self, value: str) -> None:
"""Store the username trimmed and lower-cased.

Hive's Cognito pool treats the email as case-sensitive, so a
capitalised address (e.g. from mobile autocapitalisation) fails
with an invalid-password error even though the account exists.
"""
self._username = value.strip().lower() if isinstance(value, str) else value

def generate_random_small_a(self):
"""Helper function to generate a random big integer.

Expand Down
15 changes: 15 additions & 0 deletions src/api/hive_auth_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@ def __init__( # pylint: disable=too-many-positional-arguments # noqa: PLR0913
self._region: str | None = None
self.client: Any = None

@property
def username(self) -> str:
"""Return the Hive username, normalised for Cognito."""
return self._username

@username.setter
def username(self, value: str) -> None:
"""Store the username trimmed and lower-cased.

Hive's Cognito pool treats the email as case-sensitive, so a
capitalised address (e.g. from mobile autocapitalisation) fails
with an invalid-password error even though the account exists.
"""
self._username = value.strip().lower() if isinstance(value, str) else value

async def async_init(self):
"""Initialise async variables."""
self.loop = asyncio.get_running_loop()
Expand Down
13 changes: 13 additions & 0 deletions tests/unit/test_hive_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -1279,3 +1279,16 @@ def test_without_client_secret_no_secret_hash(self):
response = auth.process_device_challenge(params)

assert "SECRET_HASH" not in response


class TestUsernameNormalisation:
"""Hive's Cognito pool is case-sensitive; usernames are lower-cased."""

def test_constructor_normalises_username(self):
auth = _make_auth(username=" User@Example.COM ")
assert auth.username == "user@example.com"

def test_reassignment_is_normalised(self):
auth = _make_auth()
auth.username = "Someone@Example.com"
assert auth.username == "someone@example.com"
27 changes: 27 additions & 0 deletions tests/unit/test_hive_auth_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -1428,3 +1428,30 @@ async def test_async_init_raises_when_login_info_is_none(self):
with patch("asyncio.get_running_loop", return_value=mock_loop):
with pytest.raises(HiveUnknownConfiguration):
await auth.async_init()


class TestUsernameNormalisation:
"""Hive's Cognito pool is case-sensitive; usernames are lower-cased."""

@pytest.mark.parametrize(
"typed",
["User@Example.com", "USER@EXAMPLE.COM", " user@example.com "],
)
async def test_constructor_normalises_username(self, typed):
auth = await _make_auth(username=typed)
assert auth.username == "user@example.com"

async def test_reassignment_is_normalised(self):
"""start_session reassigns auth.username from the HA config entry."""
auth = await _make_auth()
auth.username = "Someone@Example.com"
assert auth.username == "someone@example.com"

async def test_initiate_auth_sends_lowercase_username(self):
auth = await _make_auth(username="User@Example.com")
params = await auth.get_auth_params()
assert params["USERNAME"] == "user@example.com"

async def test_file_mode_detected_with_any_case(self):
auth = await _make_auth(username="Use@File.com")
assert auth.use_file is True
Loading