384 lines
15 KiB
Python
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
|