Files
core/tests/components/growatt_server/test_init.py

259 lines
8.1 KiB
Python

"""Tests for the Growatt Server integration."""
from datetime import timedelta
import json
from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory
import growattServer
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.growatt_server.const import (
AUTH_API_TOKEN,
AUTH_PASSWORD,
CONF_AUTH_TYPE,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_URL, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from tests.common import MockConfigEntry, async_fire_time_changed
@pytest.mark.usefixtures("init_integration")
async def test_load_unload_config_entry(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test loading and unloading the integration."""
assert mock_config_entry.state is ConfigEntryState.LOADED
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
@pytest.mark.usefixtures("init_integration")
async def test_device_info(
snapshot: SnapshotAssertion,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test device registry integration."""
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "MIN123456")})
assert device_entry is not None
assert device_entry == snapshot
@pytest.mark.parametrize(
("exception", "expected_state"),
[
(growattServer.GrowattV1ApiError("API Error"), ConfigEntryState.SETUP_ERROR),
(
json.decoder.JSONDecodeError("Invalid JSON", "", 0),
ConfigEntryState.SETUP_ERROR,
),
],
)
async def test_setup_error_on_api_failure(
hass: HomeAssistant,
mock_growatt_v1_api,
mock_config_entry: MockConfigEntry,
exception: Exception,
expected_state: ConfigEntryState,
) -> None:
"""Test setup error on API failures during device list."""
mock_growatt_v1_api.device_list.side_effect = exception
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is expected_state
@pytest.mark.usefixtures("init_integration")
async def test_coordinator_update_failed(
hass: HomeAssistant,
mock_growatt_v1_api,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test coordinator handles update failures gracefully."""
# Integration should be loaded
assert mock_config_entry.state is ConfigEntryState.LOADED
# Cause coordinator update to fail
mock_growatt_v1_api.min_detail.side_effect = growattServer.GrowattV1ApiError(
"Connection timeout"
)
# Trigger coordinator refresh
freezer.tick(timedelta(minutes=5))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# Integration should remain loaded despite coordinator error
assert mock_config_entry.state is ConfigEntryState.LOADED
async def test_classic_api_setup(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_growatt_classic_api,
mock_config_entry_classic: MockConfigEntry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test integration setup with Classic API (password auth)."""
# Classic API doesn't support MIN devices - use TLX device instead
mock_growatt_classic_api.device_list.return_value = [
{"deviceSn": "TLX123456", "deviceType": "tlx"}
]
await setup_integration(hass, mock_config_entry_classic)
assert mock_config_entry_classic.state is ConfigEntryState.LOADED
# Verify Classic API login was called
mock_growatt_classic_api.login.assert_called()
# Verify device was created
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "TLX123456")})
assert device_entry is not None
assert device_entry == snapshot
@pytest.mark.usefixtures("init_integration")
async def test_unload_removes_listeners(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that unloading removes all listeners."""
# Get initial listener count
initial_listeners = len(hass.bus.async_listeners())
# Unload the integration
await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
# Verify listeners were removed (should be same or less)
final_listeners = len(hass.bus.async_listeners())
assert final_listeners <= initial_listeners
async def test_multiple_devices_discovered(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_growatt_v1_api,
mock_config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test handling multiple devices from device_list."""
# Reset and add multiple devices
mock_config_entry_new = MockConfigEntry(
domain=DOMAIN,
data=mock_config_entry.data,
unique_id="plant_456",
)
mock_growatt_v1_api.device_list.return_value = {
"devices": [
{"device_sn": "MIN123456", "type": 7},
{"device_sn": "MIN789012", "type": 7},
]
}
with patch(
"homeassistant.components.growatt_server.coordinator.SCAN_INTERVAL",
timedelta(minutes=5),
):
await setup_integration(hass, mock_config_entry_new)
# Verify both devices were created
device1 = device_registry.async_get_device(identifiers={(DOMAIN, "MIN123456")})
device2 = device_registry.async_get_device(identifiers={(DOMAIN, "MIN789012")})
assert device1 is not None
assert device1 == snapshot(name="device_min123456")
assert device2 is not None
assert device2 == snapshot(name="device_min789012")
async def test_migrate_legacy_api_token_config(
hass: HomeAssistant,
mock_growatt_v1_api,
) -> None:
"""Test migration of legacy config entry with API token but no auth_type."""
# Create a legacy config entry without CONF_AUTH_TYPE
legacy_config = {
CONF_TOKEN: "test_token_123",
CONF_URL: "https://openapi.growatt.com/",
"plant_id": "plant_123",
}
mock_config_entry = MockConfigEntry(
domain=DOMAIN,
data=legacy_config,
unique_id="plant_123",
)
await setup_integration(hass, mock_config_entry)
# Verify migration occurred and auth_type was added
assert mock_config_entry.data[CONF_AUTH_TYPE] == AUTH_API_TOKEN
assert mock_config_entry.state is ConfigEntryState.LOADED
async def test_migrate_legacy_password_config(
hass: HomeAssistant,
mock_growatt_classic_api,
) -> None:
"""Test migration of legacy config entry with password auth but no auth_type."""
# Create a legacy config entry without CONF_AUTH_TYPE
legacy_config = {
CONF_USERNAME: "test_user",
CONF_PASSWORD: "test_password",
CONF_URL: "https://server.growatt.com/",
"plant_id": "plant_456",
}
mock_config_entry = MockConfigEntry(
domain=DOMAIN,
data=legacy_config,
unique_id="plant_456",
)
# Classic API doesn't support MIN devices - use TLX device instead
mock_growatt_classic_api.device_list.return_value = [
{"deviceSn": "TLX123456", "deviceType": "tlx"}
]
await setup_integration(hass, mock_config_entry)
# Verify migration occurred and auth_type was added
assert mock_config_entry.data[CONF_AUTH_TYPE] == AUTH_PASSWORD
assert mock_config_entry.state is ConfigEntryState.LOADED
async def test_migrate_legacy_config_no_auth_fields(
hass: HomeAssistant,
) -> None:
"""Test that config entry with no recognizable auth fields raises error."""
# Create a config entry without any auth fields
invalid_config = {
CONF_URL: "https://openapi.growatt.com/",
"plant_id": "plant_789",
}
mock_config_entry = MockConfigEntry(
domain=DOMAIN,
data=invalid_config,
unique_id="plant_789",
)
await setup_integration(hass, mock_config_entry)
# The ConfigEntryError is caught by the config entry system
# and the entry state is set to SETUP_ERROR
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR