fix: Fix CI/CD issues and enhance integration
Some checks failed
Integration Testing / Integration Tests (2024.12.0, 3.11) (push) Failing after 22s
Integration Testing / Integration Tests (2024.12.0, 3.12) (push) Failing after 21s
Integration Testing / Integration Tests (2024.12.0, 3.13) (push) Failing after 1m32s
Integration Testing / Integration Tests (2025.9.4, 3.11) (push) Failing after 15s
Integration Testing / Integration Tests (2025.9.4, 3.12) (push) Failing after 20s
Integration Testing / Integration Tests (2025.9.4, 3.13) (push) Failing after 20s
Some checks failed
Integration Testing / Integration Tests (2024.12.0, 3.11) (push) Failing after 22s
Integration Testing / Integration Tests (2024.12.0, 3.12) (push) Failing after 21s
Integration Testing / Integration Tests (2024.12.0, 3.13) (push) Failing after 1m32s
Integration Testing / Integration Tests (2025.9.4, 3.11) (push) Failing after 15s
Integration Testing / Integration Tests (2025.9.4, 3.12) (push) Failing after 20s
Integration Testing / Integration Tests (2025.9.4, 3.13) (push) Failing after 20s
Signed-off-by: Rafal Zielinski <sq4ind@gmail.com>
This commit is contained in:
@@ -1,9 +1,16 @@
|
||||
"""Test the complete AdGuard Control Hub integration."""
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@@ -13,28 +20,72 @@ class TestIntegrationSetup:
|
||||
@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"):
|
||||
with patch("custom_components.adguard_hub.AdGuardHomeAPI", return_value=mock_api), patch("custom_components.adguard_hub.async_get_clientsession") as mock_session:
|
||||
|
||||
result = await async_setup_entry(mock_hass, mock_config_entry)
|
||||
# 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 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 = pytest.AsyncMock(return_value=False)
|
||||
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):
|
||||
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(),
|
||||
@@ -46,3 +97,287 @@ class TestIntegrationSetup:
|
||||
|
||||
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
|
||||
|
Reference in New Issue
Block a user