Skip to content

Commit 17cdd76

Browse files
committed
Create httpx client using Home Assistant helper to avoid SSL blocking
1 parent 98cc6f9 commit 17cdd76

File tree

4 files changed

+283
-4
lines changed

4 files changed

+283
-4
lines changed

.vscode/settings.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,10 @@
55
},
66
"[python]": {
77
"editor.defaultFormatter": "charliermarsh.ruff"
8-
}
8+
},
9+
"python.testing.pytestArgs": [
10+
"venv"
11+
],
12+
"python.testing.unittestEnabled": false,
13+
"python.testing.pytestEnabled": true
914
}

tests/conftest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ class UpdateFailed(Exception):
125125
update_coordinator.DataUpdateCoordinator = MockUpdateCoordinator
126126
update_coordinator.UpdateFailed = UpdateFailed
127127

128+
# Mock httpx_client helper
129+
httpx_client = Mock()
130+
httpx_client.get_async_client = Mock()
131+
httpx_client.create_async_httpx_client = Mock()
132+
128133
# Mock homeassistant modules
129134
homeassistant = Mock()
130135
homeassistant.config_entries = config_entries
@@ -133,6 +138,7 @@ class UpdateFailed(Exception):
133138
homeassistant.const = const
134139
homeassistant.helpers.entity = entity
135140
homeassistant.helpers.entity_platform = entity_platform
141+
homeassistant.helpers.httpx_client = httpx_client
136142
homeassistant.helpers.update_coordinator = update_coordinator
137143
homeassistant.util.dt = dt_util
138144

@@ -147,6 +153,7 @@ class UpdateFailed(Exception):
147153
sys.modules["homeassistant.helpers"] = Mock()
148154
sys.modules["homeassistant.helpers.entity"] = entity
149155
sys.modules["homeassistant.helpers.entity_platform"] = entity_platform
156+
sys.modules["homeassistant.helpers.httpx_client"] = httpx_client
150157
sys.modules["homeassistant.helpers.update_coordinator"] = update_coordinator
151158
sys.modules["homeassistant.helpers.selector"] = Mock()
152159
sys.modules["homeassistant.helpers.device_registry"] = Mock()

