forked from homeassistant-ai/ha-mcp
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_supervisor_client.py
More file actions
153 lines (119 loc) · 6.22 KB
/
Copy pathtest_supervisor_client.py
File metadata and controls
153 lines (119 loc) · 6.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
"""Unit tests for the shared Supervisor httpx client factory.
Covers :func:`ha_mcp.client.supervisor_client.make_supervisor_httpx_client`,
which builds the ``httpx.AsyncClient`` instances used by the three direct-
Supervisor call sites (``rest_client._supervisor_logs_get``,
``tools_bug_report._fetch_addon_logs``, ``settings_ui._restart_addon``).
The factory itself is a one-liner around ``httpx.AsyncClient`` — these tests
guard the contract the three call sites rely on:
- ``base_url`` resolved through :func:`ha_mcp._version.get_supervisor_base_url`
(so the ``SUPERVISOR_BASE_URL`` E2E override flows through)
- ``Authorization: Bearer ${SUPERVISOR_TOKEN}`` preset on the client
- ``timeout`` / ``verify`` forwarded verbatim, both as plain ``float`` and as
:class:`httpx.Timeout`
- Absent or empty ``SUPERVISOR_TOKEN`` produces a ``"Bearer "`` header (the
call-site policy is to short-circuit before reaching here; this test pins
the documented graceful-degradation behaviour)
"""
from unittest.mock import patch
import httpx
import pytest
from ha_mcp.client.supervisor_client import make_supervisor_httpx_client
@pytest.fixture
def supervisor_token(monkeypatch: pytest.MonkeyPatch) -> str:
"""Set a non-empty ``SUPERVISOR_TOKEN`` for the duration of one test."""
token = "test-supervisor-token-abc123"
monkeypatch.setenv("SUPERVISOR_TOKEN", token)
return token
@pytest.fixture
def no_supervisor_token(monkeypatch: pytest.MonkeyPatch) -> None:
"""Ensure ``SUPERVISOR_TOKEN`` is unset for the duration of one test."""
monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False)
@pytest.fixture
def no_base_url_override(monkeypatch: pytest.MonkeyPatch) -> None:
"""Ensure no ``SUPERVISOR_BASE_URL`` override leaks in from the host env."""
monkeypatch.delenv("SUPERVISOR_BASE_URL", raising=False)
@pytest.mark.asyncio
async def test_returns_async_client(
supervisor_token: str, no_base_url_override: None
) -> None:
"""Factory returns a ready-to-use ``httpx.AsyncClient``."""
async with make_supervisor_httpx_client(timeout=5.0, verify=True) as client:
assert isinstance(client, httpx.AsyncClient)
@pytest.mark.asyncio
async def test_base_url_defaults_to_supervisor(
supervisor_token: str, no_base_url_override: None
) -> None:
"""Default base URL is the in-addon ``http://supervisor`` hostname."""
async with make_supervisor_httpx_client(timeout=5.0, verify=True) as client:
assert str(client.base_url) == "http://supervisor"
@pytest.mark.asyncio
async def test_base_url_honors_env_override(
supervisor_token: str, monkeypatch: pytest.MonkeyPatch
) -> None:
"""``SUPERVISOR_BASE_URL`` env override flows through to the client.
This is the path E2E tests use to point the call sites at a local mock
without /etc/hosts hacks — guarding it here keeps that wiring intact.
"""
monkeypatch.setenv("SUPERVISOR_BASE_URL", "http://127.0.0.1:9876")
async with make_supervisor_httpx_client(timeout=5.0, verify=True) as client:
assert str(client.base_url) == "http://127.0.0.1:9876"
@pytest.mark.asyncio
async def test_authorization_header_uses_token_from_env(
supervisor_token: str, no_base_url_override: None
) -> None:
"""``Authorization`` header is ``Bearer <SUPERVISOR_TOKEN>``."""
async with make_supervisor_httpx_client(timeout=5.0, verify=True) as client:
assert client.headers["Authorization"] == f"Bearer {supervisor_token}"
@pytest.mark.asyncio
async def test_authorization_header_with_absent_token(
no_supervisor_token: None, no_base_url_override: None
) -> None:
"""Missing ``SUPERVISOR_TOKEN`` falls back to a literal ``Bearer `` header.
Each call site short-circuits before reaching the factory when the token
is absent (see module docstring); this test pins the graceful-degradation
contract for any future direct caller that forgets to guard.
"""
async with make_supervisor_httpx_client(timeout=5.0, verify=True) as client:
assert client.headers["Authorization"] == "Bearer "
@pytest.mark.asyncio
async def test_timeout_forwarded_as_float(
supervisor_token: str, no_base_url_override: None
) -> None:
"""A plain ``float`` timeout is forwarded to the underlying client.
httpx normalises ``float`` to a :class:`httpx.Timeout` with the same
value applied to all four phases — assert connect + read carry the
value through.
"""
async with make_supervisor_httpx_client(timeout=7.5, verify=True) as client:
assert client.timeout.connect == 7.5
assert client.timeout.read == 7.5
@pytest.mark.asyncio
async def test_timeout_forwarded_as_httpx_timeout(
supervisor_token: str, no_base_url_override: None
) -> None:
"""An :class:`httpx.Timeout` instance is forwarded verbatim.
``rest_client._supervisor_logs_get`` passes ``httpx.Timeout(self.timeout)``
rather than a bare float; this test guards that the helper accepts the
rich type unchanged.
"""
timeout = httpx.Timeout(connect=2.0, read=30.0, write=30.0, pool=30.0)
async with make_supervisor_httpx_client(timeout=timeout, verify=True) as client:
assert client.timeout.connect == 2.0
assert client.timeout.read == 30.0
def test_verify_forwarded_to_async_client_kwarg(supervisor_token: str) -> None:
"""``verify`` is forwarded verbatim to the underlying ``httpx.AsyncClient``.
httpx does not expose the constructor's ``verify`` value via any public
attribute on the client, so a real-instance assertion can't tell apart
``verify=True`` from ``verify=False``. Patch the constructor and check
the kwarg directly — this is the only way to guard against a regression
that drops the parameter on the floor while still returning a valid
client. The ``verify`` flag is a no-op for plain ``http://supervisor``
today, but call sites pass it for symmetry with the HTTPS-override path
(#1128) and that contract has to hold.
"""
with patch("ha_mcp.client.supervisor_client.httpx.AsyncClient") as mock_ctor:
make_supervisor_httpx_client(timeout=5.0, verify=False)
assert mock_ctor.call_args.kwargs["verify"] is False
mock_ctor.reset_mock()
make_supervisor_httpx_client(timeout=5.0, verify=True)
assert mock_ctor.call_args.kwargs["verify"] is True