"""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