core/tests/components/evohome/test_storage.py

192 lines
5.9 KiB
Python

"""The tests for evohome storage load & save."""
from __future__ import annotations
from datetime import datetime, timedelta
from typing import Any, Final, NotRequired, TypedDict
import pytest
from homeassistant.components.evohome.const import DOMAIN, STORAGE_KEY, STORAGE_VER
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
from .conftest import setup_evohome
from .const import ACCESS_TOKEN, REFRESH_TOKEN, SESSION_ID, USERNAME
class _SessionDataT(TypedDict):
session_id: str
session_id_expires: NotRequired[str] # 2024-07-27T23:57:30+01:00
class _TokenStoreT(TypedDict):
username: str
refresh_token: str
access_token: str
access_token_expires: str # 2024-07-27T23:57:30+01:00
user_data: NotRequired[_SessionDataT]
class _EmptyStoreT(TypedDict):
pass
SZ_USERNAME: Final = "username"
SZ_REFRESH_TOKEN: Final = "refresh_token"
SZ_ACCESS_TOKEN: Final = "access_token"
SZ_ACCESS_TOKEN_EXPIRES: Final = "access_token_expires"
SZ_USER_DATA: Final = "user_data"
def dt_pair(dt_dtm: datetime) -> tuple[datetime, str]:
"""Return a datetime without milliseconds and its string representation."""
dt_str = dt_dtm.isoformat(timespec="seconds") # e.g. 2024-07-28T00:57:29+01:00
return dt_util.parse_datetime(dt_str, raise_on_error=True), dt_str
ACCESS_TOKEN_EXP_DTM, ACCESS_TOKEN_EXP_STR = dt_pair(dt_util.now() + timedelta(hours=1))
USERNAME_DIFF: Final = f"not_{USERNAME}"
USERNAME_SAME: Final = USERNAME
_TEST_STORAGE_BASE: Final[_TokenStoreT] = {
SZ_USERNAME: USERNAME_SAME,
SZ_REFRESH_TOKEN: REFRESH_TOKEN,
SZ_ACCESS_TOKEN: ACCESS_TOKEN,
SZ_ACCESS_TOKEN_EXPIRES: ACCESS_TOKEN_EXP_STR,
}
TEST_STORAGE_DATA: Final[dict[str, _TokenStoreT]] = {
"sans_session_id": _TEST_STORAGE_BASE,
"null_session_id": _TEST_STORAGE_BASE | {SZ_USER_DATA: None}, # type: ignore[dict-item]
"with_session_id": _TEST_STORAGE_BASE | {SZ_USER_DATA: {"session_id": SESSION_ID}},
}
TEST_STORAGE_NULL: Final[dict[str, _EmptyStoreT | None]] = {
"store_is_absent": None,
"store_was_reset": {},
}
DOMAIN_STORAGE_BASE: Final = {
"version": STORAGE_VER,
"minor_version": 1,
"key": STORAGE_KEY,
}
@pytest.mark.parametrize("install", ["minimal"])
@pytest.mark.parametrize("idx", TEST_STORAGE_NULL)
async def test_auth_tokens_null(
hass: HomeAssistant,
hass_storage: dict[str, Any],
config: dict[str, str],
idx: str,
install: str,
) -> None:
"""Test credentials manager when cache is empty."""
hass_storage[DOMAIN] = DOMAIN_STORAGE_BASE | {"data": TEST_STORAGE_NULL[idx]}
async for _ in setup_evohome(hass, config, install=install):
pass
# Confirm the expected tokens were cached to storage...
data: _TokenStoreT = hass_storage[DOMAIN]["data"]
assert data[SZ_USERNAME] == USERNAME_SAME
assert data[SZ_REFRESH_TOKEN] == f"new_{REFRESH_TOKEN}"
assert data[SZ_ACCESS_TOKEN] == f"new_{ACCESS_TOKEN}"
assert (
dt_util.parse_datetime(data[SZ_ACCESS_TOKEN_EXPIRES], raise_on_error=True)
> dt_util.now()
)
@pytest.mark.parametrize("install", ["minimal"])
@pytest.mark.parametrize("idx", TEST_STORAGE_DATA)
async def test_auth_tokens_same(
hass: HomeAssistant,
hass_storage: dict[str, Any],
config: dict[str, str],
idx: str,
install: str,
) -> None:
"""Test credentials manager when cache contains valid data for this user."""
hass_storage[DOMAIN] = DOMAIN_STORAGE_BASE | {"data": TEST_STORAGE_DATA[idx]}
async for _ in setup_evohome(hass, config, install=install):
pass
# Confirm the expected tokens were cached to storage...
data: _TokenStoreT = hass_storage[DOMAIN]["data"]
assert data[SZ_USERNAME] == USERNAME_SAME
assert data[SZ_REFRESH_TOKEN] == REFRESH_TOKEN
assert data[SZ_ACCESS_TOKEN] == ACCESS_TOKEN
assert dt_util.parse_datetime(data[SZ_ACCESS_TOKEN_EXPIRES]) == ACCESS_TOKEN_EXP_DTM
@pytest.mark.parametrize("install", ["minimal"])
@pytest.mark.parametrize("idx", TEST_STORAGE_DATA)
async def test_auth_tokens_past(
hass: HomeAssistant,
hass_storage: dict[str, Any],
config: dict[str, str],
idx: str,
install: str,
) -> None:
"""Test credentials manager when cache contains expired data for this user."""
dt_dtm, dt_str = dt_pair(dt_util.now() - timedelta(hours=1))
# make this access token have expired in the past...
test_data = TEST_STORAGE_DATA[idx].copy() # shallow copy is OK here
test_data[SZ_ACCESS_TOKEN_EXPIRES] = dt_str
hass_storage[DOMAIN] = DOMAIN_STORAGE_BASE | {"data": test_data}
async for _ in setup_evohome(hass, config, install=install):
pass
# Confirm the expected tokens were cached to storage...
data: _TokenStoreT = hass_storage[DOMAIN]["data"]
assert data[SZ_USERNAME] == USERNAME_SAME
assert data[SZ_REFRESH_TOKEN] == f"new_{REFRESH_TOKEN}"
assert data[SZ_ACCESS_TOKEN] == f"new_{ACCESS_TOKEN}"
assert (
dt_util.parse_datetime(data[SZ_ACCESS_TOKEN_EXPIRES], raise_on_error=True)
> dt_util.now()
)
@pytest.mark.parametrize("install", ["minimal"])
@pytest.mark.parametrize("idx", TEST_STORAGE_DATA)
async def test_auth_tokens_diff(
hass: HomeAssistant,
hass_storage: dict[str, Any],
config: dict[str, str],
idx: str,
install: str,
) -> None:
"""Test credentials manager when cache contains data for a different user."""
hass_storage[DOMAIN] = DOMAIN_STORAGE_BASE | {"data": TEST_STORAGE_DATA[idx]}
config["username"] = USERNAME_DIFF
async for _ in setup_evohome(hass, config, install=install):
pass
# Confirm the expected tokens were cached to storage...
data: _TokenStoreT = hass_storage[DOMAIN]["data"]
assert data[SZ_USERNAME] == USERNAME_DIFF
assert data[SZ_REFRESH_TOKEN] == f"new_{REFRESH_TOKEN}"
assert data[SZ_ACCESS_TOKEN] == f"new_{ACCESS_TOKEN}"
assert (
dt_util.parse_datetime(data[SZ_ACCESS_TOKEN_EXPIRES], raise_on_error=True)
> dt_util.now()
)