mirror of
https://github.com/home-assistant/core.git
synced 2026-10-07 23:09:19 -04:00
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
537 lines
17 KiB
Python
537 lines
17 KiB
Python
"""Test the Splunk integration init."""
|
|
|
|
from http import HTTPStatus
|
|
import logging
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from aiohttp import ClientConnectionError, ClientResponseError
|
|
from hass_splunk import SplunkPayloadError
|
|
import pytest
|
|
|
|
from homeassistant.components.splunk.const import CONF_FILTER, DOMAIN
|
|
from homeassistant.config_entries import ConfigEntryState
|
|
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, CONF_TOKEN
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers.typing import ConfigType
|
|
from homeassistant.setup import async_setup_component
|
|
|
|
from tests.common import MockConfigEntry
|
|
|
|
YAML_FILTER = {"include_domains": ["sensor"]}
|
|
|
|
|
|
async def test_setup_entry_success(
|
|
hass: HomeAssistant, mock_hass_splunk: AsyncMock, mock_config_entry: MockConfigEntry
|
|
) -> None:
|
|
"""Test successful setup from config entry."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
# Verify client was created and checked
|
|
assert mock_hass_splunk.check.call_count == 2
|
|
# First call checks connectivity
|
|
mock_hass_splunk.check.assert_any_call(connectivity=True, token=False, busy=False)
|
|
# Second call checks token
|
|
mock_hass_splunk.check.assert_any_call(connectivity=False, token=True, busy=False)
|
|
|
|
# Verify startup event was queued
|
|
assert mock_hass_splunk.queue.call_count == 1
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("side_effect", "expected_state", "expected_error_key"),
|
|
[
|
|
([False, False], ConfigEntryState.SETUP_RETRY, "cannot_connect"),
|
|
(
|
|
ClientConnectionError("Connection failed"),
|
|
ConfigEntryState.SETUP_RETRY,
|
|
"cannot_connect",
|
|
),
|
|
(TimeoutError(), ConfigEntryState.SETUP_RETRY, "timeout_connect"),
|
|
(
|
|
Exception("Unexpected error"),
|
|
ConfigEntryState.SETUP_RETRY,
|
|
"unexpected_connect_error",
|
|
),
|
|
([True, False], ConfigEntryState.SETUP_ERROR, "invalid_auth"),
|
|
],
|
|
)
|
|
async def test_setup_entry_error(
|
|
hass: HomeAssistant,
|
|
mock_hass_splunk: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
side_effect: Exception | list[bool],
|
|
expected_state: ConfigEntryState,
|
|
expected_error_key: str,
|
|
) -> None:
|
|
"""Test setup with various errors results in appropriate states."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
mock_hass_splunk.check.side_effect = side_effect
|
|
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert mock_config_entry.state is expected_state
|
|
assert mock_config_entry.error_reason_translation_key == expected_error_key
|
|
|
|
|
|
async def test_unload_entry(
|
|
hass: HomeAssistant, mock_hass_splunk: AsyncMock, mock_config_entry: MockConfigEntry
|
|
) -> None:
|
|
"""Test unloading a config entry."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
|
|
|
|
|
async def test_setup_without_yaml(
|
|
hass: HomeAssistant, mock_hass_splunk: AsyncMock
|
|
) -> None:
|
|
"""Test setup without YAML succeeds."""
|
|
assert await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"yaml_config",
|
|
[
|
|
pytest.param({CONF_FILTER: YAML_FILTER}, id="filter_only"),
|
|
pytest.param(
|
|
{
|
|
CONF_TOKEN: "yaml-token",
|
|
CONF_HOST: "yaml-host",
|
|
CONF_PORT: 8089,
|
|
CONF_SSL: False,
|
|
CONF_FILTER: YAML_FILTER,
|
|
},
|
|
id="with_removed_connection_settings",
|
|
),
|
|
],
|
|
)
|
|
async def test_event_listener_with_filter(
|
|
hass: HomeAssistant,
|
|
mock_hass_splunk: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
yaml_config: ConfigType,
|
|
) -> None:
|
|
"""Test event listener respects entity filter from YAML."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: yaml_config})
|
|
await hass.async_block_till_done()
|
|
|
|
assert hass.config_entries.async_entries(DOMAIN) == [mock_config_entry]
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
# Reset queue call count after startup event
|
|
mock_hass_splunk.queue.reset_mock()
|
|
|
|
hass.states.async_set("sensor.test", "123")
|
|
await hass.async_block_till_done()
|
|
|
|
assert mock_hass_splunk.queue.call_count == 1
|
|
|
|
mock_hass_splunk.queue.reset_mock()
|
|
|
|
hass.states.async_set("light.test", "on")
|
|
await hass.async_block_till_done()
|
|
|
|
assert mock_hass_splunk.queue.call_count == 0
|
|
|
|
|
|
async def test_yaml_connection_settings_not_imported(
|
|
hass: HomeAssistant,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test YAML connection settings are ignored and no config entry is created."""
|
|
assert await async_setup_component(
|
|
hass,
|
|
DOMAIN,
|
|
{
|
|
DOMAIN: {
|
|
CONF_TOKEN: "yaml-token",
|
|
CONF_HOST: "yaml-host",
|
|
CONF_PORT: 8089,
|
|
CONF_SSL: False,
|
|
}
|
|
},
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
assert hass.config_entries.async_entries(DOMAIN) == []
|
|
assert (
|
|
"The 'token' option has been removed, please remove it from your configuration"
|
|
in caplog.text
|
|
)
|
|
|
|
|
|
async def test_event_listener_unauthorized(
|
|
hass: HomeAssistant, mock_hass_splunk: AsyncMock, mock_config_entry: MockConfigEntry
|
|
) -> None:
|
|
"""Test event listener triggers reauth on unauthorized error."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
# Create a real state first
|
|
hass.states.async_set("sensor.test", "123")
|
|
await hass.async_block_till_done()
|
|
|
|
# Simulate unauthorized error when sending event
|
|
mock_hass_splunk.queue.side_effect = SplunkPayloadError(
|
|
0, "Unauthorized", HTTPStatus.UNAUTHORIZED
|
|
)
|
|
|
|
# Change the state to trigger an event
|
|
hass.states.async_set("sensor.test", "456")
|
|
await hass.async_block_till_done()
|
|
|
|
# Verify reauth flow was started
|
|
flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
|
assert len(flows) == 1
|
|
assert flows[0]["context"]["source"] == "reauth"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("error", "expected_log_level", "expected_message"),
|
|
[
|
|
(
|
|
ClientConnectionError("Connection failed"),
|
|
logging.DEBUG,
|
|
"Connection error sending to Splunk",
|
|
),
|
|
(
|
|
TimeoutError(),
|
|
logging.DEBUG,
|
|
"Timeout sending to Splunk",
|
|
),
|
|
(
|
|
ClientResponseError(
|
|
request_info=MagicMock(),
|
|
history=(),
|
|
status=500,
|
|
message="Internal Server Error",
|
|
),
|
|
logging.WARNING,
|
|
"Splunk response error: Internal Server Error",
|
|
),
|
|
(
|
|
SplunkPayloadError(0, "Bad request", HTTPStatus.BAD_REQUEST),
|
|
logging.WARNING,
|
|
"Splunk payload error: Bad request",
|
|
),
|
|
],
|
|
)
|
|
async def test_event_listener_error_handling(
|
|
hass: HomeAssistant,
|
|
mock_hass_splunk: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
caplog: pytest.LogCaptureFixture,
|
|
error: Exception,
|
|
expected_log_level: int,
|
|
expected_message: str,
|
|
) -> None:
|
|
"""Test event listener handles various errors gracefully."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
# Create a real state first
|
|
hass.states.async_set("sensor.test", "123")
|
|
await hass.async_block_till_done()
|
|
|
|
# Simulate error when sending event
|
|
mock_hass_splunk.queue.side_effect = error
|
|
|
|
# Change the state to trigger an event - should not raise
|
|
with caplog.at_level(logging.DEBUG):
|
|
hass.states.async_set("sensor.test", "456")
|
|
await hass.async_block_till_done()
|
|
|
|
assert any(
|
|
record.levelno == expected_log_level and expected_message in record.message
|
|
for record in caplog.records
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("error", "expected_log_level", "expected_message", "expected_traceback"),
|
|
[
|
|
(
|
|
ClientResponseError(
|
|
request_info=MagicMock(),
|
|
history=(),
|
|
status=500,
|
|
message="Internal Server Error",
|
|
),
|
|
logging.WARNING,
|
|
"Splunk response error: Internal Server Error",
|
|
False,
|
|
),
|
|
(
|
|
SplunkPayloadError(0, "Unauthorized", HTTPStatus.UNAUTHORIZED),
|
|
logging.ERROR,
|
|
"Splunk token unauthorized",
|
|
False,
|
|
),
|
|
(
|
|
ValueError("boom"),
|
|
logging.ERROR,
|
|
"Unexpected error sending event to Splunk",
|
|
True,
|
|
),
|
|
],
|
|
)
|
|
async def test_event_listener_repeated_failures_log_at_debug(
|
|
hass: HomeAssistant,
|
|
mock_hass_splunk: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
caplog: pytest.LogCaptureFixture,
|
|
error: Exception,
|
|
expected_log_level: int,
|
|
expected_message: str,
|
|
expected_traceback: bool,
|
|
) -> None:
|
|
"""Test the first failure logs at its level and the repeats log at debug."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
mock_hass_splunk.queue.side_effect = error
|
|
|
|
with caplog.at_level(logging.DEBUG):
|
|
for i in range(5):
|
|
hass.states.async_set("sensor.test", str(i))
|
|
await hass.async_block_till_done()
|
|
|
|
matching_records = [
|
|
record for record in caplog.records if expected_message in record.message
|
|
]
|
|
assert len(matching_records) == 5
|
|
assert matching_records[0].levelno == expected_log_level
|
|
# A traceback is worth one record per outage, never one per state change.
|
|
assert bool(matching_records[0].exc_info) is expected_traceback
|
|
assert all(record.levelno == logging.DEBUG for record in matching_records[1:])
|
|
assert not any(record.exc_info for record in matching_records[1:])
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("first_error", "first_message", "first_level", "second_error", "second_message"),
|
|
[
|
|
pytest.param(
|
|
ClientConnectionError("Connection failed"),
|
|
"Connection error sending to Splunk",
|
|
logging.DEBUG,
|
|
SplunkPayloadError(0, "Unauthorized", HTTPStatus.UNAUTHORIZED),
|
|
"Splunk token unauthorized",
|
|
id="more_severe_second_failure",
|
|
),
|
|
pytest.param(
|
|
SplunkPayloadError(0, "Unauthorized", HTTPStatus.UNAUTHORIZED),
|
|
"Splunk token unauthorized",
|
|
logging.ERROR,
|
|
ClientResponseError(
|
|
request_info=MagicMock(),
|
|
history=(),
|
|
status=500,
|
|
message="Internal Server Error",
|
|
),
|
|
"Splunk response error: Internal Server Error",
|
|
id="less_severe_second_failure",
|
|
),
|
|
],
|
|
)
|
|
async def test_event_listener_different_failure_during_outage_logs_at_debug(
|
|
hass: HomeAssistant,
|
|
mock_hass_splunk: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
caplog: pytest.LogCaptureFixture,
|
|
first_error: Exception,
|
|
first_message: str,
|
|
first_level: int,
|
|
second_error: Exception,
|
|
second_message: str,
|
|
) -> None:
|
|
"""Test a different failure during an outage logs at debug, whatever its level."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
with caplog.at_level(logging.DEBUG):
|
|
mock_hass_splunk.queue.side_effect = first_error
|
|
hass.states.async_set("sensor.test", "outage")
|
|
await hass.async_block_till_done()
|
|
|
|
mock_hass_splunk.queue.side_effect = second_error
|
|
for i in range(2):
|
|
hass.states.async_set("sensor.test", f"different-{i}")
|
|
await hass.async_block_till_done()
|
|
|
|
first_records = [
|
|
record for record in caplog.records if first_message in record.message
|
|
]
|
|
assert len(first_records) == 1
|
|
assert first_records[0].levelno == first_level
|
|
|
|
second_records = [
|
|
record for record in caplog.records if second_message in record.message
|
|
]
|
|
assert len(second_records) == 2
|
|
assert all(record.levelno == logging.DEBUG for record in second_records)
|
|
|
|
|
|
async def test_event_listener_recovery_logs_once_at_info(
|
|
hass: HomeAssistant,
|
|
mock_hass_splunk: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test a recovered outage logs a single info record."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
mock_hass_splunk.queue.side_effect = ClientConnectionError("Connection failed")
|
|
|
|
with caplog.at_level(logging.DEBUG):
|
|
for i in range(2):
|
|
hass.states.async_set("sensor.test", str(i))
|
|
await hass.async_block_till_done()
|
|
|
|
mock_hass_splunk.queue.side_effect = None
|
|
for i in range(2):
|
|
hass.states.async_set("sensor.test", f"recovered-{i}")
|
|
await hass.async_block_till_done()
|
|
|
|
recovery_records = [
|
|
record
|
|
for record in caplog.records
|
|
if "Sending events to Splunk has recovered" in record.message
|
|
]
|
|
assert len(recovery_records) == 1
|
|
assert recovery_records[0].levelno == logging.INFO
|
|
|
|
|
|
async def test_event_listener_failure_after_recovery_logs_at_natural_level(
|
|
hass: HomeAssistant,
|
|
mock_hass_splunk: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test a new outage after a recovery logs at its natural level again."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
mock_hass_splunk.queue.side_effect = ClientConnectionError("Connection failed")
|
|
|
|
with caplog.at_level(logging.DEBUG):
|
|
hass.states.async_set("sensor.test", "first-outage")
|
|
await hass.async_block_till_done()
|
|
|
|
mock_hass_splunk.queue.side_effect = None
|
|
hass.states.async_set("sensor.test", "recovered")
|
|
await hass.async_block_till_done()
|
|
|
|
caplog.clear()
|
|
mock_hass_splunk.queue.side_effect = ClientResponseError(
|
|
request_info=MagicMock(),
|
|
history=(),
|
|
status=500,
|
|
message="Internal Server Error",
|
|
)
|
|
hass.states.async_set("sensor.test", "second-outage")
|
|
await hass.async_block_till_done()
|
|
|
|
assert any(
|
|
record.levelno == logging.WARNING
|
|
and "Splunk response error: Internal Server Error" in record.message
|
|
for record in caplog.records
|
|
)
|
|
|
|
|
|
async def test_event_listener_coalesced_send_is_not_a_recovery(
|
|
hass: HomeAssistant,
|
|
mock_hass_splunk: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test a send coalesced into an in-flight send isn't treated as success.
|
|
|
|
hass_splunk.queue() returns False, without raising, when a send is already
|
|
in flight, which says nothing about whether this event reached Splunk.
|
|
"""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
error = ClientResponseError(
|
|
request_info=MagicMock(),
|
|
history=(),
|
|
status=500,
|
|
message="Internal Server Error",
|
|
)
|
|
mock_hass_splunk.queue.side_effect = [error, False, error]
|
|
|
|
with caplog.at_level(logging.DEBUG):
|
|
for i in range(3):
|
|
hass.states.async_set("sensor.test", str(i))
|
|
await hass.async_block_till_done()
|
|
|
|
assert not any(
|
|
"Sending events to Splunk has recovered" in record.message
|
|
for record in caplog.records
|
|
)
|
|
|
|
# Still demoted, so the coalesced send did not clear the outage either.
|
|
failure_records = [
|
|
record
|
|
for record in caplog.records
|
|
if "Splunk response error: Internal Server Error" in record.message
|
|
]
|
|
assert len(failure_records) == 2
|
|
assert failure_records[0].levelno == logging.WARNING
|
|
assert failure_records[1].levelno == logging.DEBUG
|
|
|
|
|
|
async def test_event_listener_no_recovery_message_without_prior_failure(
|
|
hass: HomeAssistant,
|
|
mock_hass_splunk: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test that a successful send without prior failures logs no recovery message."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
with caplog.at_level(logging.DEBUG):
|
|
hass.states.async_set("sensor.test", "123")
|
|
await hass.async_block_till_done()
|
|
|
|
assert not any(
|
|
"Sending events to Splunk has recovered" in record.message
|
|
for record in caplog.records
|
|
)
|