1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2026-04-02 00:07:16 +01:00
Files
supervisor/tests/test_ingress.py
Stefan Agner 3147d080a2 Unify Core user handling with HomeAssistantUser model (#6558)
* Unify Core user listing with HomeAssistantUser model

Replace the ingress-specific IngressSessionDataUser with a general
HomeAssistantUser dataclass that models the Core config/auth/list WS
response. This deduplicates the WS call (previously in both auth.py
and module.py) into a single HomeAssistant.list_users() method.

- Add HomeAssistantUser dataclass with fields matching Core's user API
- Remove get_users() and its unnecessary 5-minute Job throttle
- Auth and ingress consumers both use HomeAssistant.list_users()
- Auth API endpoint uses typed attribute access instead of dict keys
- Migrate session serialization from legacy "displayname" to "name"
- Accept both keys in schema/deserialization for backwards compat
- Add test for loading persisted sessions with legacy displayname key

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Tighten list_users() to trust Core's auth/list contract

Core's config/auth/list WS command always returns a list, never None.
Replace the silent `if not raw: return []` (which also swallowed empty
lists) with an assert, remove the dead AuthListUsersNoneResponseError
exception class, and document the HomeAssistantWSError contract in the
docstring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove | None from async_send_command return type

The WebSocket result is always set from data["result"] in _receive_json,
never explicitly to None. Remove the misleading | None from the return
type of both WSClient and HomeAssistantWebSocket async_send_command, and
drop the now-unnecessary assert in list_users.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Use HomeAssistantWSConnectionError in _ensure_connected

_ensure_connected and connect_with_auth raise on connection-level
failures, so use the more specific HomeAssistantWSConnectionError
instead of the broad HomeAssistantWSError. This allows callers to
distinguish connection errors from Core API errors (e.g. unsuccessful
WebSocket command responses). Also document that _ensure_connected can
propagate HomeAssistantAuthError from ensure_access_token.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove user list cache from _find_user_by_id

Drop the _list_of_users cache to avoid stale auth data in ingress
session creation. The method now fetches users fresh each time and
returns None on any API error instead of serving potentially outdated
cached results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 18:31:08 +01:00

141 lines
4.4 KiB
Python

"""Test ingress."""
from datetime import timedelta
import json
from pathlib import Path
from unittest.mock import ANY, patch
from supervisor.const import HomeAssistantUser, IngressSessionData
from supervisor.coresys import CoreSys
from supervisor.ingress import Ingress
from supervisor.utils.dt import utc_from_timestamp
from supervisor.utils.json import read_json_file
def test_session_handling(coresys: CoreSys):
"""Create and test session."""
session = coresys.ingress.create_session()
validate = coresys.ingress.sessions[session]
assert session
assert validate
assert coresys.ingress.validate_session(session)
assert coresys.ingress.sessions[session] != validate
not_valid = utc_from_timestamp(validate) - timedelta(minutes=20)
coresys.ingress.sessions[session] = not_valid.timestamp()
assert not coresys.ingress.validate_session(session)
assert not coresys.ingress.validate_session("invalid session")
session_data = coresys.ingress.get_session_data(session)
assert session_data is None
def test_session_handling_with_session_data(coresys: CoreSys):
"""Create and test session."""
session = coresys.ingress.create_session(
IngressSessionData(HomeAssistantUser("some-id"))
)
assert session
session_data = coresys.ingress.get_session_data(session)
assert session_data.user.id == "some-id"
async def test_save_on_unload(coresys: CoreSys):
"""Test called save on unload."""
coresys.ingress.create_session()
await coresys.ingress.unload()
assert coresys.ingress.save_data.called
async def test_dynamic_ports(coresys: CoreSys):
"""Test dyanmic port handling."""
port_test1 = await coresys.ingress.get_dynamic_port("test1")
assert port_test1
assert coresys.ingress.save_data.called
assert port_test1 == await coresys.ingress.get_dynamic_port("test1")
port_test2 = await coresys.ingress.get_dynamic_port("test2")
assert port_test2
assert port_test2 != port_test1
assert port_test2 >= 62000
assert port_test2 <= 65500
assert port_test1 >= 62000
assert port_test1 <= 65500
async def test_ingress_save_data(coresys: CoreSys, tmp_supervisor_data: Path):
"""Test saving ingress data to file."""
config_file = tmp_supervisor_data / "ingress.json"
with patch("supervisor.ingress.FILE_HASSIO_INGRESS", new=config_file):
ingress = await Ingress(coresys).load_config()
session = ingress.create_session(
IngressSessionData(HomeAssistantUser("123", name="Test", username="test"))
)
await ingress.save_data()
def get_config():
assert config_file.exists()
return read_json_file(config_file)
assert await coresys.run_in_executor(get_config) == {
"session": {session: ANY},
"session_data": {
session: {"user": {"id": "123", "name": "Test", "username": "test"}}
},
"ports": {},
}
async def test_ingress_load_legacy_displayname(
coresys: CoreSys, tmp_supervisor_data: Path
):
"""Test loading session data with legacy 'displayname' key."""
config_file = tmp_supervisor_data / "ingress.json"
session_token = "a" * 128
config_file.write_text(
json.dumps(
{
"session": {session_token: 9999999999.0},
"session_data": {
session_token: {
"user": {
"id": "456",
"displayname": "Legacy Name",
"username": "legacy",
}
}
},
"ports": {},
}
)
)
with patch("supervisor.ingress.FILE_HASSIO_INGRESS", new=config_file):
ingress = await Ingress(coresys).load_config()
session_data = ingress.get_session_data(session_token)
assert session_data is not None
assert session_data.user.id == "456"
assert session_data.user.name == "Legacy Name"
assert session_data.user.username == "legacy"
async def test_ingress_reload_ignore_none_data(coresys: CoreSys):
"""Test reloading ingress does not add None for session data and create errors."""
session = coresys.ingress.create_session()
assert session in coresys.ingress.sessions
assert session not in coresys.ingress.sessions_data
await coresys.ingress.reload()
assert session in coresys.ingress.sessions
assert session not in coresys.ingress.sessions_data