Reimplement UI injection (#236)
This commit is contained in:
committed by
GitHub
parent
fdc93e2719
commit
fd3643685d
260
tests/test_state_store.py
Normal file
260
tests/test_state_store.py
Normal file
@@ -0,0 +1,260 @@
|
||||
"""Tests for the state store."""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from auth_oidc.stores.state_store import MAX_DEVICE_CODE_ATTEMPTS, StateStore
|
||||
|
||||
TEST_IP = "127.0.0.1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state_store_generate_and_receive_state(hass: HomeAssistant):
|
||||
"""Test creating a state, storing user info, and receiving it once."""
|
||||
store_mock = AsyncMock()
|
||||
with patch("homeassistant.helpers.storage.Store", return_value=store_mock):
|
||||
state_store = StateStore(hass)
|
||||
|
||||
store_mock.async_load.return_value = {}
|
||||
await state_store.async_load()
|
||||
assert state_store.get_data() == {}
|
||||
|
||||
redirect_uri = "https://example.com/callback"
|
||||
state_id = await state_store.async_create_state_from_url(redirect_uri, TEST_IP)
|
||||
assert state_id in state_store.get_data()
|
||||
assert (
|
||||
await state_store.async_get_redirect_uri_for_state(state_id, TEST_IP)
|
||||
== redirect_uri
|
||||
)
|
||||
|
||||
user_info = {
|
||||
"sub": "user1",
|
||||
"display_name": "Test User",
|
||||
"username": "testuser",
|
||||
"role": "system-users",
|
||||
}
|
||||
assert (
|
||||
await state_store.async_add_userinfo_to_state(state_id, user_info) is True
|
||||
)
|
||||
assert state_id in state_store.get_data()
|
||||
assert await state_store.async_is_state_ready(state_id, TEST_IP) is True
|
||||
assert state_id in state_store.get_data()
|
||||
|
||||
result = await state_store.async_receive_userinfo_for_state(state_id, TEST_IP)
|
||||
assert result == user_info
|
||||
assert state_id not in state_store.get_data()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state_store_generate_code_and_link_state(hass: HomeAssistant):
|
||||
"""Test generating a device code and linking another state to it."""
|
||||
store_mock = AsyncMock()
|
||||
with patch("homeassistant.helpers.storage.Store", return_value=store_mock):
|
||||
state_store = StateStore(hass)
|
||||
|
||||
store_mock.async_load.return_value = {}
|
||||
await state_store.async_load()
|
||||
|
||||
donor_state = await state_store.async_create_state_from_url(
|
||||
"https://example.com/donor", TEST_IP
|
||||
)
|
||||
target_state = await state_store.async_create_state_from_url(
|
||||
"https://example.com/target", TEST_IP
|
||||
)
|
||||
|
||||
code = await state_store.async_generate_code_for_state(target_state)
|
||||
assert code is not None
|
||||
assert len(code) == 6
|
||||
assert code.isdigit()
|
||||
|
||||
user_info = {
|
||||
"sub": "user2",
|
||||
"display_name": "Device User",
|
||||
"username": "deviceuser",
|
||||
"role": "system-admin",
|
||||
}
|
||||
assert (
|
||||
await state_store.async_add_userinfo_to_state(donor_state, user_info)
|
||||
is True
|
||||
)
|
||||
assert donor_state in state_store.get_data()
|
||||
|
||||
assert (
|
||||
await state_store.async_link_state_to_code(donor_state, code, TEST_IP)
|
||||
is True
|
||||
)
|
||||
assert donor_state not in state_store.get_data()
|
||||
assert await state_store.async_is_state_ready(target_state, TEST_IP) is True
|
||||
assert target_state in state_store.get_data()
|
||||
assert (
|
||||
await state_store.async_receive_userinfo_for_state(target_state, TEST_IP)
|
||||
== user_info
|
||||
)
|
||||
assert target_state not in state_store.get_data()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state_store_link_state_returns_false_for_wrong_code(hass: HomeAssistant):
|
||||
"""Test linking fails when the device code does not match any state."""
|
||||
store_mock = AsyncMock()
|
||||
with patch("homeassistant.helpers.storage.Store", return_value=store_mock):
|
||||
state_store = StateStore(hass)
|
||||
|
||||
store_mock.async_load.return_value = {}
|
||||
await state_store.async_load()
|
||||
|
||||
donor_state = await state_store.async_create_state_from_url(
|
||||
"https://example.com/donor", TEST_IP
|
||||
)
|
||||
target_state = await state_store.async_create_state_from_url(
|
||||
"https://example.com/target", TEST_IP
|
||||
)
|
||||
await state_store.async_generate_code_for_state(target_state)
|
||||
|
||||
user_info = {
|
||||
"sub": "user3",
|
||||
"display_name": "Wrong Code User",
|
||||
"username": "wrongcode",
|
||||
"role": "system-users",
|
||||
}
|
||||
assert (
|
||||
await state_store.async_add_userinfo_to_state(donor_state, user_info)
|
||||
is True
|
||||
)
|
||||
|
||||
assert (
|
||||
await state_store.async_link_state_to_code(donor_state, "000000", TEST_IP)
|
||||
is False
|
||||
)
|
||||
assert donor_state in state_store.get_data()
|
||||
assert await state_store.async_is_state_ready(target_state, TEST_IP) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state_store_throttles_device_code_link_attempts(hass: HomeAssistant):
|
||||
"""Test that repeated wrong device codes are throttled per state."""
|
||||
store_mock = AsyncMock()
|
||||
with patch("homeassistant.helpers.storage.Store", return_value=store_mock):
|
||||
state_store = StateStore(hass)
|
||||
|
||||
store_mock.async_load.return_value = {}
|
||||
await state_store.async_load()
|
||||
|
||||
donor_state = await state_store.async_create_state_from_url(
|
||||
"https://example.com/donor", TEST_IP
|
||||
)
|
||||
target_state = await state_store.async_create_state_from_url(
|
||||
"https://example.com/target", TEST_IP
|
||||
)
|
||||
code = await state_store.async_generate_code_for_state(target_state)
|
||||
assert code is not None
|
||||
|
||||
user_info = {
|
||||
"sub": "user-throttle",
|
||||
"display_name": "Throttle User",
|
||||
"username": "throttle",
|
||||
"role": "system-users",
|
||||
}
|
||||
assert await state_store.async_add_userinfo_to_state(donor_state, user_info)
|
||||
|
||||
for _ in range(MAX_DEVICE_CODE_ATTEMPTS):
|
||||
assert (
|
||||
await state_store.async_link_state_to_code(
|
||||
donor_state, "000000", TEST_IP
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
assert (
|
||||
await state_store.async_link_state_to_code(donor_state, code, TEST_IP)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state_store_expired_state(hass: HomeAssistant):
|
||||
"""Test that expired states are treated as invalid."""
|
||||
store_mock = AsyncMock()
|
||||
with patch("homeassistant.helpers.storage.Store", return_value=store_mock):
|
||||
state_store = StateStore(hass)
|
||||
|
||||
store_mock.async_load.return_value = {}
|
||||
await state_store.async_load()
|
||||
|
||||
state_id = await state_store.async_create_state_from_url(
|
||||
"https://example.com/expired", TEST_IP
|
||||
)
|
||||
state_store.get_data()[state_id]["expiration"] = (
|
||||
datetime.now(timezone.utc) - timedelta(minutes=10)
|
||||
).isoformat()
|
||||
|
||||
assert (
|
||||
await state_store.async_get_redirect_uri_for_state(state_id, TEST_IP)
|
||||
is None
|
||||
)
|
||||
assert await state_store.async_is_state_ready(state_id, TEST_IP) is False
|
||||
assert (
|
||||
await state_store.async_receive_userinfo_for_state(state_id, TEST_IP)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state_store_data_not_loaded(hass: HomeAssistant):
|
||||
"""Test that using the store before loading raises RuntimeError."""
|
||||
store_mock = AsyncMock()
|
||||
with patch("homeassistant.helpers.storage.Store", return_value=store_mock):
|
||||
state_store = StateStore(hass)
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
await state_store.async_create_state_from_url(
|
||||
"https://example.com", TEST_IP
|
||||
)
|
||||
with pytest.raises(RuntimeError):
|
||||
await state_store.async_generate_code_for_state("state")
|
||||
with pytest.raises(RuntimeError):
|
||||
await state_store.async_add_userinfo_to_state(
|
||||
"state",
|
||||
{
|
||||
"sub": "user4",
|
||||
"display_name": "Not Loaded",
|
||||
"username": "notloaded",
|
||||
"role": "system-users",
|
||||
},
|
||||
)
|
||||
with pytest.raises(RuntimeError):
|
||||
await state_store.async_get_redirect_uri_for_state("state", TEST_IP)
|
||||
with pytest.raises(RuntimeError):
|
||||
await state_store.async_is_state_ready("state", TEST_IP)
|
||||
with pytest.raises(RuntimeError):
|
||||
await state_store.async_link_state_to_code("state", "123456", TEST_IP)
|
||||
with pytest.raises(RuntimeError):
|
||||
await state_store.async_receive_userinfo_for_state("state", TEST_IP)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state_store_missing_keys(hass: HomeAssistant):
|
||||
"""Test that missing keys raise correct responses."""
|
||||
store_mock = AsyncMock()
|
||||
with patch("homeassistant.helpers.storage.Store", return_value=store_mock):
|
||||
state_store = StateStore(hass)
|
||||
|
||||
# async_generate_code_for_state returns None if state_id is not found
|
||||
store_mock.async_load.return_value = {}
|
||||
await state_store.async_load()
|
||||
assert await state_store.async_generate_code_for_state("nonexistent") is None
|
||||
|
||||
# async_add_userinfo_to_state returns False if state_id is not found
|
||||
user_info = {
|
||||
"sub": "user5",
|
||||
"display_name": "Missing Keys",
|
||||
"username": "missingkeys",
|
||||
"role": "system-users",
|
||||
}
|
||||
assert (
|
||||
await state_store.async_add_userinfo_to_state("nonexistent", user_info)
|
||||
is False
|
||||
)
|
||||
Reference in New Issue
Block a user