Files
adguard-control-hub/tests/test_integration.py

384 lines
15 KiB
Python

"""Test the complete AdGuard Control Hub integration."""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import UpdateFailed
from custom_components.adguard_hub import (
async_setup_entry,
async_unload_entry,
AdGuardControlHubCoordinator,
)
from custom_components.adguard_hub.api import AdGuardConnectionError, AdGuardAuthError
from custom_components.adguard_hub.const import DOMAIN
class TestIntegrationSetup:
"""Test integration setup and unload."""
@pytest.mark.asyncio
async def test_setup_entry_success(self, mock_hass, mock_config_entry, mock_api):
"""Test successful setup of config entry."""
with patch("custom_components.adguard_hub.AdGuardHomeAPI", return_value=mock_api), patch("custom_components.adguard_hub.async_get_clientsession") as mock_session:
# Mock the coordinator's first refresh
with patch("custom_components.adguard_hub.AdGuardControlHubCoordinator.async_config_entry_first_refresh", new=AsyncMock()):
result = await async_setup_entry(mock_hass, mock_config_entry)
assert result is True
assert DOMAIN in mock_hass.data
assert mock_config_entry.entry_id in mock_hass.data[DOMAIN]
assert "coordinator" in mock_hass.data[DOMAIN][mock_config_entry.entry_id]
assert "api" in mock_hass.data[DOMAIN][mock_config_entry.entry_id]
# Verify platforms setup
mock_hass.config_entries.async_forward_entry_setups.assert_called_once()
@pytest.mark.asyncio
async def test_setup_entry_connection_failure(self, mock_hass, mock_config_entry):
"""Test setup failure due to connection error."""
mock_api = MagicMock()
mock_api.test_connection = AsyncMock(return_value=False)
with patch("custom_components.adguard_hub.AdGuardHomeAPI", return_value=mock_api), patch("custom_components.adguard_hub.async_get_clientsession"):
with pytest.raises(ConfigEntryNotReady, match="Unable to connect to AdGuard Home"):
await async_setup_entry(mock_hass, mock_config_entry)
@pytest.mark.asyncio
async def test_setup_entry_api_error(self, mock_hass, mock_config_entry):
"""Test setup failure due to API error."""
mock_api = MagicMock()
mock_api.test_connection = AsyncMock(side_effect=AdGuardAuthError("Auth failed"))
with patch("custom_components.adguard_hub.AdGuardHomeAPI", return_value=mock_api), patch("custom_components.adguard_hub.async_get_clientsession"):
with pytest.raises(ConfigEntryNotReady, match="Unable to connect"):
await async_setup_entry(mock_hass, mock_config_entry)
@pytest.mark.asyncio
async def test_setup_entry_coordinator_failure(self, mock_hass, mock_config_entry, mock_api):
"""Test setup failure due to coordinator refresh error."""
with patch("custom_components.adguard_hub.AdGuardHomeAPI", return_value=mock_api), patch("custom_components.adguard_hub.async_get_clientsession"), patch.object(AdGuardControlHubCoordinator, "async_config_entry_first_refresh",
side_effect=UpdateFailed("Refresh failed")):
with pytest.raises(ConfigEntryNotReady, match="Failed to fetch initial data"):
await async_setup_entry(mock_hass, mock_config_entry)
@pytest.mark.asyncio
async def test_setup_entry_platform_failure(self, mock_hass, mock_config_entry, mock_api):
"""Test setup failure due to platform setup error."""
mock_hass.config_entries.async_forward_entry_setups = AsyncMock(
side_effect=Exception("Platform setup failed")
)
with patch("custom_components.adguard_hub.AdGuardHomeAPI", return_value=mock_api), patch("custom_components.adguard_hub.async_get_clientsession"), patch.object(AdGuardControlHubCoordinator, "async_config_entry_first_refresh",
new=AsyncMock()):
with pytest.raises(ConfigEntryNotReady, match="Failed to set up platforms"):
await async_setup_entry(mock_hass, mock_config_entry)
# Verify cleanup
assert mock_config_entry.entry_id not in mock_hass.data.get(DOMAIN, {})
@pytest.mark.asyncio
async def test_unload_entry_success(self, mock_hass, mock_config_entry):
"""Test successful unloading of config entry."""
# Set up initial data
mock_hass.data[DOMAIN] = {
mock_config_entry.entry_id: {
"coordinator": MagicMock(),
"api": MagicMock(),
}
}
result = await async_unload_entry(mock_hass, mock_config_entry)
assert result is True
assert mock_config_entry.entry_id not in mock_hass.data[DOMAIN]
mock_hass.config_entries.async_unload_platforms.assert_called_once()
@pytest.mark.asyncio
async def test_unload_entry_last_instance(self, mock_hass, mock_config_entry):
"""Test unloading last config entry unregisters services."""
# Set up services
mock_services = MagicMock()
mock_services.unregister_services = MagicMock()
mock_hass.data[f"{DOMAIN}_services"] = mock_services
mock_hass.data[DOMAIN] = {
mock_config_entry.entry_id: {
"coordinator": MagicMock(),
"api": MagicMock(),
}
}
result = await async_unload_entry(mock_hass, mock_config_entry)
assert result is True
assert f"{DOMAIN}_services" not in mock_hass.data
assert DOMAIN not in mock_hass.data
mock_services.unregister_services.assert_called_once()
class TestCoordinator:
"""Test the data update coordinator."""
def test_coordinator_initialization(self, mock_hass, mock_api):
"""Test coordinator initialization."""
coordinator = AdGuardControlHubCoordinator(mock_hass, mock_api)
assert coordinator.api == mock_api
assert coordinator.name == f"{DOMAIN}_coordinator"
@pytest.mark.asyncio
async def test_coordinator_update_success(self, mock_hass, mock_api):
"""Test successful coordinator data update."""
coordinator = AdGuardControlHubCoordinator(mock_hass, mock_api)
data = await coordinator._async_update_data()
assert "clients" in data
assert "statistics" in data
assert "status" in data
assert "test_client" in data["clients"]
assert data["statistics"]["num_dns_queries"] == 10000
assert data["status"]["protection_enabled"] is True
@pytest.mark.asyncio
async def test_coordinator_update_partial_failure(self, mock_hass, mock_api):
"""Test coordinator update with partial API failures."""
# Make one API call fail
mock_api.get_clients = AsyncMock(side_effect=Exception("Client fetch failed"))
coordinator = AdGuardControlHubCoordinator(mock_hass, mock_api)
data = await coordinator._async_update_data()
# Should still return data from successful calls
assert "clients" in data
assert "statistics" in data
assert "status" in data
assert data["statistics"]["num_dns_queries"] == 10000
@pytest.mark.asyncio
async def test_coordinator_update_connection_error(self, mock_hass, mock_api):
"""Test coordinator update with connection error."""
mock_api.get_status = AsyncMock(side_effect=AdGuardConnectionError("Connection failed"))
mock_api.get_clients = AsyncMock(side_effect=AdGuardConnectionError("Connection failed"))
mock_api.get_statistics = AsyncMock(side_effect=AdGuardConnectionError("Connection failed"))
coordinator = AdGuardControlHubCoordinator(mock_hass, mock_api)
with pytest.raises(UpdateFailed, match="Connection error to AdGuard Home"):
await coordinator._async_update_data()
@pytest.mark.asyncio
async def test_coordinator_update_unexpected_error(self, mock_hass, mock_api):
"""Test coordinator update with unexpected error."""
mock_api.get_status = AsyncMock(side_effect=Exception("Unexpected error"))
mock_api.get_clients = AsyncMock(side_effect=Exception("Unexpected error"))
mock_api.get_statistics = AsyncMock(side_effect=Exception("Unexpected error"))
coordinator = AdGuardControlHubCoordinator(mock_hass, mock_api)
with pytest.raises(UpdateFailed, match="Error communicating with AdGuard Control Hub"):
await coordinator._async_update_data()
def test_coordinator_properties(self, mock_hass, mock_api):
"""Test coordinator properties."""
coordinator = AdGuardControlHubCoordinator(mock_hass, mock_api)
# Set test data
test_clients = {"client1": {"name": "client1"}}
test_stats = {"num_dns_queries": 5000}
test_status = {"protection_enabled": False}
coordinator._clients = test_clients
coordinator._statistics = test_stats
coordinator._protection_status = test_status
assert coordinator.clients == test_clients
assert coordinator.statistics == test_stats
assert coordinator.protection_status == test_status
def test_coordinator_properties_empty_data(self, mock_hass, mock_api):
"""Test coordinator properties with empty data."""
coordinator = AdGuardControlHubCoordinator(mock_hass, mock_api)
# Properties should return empty containers, not None
assert coordinator.clients == {}
assert coordinator.statistics == {}
assert coordinator.protection_status == {}
class TestServices:
"""Test service functionality."""
def test_services_registration(self, mock_hass):
"""Test that services are properly registered."""
from custom_components.adguard_hub.services import AdGuardControlHubServices
services = AdGuardControlHubServices(mock_hass)
services.register_services()
# Verify services registration was called
assert mock_hass.services.register.called
# Verify correct number of service registrations
expected_call_count = 6 # block_services, unblock_services, emergency_unblock, add_client, remove_client, refresh_data
assert mock_hass.services.register.call_count == expected_call_count
def test_services_unregistration(self, mock_hass):
"""Test that services are properly unregistered."""
from custom_components.adguard_hub.services import AdGuardControlHubServices
# Mock service existence
mock_hass.services.has_service.return_value = True
services = AdGuardControlHubServices(mock_hass)
services.unregister_services()
# Verify correct number of service removals
expected_call_count = 6
assert mock_hass.services.remove.call_count == expected_call_count
@pytest.mark.asyncio
async def test_block_services_success(self, mock_hass, mock_api):
"""Test successful service blocking."""
from custom_components.adguard_hub.services import AdGuardControlHubServices
mock_hass.data[DOMAIN] = {
"entry_id": {"api": mock_api}
}
services = AdGuardControlHubServices(mock_hass)
call = MagicMock()
call.data = {
"client_name": "test_client",
"services": ["youtube", "netflix"]
}
await services.block_services(call)
mock_api.get_client_by_name.assert_called_once_with("test_client")
mock_api.update_client_blocked_services.assert_called_once()
@pytest.mark.asyncio
async def test_unblock_services_success(self, mock_hass, mock_api):
"""Test successful service unblocking."""
from custom_components.adguard_hub.services import AdGuardControlHubServices
mock_hass.data[DOMAIN] = {
"entry_id": {"api": mock_api}
}
services = AdGuardControlHubServices(mock_hass)
call = MagicMock()
call.data = {
"client_name": "test_client",
"services": ["youtube"]
}
await services.unblock_services(call)
mock_api.get_client_by_name.assert_called_once_with("test_client")
mock_api.update_client_blocked_services.assert_called_once()
@pytest.mark.asyncio
async def test_emergency_unblock_global(self, mock_hass, mock_api):
"""Test emergency unblock for all clients."""
from custom_components.adguard_hub.services import AdGuardControlHubServices
mock_hass.data[DOMAIN] = {
"entry_id": {"api": mock_api}
}
services = AdGuardControlHubServices(mock_hass)
call = MagicMock()
call.data = {
"duration": 300,
"clients": ["all"]
}
await services.emergency_unblock(call)
mock_api.set_protection.assert_called_once_with(False)
@pytest.mark.asyncio
async def test_refresh_data_success(self, mock_hass, mock_coordinator):
"""Test successful data refresh."""
from custom_components.adguard_hub.services import AdGuardControlHubServices
mock_hass.data[DOMAIN] = {
"entry_id": {"coordinator": mock_coordinator}
}
services = AdGuardControlHubServices(mock_hass)
call = MagicMock()
call.data = {}
await services.refresh_data(call)
mock_coordinator.async_request_refresh.assert_called_once()
class TestConstants:
"""Test constant definitions."""
def test_blocked_services_constants(self):
"""Test that blocked services are properly defined."""
from custom_components.adguard_hub.const import BLOCKED_SERVICES
required_services = ["youtube", "netflix", "gaming", "facebook"]
for service in required_services:
assert service in BLOCKED_SERVICES
assert isinstance(BLOCKED_SERVICES[service], str)
assert len(BLOCKED_SERVICES[service]) > 0
def test_api_endpoints_constants(self):
"""Test that API endpoints are properly defined."""
from custom_components.adguard_hub.const import API_ENDPOINTS
required_endpoints = [
"status", "clients", "stats", "protection",
"clients_add", "clients_update", "clients_delete"
]
for endpoint in required_endpoints:
assert endpoint in API_ENDPOINTS
assert API_ENDPOINTS[endpoint].startswith("/")
def test_platform_constants(self):
"""Test platform constants."""
from custom_components.adguard_hub.const import PLATFORMS
expected_platforms = ["switch", "binary_sensor", "sensor"]
assert PLATFORMS == expected_platforms
def test_service_constants(self):
"""Test service name constants."""
from custom_components.adguard_hub.const import (
SERVICE_BLOCK_SERVICES,
SERVICE_UNBLOCK_SERVICES,
SERVICE_EMERGENCY_UNBLOCK,
SERVICE_ADD_CLIENT,
SERVICE_REMOVE_CLIENT,
SERVICE_REFRESH_DATA,
)
services = [
SERVICE_BLOCK_SERVICES,
SERVICE_UNBLOCK_SERVICES,
SERVICE_EMERGENCY_UNBLOCK,
SERVICE_ADD_CLIENT,
SERVICE_REMOVE_CLIENT,
SERVICE_REFRESH_DATA,
]
for service in services:
assert isinstance(service, str)
assert len(service) > 0
assert "_" in service # Snake case format