fix: actually check the endpoint during setup

async_setup_entry handed client.with_options(timeout=10.0).models.list to the
executor. On AsyncOpenAI that returns an awaitable paginator, so it came back
unawaited, no request was made, and a bad key or an unreachable endpoint still
set up. It now awaits client.models.list(timeout=10.0), matching core's
openai_conversation, so a 401 raises ConfigEntryAuthFailed and a connection
error ConfigEntryNotReady, as the README already says.

The shared mock_init_component fixture now patches models.list with an
AsyncMock and keeps the patch for the whole test, because tests that change a
subentry reload the entry and run setup again.

Reported by @MartinNuc in #1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUd68TsT5QZ68Dtfw2uWBU
This commit is contained in:
Adam SmithandClaude Opus 5.5 committed 2026-10-11 06:45:52 -04:00
1 parent 151d741a71
commit a6faa44b2f
3 files changed
+64 -7

No files matched your search

+54 -3
View File
@@ -2,6 +2,11 @@
from unittest.mock import AsyncMock, patch
import httpx
import openai
import pytest
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from pytest_homeassistant_custom_component.common import MockConfigEntry
@@ -26,7 +31,7 @@ async def test_client_built_with_base_url(hass: HomeAssistant) -> None:
"custom_components.openai_compatible.openai.AsyncOpenAI"
) as mock_client:
mock_client.return_value.platform_headers = lambda: {}
mock_client.return_value.with_options.return_value.models.list = AsyncMock()
mock_client.return_value.models.list = AsyncMock()
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
@@ -47,7 +52,7 @@ async def test_api_key_optional(hass: HomeAssistant) -> None:
"custom_components.openai_compatible.openai.AsyncOpenAI"
) as mock_client:
mock_client.return_value.platform_headers = lambda: {}
mock_client.return_value.with_options.return_value.models.list = AsyncMock()
mock_client.return_value.models.list = AsyncMock()
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
@@ -73,10 +78,56 @@ async def test_migration_backfills_missing_base_url(hass: HomeAssistant) -> None
"custom_components.openai_compatible.openai.AsyncOpenAI"
) as mock_client:
mock_client.return_value.platform_headers = lambda: {}
mock_client.return_value.with_options.return_value.models.list = AsyncMock()
mock_client.return_value.models.list = AsyncMock()
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.data[CONF_BASE_URL] == DEFAULT_BASE_URL
assert entry.minor_version == 8
assert mock_client.call_args.kwargs["base_url"] == DEFAULT_BASE_URL
@pytest.mark.parametrize(
("error", "state"),
[
(
openai.AuthenticationError(
"bad key",
response=httpx.Response(401, request=httpx.Request("GET", "http://x")),
body=None,
),
ConfigEntryState.SETUP_ERROR,
),
(
openai.APIConnectionError(request=httpx.Request("GET", "http://x")),
ConfigEntryState.SETUP_RETRY,
),
],
)
async def test_setup_checks_connection(
hass: HomeAssistant, error: Exception, state: ConfigEntryState
) -> None:
"""Setup must actually call GET /models and act on its failure.
AsyncModels.list returns an awaitable paginator; handing the bound method to
the executor returned it unawaited, so no request was made and a bad key or
unreachable endpoint still set up.
"""
entry = MockConfigEntry(
domain=DOMAIN,
data={"api_key": "sk-test", CONF_BASE_URL: "http://192.0.2.10:4000/v1"},
version=2,
minor_version=8,
)
entry.add_to_hass(hass)
with patch(
"custom_components.openai_compatible.openai.AsyncOpenAI"
) as mock_client:
mock_client.return_value.platform_headers = lambda: {}
mock_client.return_value.models.list = AsyncMock(side_effect=error)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
mock_client.return_value.models.list.assert_awaited_once_with(timeout=10.0)
assert entry.state is state