tests/test_core.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
"""Test the core module."""
2+
3+
from __future__ import annotations
4+
5+
from unittest.mock import AsyncMock, Mock, patch
6+
7+
import pytest
8+
9+
from unifi_network.core import UnifiNetworkCore
10+
11+
12+
@pytest.fixture
13+
def mock_hass():
14+
"""Create a mock Home Assistant instance."""
15+
hass = Mock()
16+
hass.data = {}
17+
return hass
18+
19+
20+
@pytest.fixture
21+
def mock_httpx_client():
22+
"""Create a mock httpx AsyncClient."""
23+
return AsyncMock()
24+
25+
26+
@patch("unifi_network.core.create_async_httpx_client")
27+
@patch("unifi_network.core.Client")
28+
@patch("unifi_network.core.UnifiDeviceCoordinator")
29+
@patch("unifi_network.core.UnifiClientCoordinator")
30+
@pytest.mark.asyncio
31+
async def test_init_creates_client_and_async_init_refreshes_coordinators(
32+
mock_client_coordinator,
33+
mock_device_coordinator,
34+
mock_client_class,
35+
mock_create_client,
36+
mock_hass,
37+
):
38+
"""Test that __init__ creates client and async_init refreshes coordinators."""
39+
mock_client = Mock()
40+
mock_httpx_client = Mock()
41+
mock_httpx_client.headers = Mock()
42+
mock_client_class.return_value = mock_client
43+
mock_create_client.return_value = mock_httpx_client
44+
45+
mock_device_coordinator_instance = AsyncMock()
46+
mock_client_coordinator_instance = AsyncMock()
47+
mock_device_coordinator.return_value = mock_device_coordinator_instance
48+
mock_client_coordinator.return_value = mock_client_coordinator_instance
49+
50+
core = UnifiNetworkCore(
51+
hass=mock_hass,
52+
base_url="https://unifi.example.com",
53+
site_id="default",
54+
api_key="test-key",
55+
enable_devices=True,
56+
enable_clients=True,
57+
verify_ssl=True,
58+
)
59+
60+
# Verify create_async_httpx_client was called with correct parameters during __init__
61+
mock_create_client.assert_called_once_with(
62+
mock_hass,
63+
verify_ssl=True,
64+
base_url="https://unifi.example.com",
65+
)
66+
67+
# Verify headers were added to the httpx client during __init__
68+
mock_httpx_client.headers.update.assert_called_once_with({"X-API-Key": "test-key"})
69+
70+
# Verify Client was called with correct parameters during __init__
71+
mock_client_class.assert_called_once_with(base_url="https://unifi.example.com")
72+
73+
# Verify set_async_httpx_client was called during __init__
74+
mock_client.set_async_httpx_client.assert_called_once_with(mock_httpx_client)
75+
76+
# Verify the client was created during __init__
77+
assert core.client == mock_client
78+
79+
await core.async_init()
80+
81+
# Verify coordinators were refreshed
82+
mock_device_coordinator_instance.async_config_entry_first_refresh.assert_called_once()
83+
mock_client_coordinator_instance.async_config_entry_first_refresh.assert_called_once()
84+
85+
86+
@patch("unifi_network.core.create_async_httpx_client")
87+
@patch("unifi_network.core.Client")
88+
@patch("unifi_network.core.UnifiDeviceCoordinator")
89+
@patch("unifi_network.core.UnifiClientCoordinator")
90+
@pytest.mark.asyncio
91+
async def test_init_respects_verify_ssl_false(
92+
mock_client_coordinator,
93+
mock_device_coordinator,
94+
mock_client_class,
95+
mock_create_client,
96+
mock_hass,
97+
):
98+
"""Test that __init__ respects verify_ssl=False setting."""
99+
# Setup mocks
100+
mock_client = Mock()
101+
mock_httpx_client = Mock()
102+
mock_httpx_client.headers = Mock()
103+
mock_client_class.return_value = mock_client
104+
mock_create_client.return_value = mock_httpx_client
105+
mock_device_coordinator_instance = AsyncMock()
106+
mock_client_coordinator_instance = AsyncMock()
107+
mock_device_coordinator.return_value = mock_device_coordinator_instance
108+
mock_client_coordinator.return_value = mock_client_coordinator_instance
109+
110+
# Create core instance with verify_ssl=False
111+
core = UnifiNetworkCore(
112+
hass=mock_hass,
113+
base_url="https://unifi.example.com",
114+
site_id="default",
115+
api_key="test-key",
116+
enable_devices=True,
117+
enable_clients=True,
118+
verify_ssl=False,
119+
)
120+
121+
# Verify create_async_httpx_client was called with verify_ssl=False during __init__
122+
mock_create_client.assert_called_once_with(
123+
mock_hass,
124+
verify_ssl=False,
125+
base_url="https://unifi.example.com",
126+
)
127+
128+
# Verify headers were added to the httpx client during __init__
129+
mock_httpx_client.headers.update.assert_called_once_with({"X-API-Key": "test-key"})
130+
131+
# Verify Client was called with correct parameters during __init__
132+
mock_client_class.assert_called_once_with(base_url="https://unifi.example.com")
133+
134+
# Verify set_async_httpx_client was called during __init__
135+
mock_client.set_async_httpx_client.assert_called_once_with(mock_httpx_client)
136+
137+
# Verify the client was created during __init__
138+
assert core.client == mock_client
139+
140+
# Call async_init
141+
await core.async_init()
142+
143+
# Verify coordinators were refreshed
144+
mock_device_coordinator_instance.async_config_entry_first_refresh.assert_called_once()
145+
mock_client_coordinator_instance.async_config_entry_first_refresh.assert_called_once()
146+
147+
148+
@patch("unifi_network.core.create_async_httpx_client")
149+
@patch("unifi_network.core.Client")
150+
@pytest.mark.asyncio
151+
async def test_init_without_coordinators(
152+
mock_client_class,
153+
mock_create_client,
154+
mock_hass,
155+
):
156+
"""Test that __init__ creates client when coordinators are disabled."""
157+
# Setup mocks
158+
mock_client = Mock()
159+
mock_httpx_client = Mock()
160+
mock_httpx_client.headers = Mock()
161+
mock_client_class.return_value = mock_client
162+
mock_create_client.return_value = mock_httpx_client
163+
164+
# Create core instance with both coordinators disabled
165+
core = UnifiNetworkCore(
166+
hass=mock_hass,
167+
base_url="https://unifi.example.com",
168+
site_id="default",
169+
api_key="test-key",
170+
enable_devices=False,
171+
enable_clients=False,
172+
verify_ssl=True,
173+
)
174+
175+
# Verify create_async_httpx_client was called with correct parameters during __init__
176+
mock_create_client.assert_called_once_with(
177+
mock_hass,
178+
verify_ssl=True,
179+
base_url="https://unifi.example.com",
180+
)
181+
182+
# Verify headers were added to the httpx client during __init__
183+
mock_httpx_client.headers.update.assert_called_once_with({"X-API-Key": "test-key"})
184+
185+
# Verify Client was called with correct parameters during __init__
186+
mock_client_class.assert_called_once_with(base_url="https://unifi.example.com")
187+
188+
# Verify set_async_httpx_client was called during __init__
189+
mock_client.set_async_httpx_client.assert_called_once_with(mock_httpx_client)
190+
191+
# Verify the client was created during __init__ but coordinators are None
192+
assert core.client == mock_client
193+
assert core.device_coordinator is None
194+
assert core.client_coordinator is None
195+
196+
# Call async_init
197+
await core.async_init()
198+
199+
# Since coordinators are disabled, no refresh calls should be made
200+
# This completes successfully without any coordinator operations
201+
202+
203+
@patch("unifi_network.core.create_async_httpx_client")
204+
@patch("unifi_network.core.Client")
205+
@pytest.mark.asyncio
206+
async def test_init_without_api_key(
207+
mock_client_class,
208+
mock_create_client,
209+
mock_hass,
210+
):
211+
"""Test that __init__ works when api_key is None."""
212+
# Setup mocks
213+
mock_client = Mock()
214+
mock_httpx_client = Mock()
215+
mock_httpx_client.headers = Mock()
216+
mock_client_class.return_value = mock_client
217+
mock_create_client.return_value = mock_httpx_client
218+
219+
# Create core instance without api_key
220+
core = UnifiNetworkCore(
221+
hass=mock_hass,
222+
base_url="https://unifi.example.com",
223+
site_id="default",
224+
api_key=None,
225+
enable_devices=False,
226+
enable_clients=False,
227+
verify_ssl=True,
228+
)
229+
230+
# Verify create_async_httpx_client was called with correct parameters during __init__
231+
mock_create_client.assert_called_once_with(
232+
mock_hass,
233+
verify_ssl=True,
234+
base_url="https://unifi.example.com",
235+
)
236+
237+
# Verify headers were NOT updated when api_key is None
238+
mock_httpx_client.headers.update.assert_not_called()
239+
240+
# Verify Client was called with correct parameters during __init__
241+
mock_client_class.assert_called_once_with(base_url="https://unifi.example.com")
242+
243+
# Verify set_async_httpx_client was called during __init__
244+
mock_client.set_async_httpx_client.assert_called_once_with(mock_httpx_client)
245+
246+
# Verify the client was created during __init__
247+
assert core.client == mock_client
248+
assert core.device_coordinator is None
249+
assert core.client_coordinator is None
250+
251+
# Call async_init
252+
await core.async_init()
253+
254+
# Since coordinators are disabled, no refresh calls should be made
255+
# This completes successfully without any coordinator operations

unifi_network/core.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
from homeassistant.core import HomeAssistant
6+
from homeassistant.helpers.httpx_client import create_async_httpx_client
67

78
from .api_client import Client
89
from .coordinator import UnifiClientCoordinator, UnifiDeviceCoordinator
@@ -25,9 +26,20 @@ def __init__(
2526
self.hass = hass
2627
self.site_id = site_id
2728

28-
# Initialize API client
29-
headers = {"X-API-Key": api_key} if api_key else None
30-
self.client = Client(base_url=base_url, headers=headers, verify_ssl=verify_ssl)
29+
# Create httpx client using Home Assistant helper to avoid SSL blocking
30+
async_httpx_client = create_async_httpx_client(
31+
hass,
32+
verify_ssl=verify_ssl,
33+
base_url=base_url,
34+
)
35+
36+
# Add API headers to the client
37+
if api_key:
38+
async_httpx_client.headers.update({"X-API-Key": api_key})
39+
40+
# Initialize API client and set httpx client
41+
self.client = Client(base_url=base_url)
42+
self.client.set_async_httpx_client(async_httpx_client)
3143

3244
# Initialize coordinators based on enabled features
3345
self.device_coordinator = None

0 commit comments

Comments
 (0)