746 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			746 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Python
		
	
	
"""Tests for the LaMetric config flow."""
 | 
						|
from collections.abc import Awaitable, Callable
 | 
						|
from http import HTTPStatus
 | 
						|
from unittest.mock import MagicMock
 | 
						|
 | 
						|
from aiohttp.test_utils import TestClient
 | 
						|
from demetriek import (
 | 
						|
    LaMetricConnectionError,
 | 
						|
    LaMetricConnectionTimeoutError,
 | 
						|
    LaMetricError,
 | 
						|
)
 | 
						|
import pytest
 | 
						|
 | 
						|
from homeassistant.components.dhcp import DhcpServiceInfo
 | 
						|
from homeassistant.components.lametric.const import DOMAIN
 | 
						|
from homeassistant.components.ssdp import (
 | 
						|
    ATTR_UPNP_FRIENDLY_NAME,
 | 
						|
    ATTR_UPNP_SERIAL,
 | 
						|
    SsdpServiceInfo,
 | 
						|
)
 | 
						|
from homeassistant.config_entries import SOURCE_DHCP, SOURCE_SSDP, SOURCE_USER
 | 
						|
from homeassistant.const import CONF_API_KEY, CONF_DEVICE, CONF_HOST, CONF_MAC
 | 
						|
from homeassistant.core import HomeAssistant
 | 
						|
from homeassistant.data_entry_flow import FlowResultType
 | 
						|
from homeassistant.helpers import config_entry_oauth2_flow
 | 
						|
 | 
						|
from tests.common import MockConfigEntry
 | 
						|
from tests.test_util.aiohttp import AiohttpClientMocker
 | 
						|
 | 
						|
SSDP_DISCOVERY_INFO = SsdpServiceInfo(
 | 
						|
    ssdp_usn="mock_usn",
 | 
						|
    ssdp_st="mock_st",
 | 
						|
    ssdp_location="http://127.0.0.1:44057/465d585b-1c05-444a-b14e-6ffb875b46a6/device_description.xml",
 | 
						|
    upnp={
 | 
						|
        ATTR_UPNP_FRIENDLY_NAME: "LaMetric Time (LM1245)",
 | 
						|
        ATTR_UPNP_SERIAL: "SA110405124500W00BS9",
 | 
						|
    },
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
async def test_full_cloud_import_flow_multiple_devices(
 | 
						|
    hass: HomeAssistant,
 | 
						|
    hass_client_no_auth: Callable[[], Awaitable[TestClient]],
 | 
						|
    aioclient_mock: AiohttpClientMocker,
 | 
						|
    current_request_with_host: None,
 | 
						|
    mock_setup_entry: MagicMock,
 | 
						|
    mock_lametric_cloud_config_flow: MagicMock,
 | 
						|
    mock_lametric_config_flow: MagicMock,
 | 
						|
) -> None:
 | 
						|
    """Check a full flow importing from cloud, with multiple devices."""
 | 
						|
    result = await hass.config_entries.flow.async_init(
 | 
						|
        DOMAIN, context={"source": SOURCE_USER}
 | 
						|
    )
 | 
						|
 | 
						|
    assert result.get("type") == FlowResultType.MENU
 | 
						|
    assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud"
 | 
						|
    assert result.get("menu_options") == ["pick_implementation", "manual_entry"]
 | 
						|
    assert "flow_id" in result
 | 
						|
    flow_id = result["flow_id"]
 | 
						|
 | 
						|
    result2 = await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={"next_step_id": "pick_implementation"}
 | 
						|
    )
 | 
						|
 | 
						|
    # pylint: disable=protected-access
 | 
						|
    state = config_entry_oauth2_flow._encode_jwt(
 | 
						|
        hass,
 | 
						|
        {
 | 
						|
            "flow_id": flow_id,
 | 
						|
            "redirect_uri": "https://example.com/auth/external/callback",
 | 
						|
        },
 | 
						|
    )
 | 
						|
 | 
						|
    assert result2.get("type") == FlowResultType.EXTERNAL_STEP
 | 
						|
    assert result2.get("url") == (
 | 
						|
        "https://developer.lametric.com/api/v2/oauth2/authorize"
 | 
						|
        "?response_type=code&client_id=client"
 | 
						|
        "&redirect_uri=https://example.com/auth/external/callback"
 | 
						|
        f"&state={state}"
 | 
						|
        "&scope=basic+devices_read"
 | 
						|
    )
 | 
						|
 | 
						|
    client = await hass_client_no_auth()
 | 
						|
    resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
 | 
						|
    assert resp.status == HTTPStatus.OK
 | 
						|
    assert resp.headers["content-type"] == "text/html; charset=utf-8"
 | 
						|
 | 
						|
    aioclient_mock.post(
 | 
						|
        "https://developer.lametric.com/api/v2/oauth2/token",
 | 
						|
        json={
 | 
						|
            "refresh_token": "mock-refresh-token",
 | 
						|
            "access_token": "mock-access-token",
 | 
						|
            "type": "Bearer",
 | 
						|
            "expires_in": 60,
 | 
						|
        },
 | 
						|
    )
 | 
						|
 | 
						|
    result3 = await hass.config_entries.flow.async_configure(flow_id)
 | 
						|
 | 
						|
    assert result3.get("type") == FlowResultType.FORM
 | 
						|
    assert result3.get("step_id") == "cloud_select_device"
 | 
						|
 | 
						|
    result4 = await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"}
 | 
						|
    )
 | 
						|
 | 
						|
    assert result4.get("type") == FlowResultType.CREATE_ENTRY
 | 
						|
    assert result4.get("title") == "Frenck's LaMetric"
 | 
						|
    assert result4.get("data") == {
 | 
						|
        CONF_HOST: "127.0.0.1",
 | 
						|
        CONF_API_KEY: "mock-api-key",
 | 
						|
        CONF_MAC: "AA:BB:CC:DD:EE:FF",
 | 
						|
    }
 | 
						|
    assert "result" in result4
 | 
						|
    assert result4["result"].unique_id == "SA110405124500W00BS9"
 | 
						|
 | 
						|
    assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1
 | 
						|
    assert len(mock_lametric_config_flow.device.mock_calls) == 1
 | 
						|
    assert len(mock_lametric_config_flow.notify.mock_calls) == 1
 | 
						|
    assert len(mock_setup_entry.mock_calls) == 1
 | 
						|
 | 
						|
 | 
						|
async def test_full_cloud_import_flow_single_device(
 | 
						|
    hass: HomeAssistant,
 | 
						|
    hass_client_no_auth: Callable[[], Awaitable[TestClient]],
 | 
						|
    aioclient_mock: AiohttpClientMocker,
 | 
						|
    current_request_with_host: None,
 | 
						|
    mock_setup_entry: MagicMock,
 | 
						|
    mock_lametric_cloud_config_flow: MagicMock,
 | 
						|
    mock_lametric_config_flow: MagicMock,
 | 
						|
) -> None:
 | 
						|
    """Check a full flow importing from cloud, with a single device."""
 | 
						|
    result = await hass.config_entries.flow.async_init(
 | 
						|
        DOMAIN, context={"source": SOURCE_USER}
 | 
						|
    )
 | 
						|
 | 
						|
    assert result.get("type") == FlowResultType.MENU
 | 
						|
    assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud"
 | 
						|
    assert result.get("menu_options") == ["pick_implementation", "manual_entry"]
 | 
						|
    assert "flow_id" in result
 | 
						|
    flow_id = result["flow_id"]
 | 
						|
 | 
						|
    result2 = await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={"next_step_id": "pick_implementation"}
 | 
						|
    )
 | 
						|
 | 
						|
    # pylint: disable=protected-access
 | 
						|
    state = config_entry_oauth2_flow._encode_jwt(
 | 
						|
        hass,
 | 
						|
        {
 | 
						|
            "flow_id": flow_id,
 | 
						|
            "redirect_uri": "https://example.com/auth/external/callback",
 | 
						|
        },
 | 
						|
    )
 | 
						|
 | 
						|
    assert result2.get("type") == FlowResultType.EXTERNAL_STEP
 | 
						|
    assert result2.get("url") == (
 | 
						|
        "https://developer.lametric.com/api/v2/oauth2/authorize"
 | 
						|
        "?response_type=code&client_id=client"
 | 
						|
        "&redirect_uri=https://example.com/auth/external/callback"
 | 
						|
        f"&state={state}"
 | 
						|
        "&scope=basic+devices_read"
 | 
						|
    )
 | 
						|
 | 
						|
    client = await hass_client_no_auth()
 | 
						|
    resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
 | 
						|
    assert resp.status == HTTPStatus.OK
 | 
						|
    assert resp.headers["content-type"] == "text/html; charset=utf-8"
 | 
						|
 | 
						|
    aioclient_mock.post(
 | 
						|
        "https://developer.lametric.com/api/v2/oauth2/token",
 | 
						|
        json={
 | 
						|
            "refresh_token": "mock-refresh-token",
 | 
						|
            "access_token": "mock-access-token",
 | 
						|
            "type": "Bearer",
 | 
						|
            "expires_in": 60,
 | 
						|
        },
 | 
						|
    )
 | 
						|
 | 
						|
    # Stage a single device
 | 
						|
    # Should skip step that ask for device selection
 | 
						|
    mock_lametric_cloud_config_flow.devices.return_value = [
 | 
						|
        mock_lametric_cloud_config_flow.devices.return_value[0]
 | 
						|
    ]
 | 
						|
    result3 = await hass.config_entries.flow.async_configure(flow_id)
 | 
						|
 | 
						|
    assert result3.get("type") == FlowResultType.CREATE_ENTRY
 | 
						|
    assert result3.get("title") == "Frenck's LaMetric"
 | 
						|
    assert result3.get("data") == {
 | 
						|
        CONF_HOST: "127.0.0.1",
 | 
						|
        CONF_API_KEY: "mock-api-key",
 | 
						|
        CONF_MAC: "AA:BB:CC:DD:EE:FF",
 | 
						|
    }
 | 
						|
    assert "result" in result3
 | 
						|
    assert result3["result"].unique_id == "SA110405124500W00BS9"
 | 
						|
 | 
						|
    assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1
 | 
						|
    assert len(mock_lametric_config_flow.device.mock_calls) == 1
 | 
						|
    assert len(mock_lametric_config_flow.notify.mock_calls) == 1
 | 
						|
    assert len(mock_setup_entry.mock_calls) == 1
 | 
						|
 | 
						|
 | 
						|
async def test_full_manual(
 | 
						|
    hass: HomeAssistant,
 | 
						|
    mock_setup_entry: MagicMock,
 | 
						|
    mock_lametric_config_flow: MagicMock,
 | 
						|
) -> None:
 | 
						|
    """Check a full flow manual entry."""
 | 
						|
    result = await hass.config_entries.flow.async_init(
 | 
						|
        DOMAIN, context={"source": SOURCE_USER}
 | 
						|
    )
 | 
						|
 | 
						|
    assert result.get("type") == FlowResultType.MENU
 | 
						|
    assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud"
 | 
						|
    assert result.get("menu_options") == ["pick_implementation", "manual_entry"]
 | 
						|
    assert "flow_id" in result
 | 
						|
    flow_id = result["flow_id"]
 | 
						|
 | 
						|
    result2 = await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={"next_step_id": "manual_entry"}
 | 
						|
    )
 | 
						|
 | 
						|
    assert result2.get("type") == FlowResultType.FORM
 | 
						|
    assert result2.get("step_id") == "manual_entry"
 | 
						|
 | 
						|
    result3 = await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"}
 | 
						|
    )
 | 
						|
 | 
						|
    assert result3.get("type") == FlowResultType.CREATE_ENTRY
 | 
						|
    assert result3.get("title") == "Frenck's LaMetric"
 | 
						|
    assert result3.get("data") == {
 | 
						|
        CONF_HOST: "127.0.0.1",
 | 
						|
        CONF_API_KEY: "mock-api-key",
 | 
						|
        CONF_MAC: "AA:BB:CC:DD:EE:FF",
 | 
						|
    }
 | 
						|
    assert "result" in result3
 | 
						|
    assert result3["result"].unique_id == "SA110405124500W00BS9"
 | 
						|
 | 
						|
    assert len(mock_lametric_config_flow.device.mock_calls) == 1
 | 
						|
    assert len(mock_lametric_config_flow.notify.mock_calls) == 1
 | 
						|
    assert len(mock_setup_entry.mock_calls) == 1
 | 
						|
 | 
						|
 | 
						|
async def test_full_ssdp_with_cloud_import(
 | 
						|
    hass: HomeAssistant,
 | 
						|
    hass_client_no_auth: Callable[[], Awaitable[TestClient]],
 | 
						|
    aioclient_mock: AiohttpClientMocker,
 | 
						|
    current_request_with_host: None,
 | 
						|
    mock_setup_entry: MagicMock,
 | 
						|
    mock_lametric_cloud_config_flow: MagicMock,
 | 
						|
    mock_lametric_config_flow: MagicMock,
 | 
						|
) -> None:
 | 
						|
    """Check a full flow triggered by SSDP, importing from cloud."""
 | 
						|
    result = await hass.config_entries.flow.async_init(
 | 
						|
        DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DISCOVERY_INFO
 | 
						|
    )
 | 
						|
 | 
						|
    assert result.get("type") == FlowResultType.MENU
 | 
						|
    assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud"
 | 
						|
    assert result.get("menu_options") == ["pick_implementation", "manual_entry"]
 | 
						|
    assert "flow_id" in result
 | 
						|
    flow_id = result["flow_id"]
 | 
						|
 | 
						|
    result2 = await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={"next_step_id": "pick_implementation"}
 | 
						|
    )
 | 
						|
 | 
						|
    # pylint: disable=protected-access
 | 
						|
    state = config_entry_oauth2_flow._encode_jwt(
 | 
						|
        hass,
 | 
						|
        {
 | 
						|
            "flow_id": flow_id,
 | 
						|
            "redirect_uri": "https://example.com/auth/external/callback",
 | 
						|
        },
 | 
						|
    )
 | 
						|
 | 
						|
    assert result2.get("type") == FlowResultType.EXTERNAL_STEP
 | 
						|
    assert result2.get("url") == (
 | 
						|
        "https://developer.lametric.com/api/v2/oauth2/authorize"
 | 
						|
        "?response_type=code&client_id=client"
 | 
						|
        "&redirect_uri=https://example.com/auth/external/callback"
 | 
						|
        f"&state={state}"
 | 
						|
        "&scope=basic+devices_read"
 | 
						|
    )
 | 
						|
 | 
						|
    client = await hass_client_no_auth()
 | 
						|
    resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
 | 
						|
    assert resp.status == HTTPStatus.OK
 | 
						|
    assert resp.headers["content-type"] == "text/html; charset=utf-8"
 | 
						|
 | 
						|
    aioclient_mock.post(
 | 
						|
        "https://developer.lametric.com/api/v2/oauth2/token",
 | 
						|
        json={
 | 
						|
            "refresh_token": "mock-refresh-token",
 | 
						|
            "access_token": "mock-access-token",
 | 
						|
            "type": "Bearer",
 | 
						|
            "expires_in": 60,
 | 
						|
        },
 | 
						|
    )
 | 
						|
 | 
						|
    result3 = await hass.config_entries.flow.async_configure(flow_id)
 | 
						|
 | 
						|
    assert result3.get("type") == FlowResultType.CREATE_ENTRY
 | 
						|
    assert result3.get("title") == "Frenck's LaMetric"
 | 
						|
    assert result3.get("data") == {
 | 
						|
        CONF_HOST: "127.0.0.1",
 | 
						|
        CONF_API_KEY: "mock-api-key",
 | 
						|
        CONF_MAC: "AA:BB:CC:DD:EE:FF",
 | 
						|
    }
 | 
						|
    assert "result" in result3
 | 
						|
    assert result3["result"].unique_id == "SA110405124500W00BS9"
 | 
						|
 | 
						|
    assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1
 | 
						|
    assert len(mock_lametric_config_flow.device.mock_calls) == 1
 | 
						|
    assert len(mock_lametric_config_flow.notify.mock_calls) == 1
 | 
						|
    assert len(mock_setup_entry.mock_calls) == 1
 | 
						|
 | 
						|
 | 
						|
async def test_full_ssdp_manual_entry(
 | 
						|
    hass: HomeAssistant,
 | 
						|
    mock_setup_entry: MagicMock,
 | 
						|
    mock_lametric_config_flow: MagicMock,
 | 
						|
) -> None:
 | 
						|
    """Check a full flow triggered by SSDP, with manual API key entry."""
 | 
						|
    result = await hass.config_entries.flow.async_init(
 | 
						|
        DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DISCOVERY_INFO
 | 
						|
    )
 | 
						|
 | 
						|
    assert result.get("type") == FlowResultType.MENU
 | 
						|
    assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud"
 | 
						|
    assert result.get("menu_options") == ["pick_implementation", "manual_entry"]
 | 
						|
    assert "flow_id" in result
 | 
						|
    flow_id = result["flow_id"]
 | 
						|
 | 
						|
    result2 = await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={"next_step_id": "manual_entry"}
 | 
						|
    )
 | 
						|
 | 
						|
    assert result2.get("type") == FlowResultType.FORM
 | 
						|
    assert result2.get("step_id") == "manual_entry"
 | 
						|
 | 
						|
    result3 = await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={CONF_API_KEY: "mock-api-key"}
 | 
						|
    )
 | 
						|
 | 
						|
    assert result3.get("type") == FlowResultType.CREATE_ENTRY
 | 
						|
    assert result3.get("title") == "Frenck's LaMetric"
 | 
						|
    assert result3.get("data") == {
 | 
						|
        CONF_HOST: "127.0.0.1",
 | 
						|
        CONF_API_KEY: "mock-api-key",
 | 
						|
        CONF_MAC: "AA:BB:CC:DD:EE:FF",
 | 
						|
    }
 | 
						|
    assert "result" in result3
 | 
						|
    assert result3["result"].unique_id == "SA110405124500W00BS9"
 | 
						|
 | 
						|
    assert len(mock_lametric_config_flow.device.mock_calls) == 1
 | 
						|
    assert len(mock_lametric_config_flow.notify.mock_calls) == 1
 | 
						|
    assert len(mock_setup_entry.mock_calls) == 1
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.parametrize(
 | 
						|
    "data,reason",
 | 
						|
    [
 | 
						|
        (
 | 
						|
            SsdpServiceInfo(ssdp_usn="mock_usn", ssdp_st="mock_st", upnp={}),
 | 
						|
            "invalid_discovery_info",
 | 
						|
        ),
 | 
						|
        (
 | 
						|
            SsdpServiceInfo(
 | 
						|
                ssdp_usn="mock_usn",
 | 
						|
                ssdp_st="mock_st",
 | 
						|
                ssdp_location="http://169.254.0.1:44057/465d585b-1c05-444a-b14e-6ffb875b46a6/device_description.xml",
 | 
						|
                upnp={
 | 
						|
                    ATTR_UPNP_SERIAL: "SA110405124500W00BS9",
 | 
						|
                },
 | 
						|
            ),
 | 
						|
            "link_local_address",
 | 
						|
        ),
 | 
						|
    ],
 | 
						|
)
 | 
						|
async def test_ssdp_abort_invalid_discovery(
 | 
						|
    hass: HomeAssistant, data: SsdpServiceInfo, reason: str
 | 
						|
) -> None:
 | 
						|
    """Check a full flow triggered by SSDP, with manual API key entry."""
 | 
						|
    result = await hass.config_entries.flow.async_init(
 | 
						|
        DOMAIN, context={"source": SOURCE_SSDP}, data=data
 | 
						|
    )
 | 
						|
    assert result.get("type") == FlowResultType.ABORT
 | 
						|
    assert result.get("reason") == reason
 | 
						|
 | 
						|
 | 
						|
async def test_cloud_import_updates_existing_entry(
 | 
						|
    hass: HomeAssistant,
 | 
						|
    hass_client_no_auth: Callable[[], Awaitable[TestClient]],
 | 
						|
    aioclient_mock: AiohttpClientMocker,
 | 
						|
    current_request_with_host: None,
 | 
						|
    mock_lametric_cloud_config_flow: MagicMock,
 | 
						|
    mock_lametric_config_flow: MagicMock,
 | 
						|
    mock_config_entry: MockConfigEntry,
 | 
						|
) -> None:
 | 
						|
    """Test cloud importing existing device updates existing entry."""
 | 
						|
    mock_config_entry.add_to_hass(hass)
 | 
						|
 | 
						|
    result = await hass.config_entries.flow.async_init(
 | 
						|
        DOMAIN, context={"source": SOURCE_USER}
 | 
						|
    )
 | 
						|
    assert "flow_id" in result
 | 
						|
    flow_id = result["flow_id"]
 | 
						|
 | 
						|
    await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={"next_step_id": "pick_implementation"}
 | 
						|
    )
 | 
						|
 | 
						|
    # pylint: disable=protected-access
 | 
						|
    state = config_entry_oauth2_flow._encode_jwt(
 | 
						|
        hass,
 | 
						|
        {
 | 
						|
            "flow_id": flow_id,
 | 
						|
            "redirect_uri": "https://example.com/auth/external/callback",
 | 
						|
        },
 | 
						|
    )
 | 
						|
    client = await hass_client_no_auth()
 | 
						|
    await client.get(f"/auth/external/callback?code=abcd&state={state}")
 | 
						|
    aioclient_mock.post(
 | 
						|
        "https://developer.lametric.com/api/v2/oauth2/token",
 | 
						|
        json={
 | 
						|
            "refresh_token": "mock-refresh-token",
 | 
						|
            "access_token": "mock-access-token",
 | 
						|
            "type": "Bearer",
 | 
						|
            "expires_in": 60,
 | 
						|
        },
 | 
						|
    )
 | 
						|
    await hass.config_entries.flow.async_configure(flow_id)
 | 
						|
 | 
						|
    result2 = await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"}
 | 
						|
    )
 | 
						|
 | 
						|
    assert result2.get("type") == FlowResultType.ABORT
 | 
						|
    assert result2.get("reason") == "already_configured"
 | 
						|
    assert mock_config_entry.data == {
 | 
						|
        CONF_HOST: "127.0.0.1",
 | 
						|
        CONF_API_KEY: "mock-api-key",
 | 
						|
        CONF_MAC: "AA:BB:CC:DD:EE:FF",
 | 
						|
    }
 | 
						|
 | 
						|
    assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1
 | 
						|
    assert len(mock_lametric_config_flow.device.mock_calls) == 1
 | 
						|
    assert len(mock_lametric_config_flow.notify.mock_calls) == 0
 | 
						|
 | 
						|
 | 
						|
async def test_manual_updates_existing_entry(
 | 
						|
    hass: HomeAssistant,
 | 
						|
    mock_lametric_config_flow: MagicMock,
 | 
						|
    mock_config_entry: MockConfigEntry,
 | 
						|
) -> None:
 | 
						|
    """Test adding existing device updates existing entry."""
 | 
						|
    mock_config_entry.add_to_hass(hass)
 | 
						|
 | 
						|
    result = await hass.config_entries.flow.async_init(
 | 
						|
        DOMAIN, context={"source": SOURCE_USER}
 | 
						|
    )
 | 
						|
    assert "flow_id" in result
 | 
						|
    flow_id = result["flow_id"]
 | 
						|
 | 
						|
    await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={"next_step_id": "manual_entry"}
 | 
						|
    )
 | 
						|
 | 
						|
    result3 = await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"}
 | 
						|
    )
 | 
						|
 | 
						|
    assert result3.get("type") == FlowResultType.ABORT
 | 
						|
    assert result3.get("reason") == "already_configured"
 | 
						|
    assert mock_config_entry.data == {
 | 
						|
        CONF_HOST: "127.0.0.1",
 | 
						|
        CONF_API_KEY: "mock-api-key",
 | 
						|
        CONF_MAC: "AA:BB:CC:DD:EE:FF",
 | 
						|
    }
 | 
						|
 | 
						|
    assert len(mock_lametric_config_flow.device.mock_calls) == 1
 | 
						|
    assert len(mock_lametric_config_flow.notify.mock_calls) == 0
 | 
						|
 | 
						|
 | 
						|
async def test_discovery_updates_existing_entry(
 | 
						|
    hass: HomeAssistant, mock_config_entry: MockConfigEntry
 | 
						|
) -> None:
 | 
						|
    """Test discovery of existing device updates entry."""
 | 
						|
    mock_config_entry.add_to_hass(hass)
 | 
						|
 | 
						|
    result = await hass.config_entries.flow.async_init(
 | 
						|
        DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DISCOVERY_INFO
 | 
						|
    )
 | 
						|
 | 
						|
    assert result.get("type") == FlowResultType.ABORT
 | 
						|
    assert result.get("reason") == "already_configured"
 | 
						|
    assert mock_config_entry.data == {
 | 
						|
        CONF_HOST: "127.0.0.1",
 | 
						|
        CONF_API_KEY: "mock-from-fixture",
 | 
						|
        CONF_MAC: "AA:BB:CC:DD:EE:FF",
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
async def test_cloud_abort_no_devices(
 | 
						|
    hass: HomeAssistant,
 | 
						|
    hass_client_no_auth: Callable[[], Awaitable[TestClient]],
 | 
						|
    aioclient_mock: AiohttpClientMocker,
 | 
						|
    current_request_with_host: None,
 | 
						|
    mock_lametric_cloud_config_flow: MagicMock,
 | 
						|
) -> None:
 | 
						|
    """Test cloud importing aborts when account has no devices."""
 | 
						|
    result = await hass.config_entries.flow.async_init(
 | 
						|
        DOMAIN, context={"source": SOURCE_USER}
 | 
						|
    )
 | 
						|
    assert "flow_id" in result
 | 
						|
    flow_id = result["flow_id"]
 | 
						|
 | 
						|
    await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={"next_step_id": "pick_implementation"}
 | 
						|
    )
 | 
						|
 | 
						|
    # pylint: disable=protected-access
 | 
						|
    state = config_entry_oauth2_flow._encode_jwt(
 | 
						|
        hass,
 | 
						|
        {
 | 
						|
            "flow_id": flow_id,
 | 
						|
            "redirect_uri": "https://example.com/auth/external/callback",
 | 
						|
        },
 | 
						|
    )
 | 
						|
    client = await hass_client_no_auth()
 | 
						|
    await client.get(f"/auth/external/callback?code=abcd&state={state}")
 | 
						|
    aioclient_mock.post(
 | 
						|
        "https://developer.lametric.com/api/v2/oauth2/token",
 | 
						|
        json={
 | 
						|
            "refresh_token": "mock-refresh-token",
 | 
						|
            "access_token": "mock-access-token",
 | 
						|
            "type": "Bearer",
 | 
						|
            "expires_in": 60,
 | 
						|
        },
 | 
						|
    )
 | 
						|
 | 
						|
    # Stage there are no devices
 | 
						|
    mock_lametric_cloud_config_flow.devices.return_value = []
 | 
						|
    result2 = await hass.config_entries.flow.async_configure(flow_id)
 | 
						|
 | 
						|
    assert result2.get("type") == FlowResultType.ABORT
 | 
						|
    assert result2.get("reason") == "no_devices"
 | 
						|
 | 
						|
    assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.parametrize(
 | 
						|
    "side_effect,reason",
 | 
						|
    [
 | 
						|
        (LaMetricConnectionTimeoutError, "cannot_connect"),
 | 
						|
        (LaMetricConnectionError, "cannot_connect"),
 | 
						|
        (LaMetricError, "unknown"),
 | 
						|
        (RuntimeError, "unknown"),
 | 
						|
    ],
 | 
						|
)
 | 
						|
async def test_manual_errors(
 | 
						|
    hass: HomeAssistant,
 | 
						|
    mock_lametric_config_flow: MagicMock,
 | 
						|
    mock_setup_entry: MagicMock,
 | 
						|
    side_effect: Exception,
 | 
						|
    reason: str,
 | 
						|
) -> None:
 | 
						|
    """Test adding existing device updates existing entry."""
 | 
						|
    result = await hass.config_entries.flow.async_init(
 | 
						|
        DOMAIN, context={"source": SOURCE_USER}
 | 
						|
    )
 | 
						|
    assert "flow_id" in result
 | 
						|
    flow_id = result["flow_id"]
 | 
						|
 | 
						|
    await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={"next_step_id": "manual_entry"}
 | 
						|
    )
 | 
						|
 | 
						|
    mock_lametric_config_flow.device.side_effect = side_effect
 | 
						|
    result2 = await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"}
 | 
						|
    )
 | 
						|
 | 
						|
    assert result2.get("type") == FlowResultType.FORM
 | 
						|
    assert result2.get("step_id") == "manual_entry"
 | 
						|
    assert result2.get("errors") == {"base": reason}
 | 
						|
 | 
						|
    assert len(mock_lametric_config_flow.device.mock_calls) == 1
 | 
						|
    assert len(mock_lametric_config_flow.notify.mock_calls) == 0
 | 
						|
    assert len(mock_setup_entry.mock_calls) == 0
 | 
						|
 | 
						|
    mock_lametric_config_flow.device.side_effect = None
 | 
						|
    result3 = await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"}
 | 
						|
    )
 | 
						|
 | 
						|
    assert result3.get("type") == FlowResultType.CREATE_ENTRY
 | 
						|
    assert result3.get("title") == "Frenck's LaMetric"
 | 
						|
    assert result3.get("data") == {
 | 
						|
        CONF_HOST: "127.0.0.1",
 | 
						|
        CONF_API_KEY: "mock-api-key",
 | 
						|
        CONF_MAC: "AA:BB:CC:DD:EE:FF",
 | 
						|
    }
 | 
						|
    assert "result" in result3
 | 
						|
    assert result3["result"].unique_id == "SA110405124500W00BS9"
 | 
						|
 | 
						|
    assert len(mock_lametric_config_flow.device.mock_calls) == 2
 | 
						|
    assert len(mock_lametric_config_flow.notify.mock_calls) == 1
 | 
						|
    assert len(mock_setup_entry.mock_calls) == 1
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.parametrize(
 | 
						|
    "side_effect,reason",
 | 
						|
    [
 | 
						|
        (LaMetricConnectionTimeoutError, "cannot_connect"),
 | 
						|
        (LaMetricConnectionError, "cannot_connect"),
 | 
						|
        (LaMetricError, "unknown"),
 | 
						|
        (RuntimeError, "unknown"),
 | 
						|
    ],
 | 
						|
)
 | 
						|
async def test_cloud_errors(
 | 
						|
    hass: HomeAssistant,
 | 
						|
    hass_client_no_auth: Callable[[], Awaitable[TestClient]],
 | 
						|
    aioclient_mock: AiohttpClientMocker,
 | 
						|
    current_request_with_host: None,
 | 
						|
    mock_setup_entry: MagicMock,
 | 
						|
    mock_lametric_cloud_config_flow: MagicMock,
 | 
						|
    mock_lametric_config_flow: MagicMock,
 | 
						|
    side_effect: Exception,
 | 
						|
    reason: str,
 | 
						|
) -> None:
 | 
						|
    """Test adding existing device updates existing entry."""
 | 
						|
    result = await hass.config_entries.flow.async_init(
 | 
						|
        DOMAIN, context={"source": SOURCE_USER}
 | 
						|
    )
 | 
						|
    assert "flow_id" in result
 | 
						|
    flow_id = result["flow_id"]
 | 
						|
 | 
						|
    await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={"next_step_id": "pick_implementation"}
 | 
						|
    )
 | 
						|
 | 
						|
    # pylint: disable=protected-access
 | 
						|
    state = config_entry_oauth2_flow._encode_jwt(
 | 
						|
        hass,
 | 
						|
        {
 | 
						|
            "flow_id": flow_id,
 | 
						|
            "redirect_uri": "https://example.com/auth/external/callback",
 | 
						|
        },
 | 
						|
    )
 | 
						|
    client = await hass_client_no_auth()
 | 
						|
    await client.get(f"/auth/external/callback?code=abcd&state={state}")
 | 
						|
    aioclient_mock.post(
 | 
						|
        "https://developer.lametric.com/api/v2/oauth2/token",
 | 
						|
        json={
 | 
						|
            "refresh_token": "mock-refresh-token",
 | 
						|
            "access_token": "mock-access-token",
 | 
						|
            "type": "Bearer",
 | 
						|
            "expires_in": 60,
 | 
						|
        },
 | 
						|
    )
 | 
						|
    await hass.config_entries.flow.async_configure(flow_id)
 | 
						|
 | 
						|
    mock_lametric_config_flow.device.side_effect = side_effect
 | 
						|
    result2 = await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"}
 | 
						|
    )
 | 
						|
 | 
						|
    assert result2.get("type") == FlowResultType.FORM
 | 
						|
    assert result2.get("step_id") == "cloud_select_device"
 | 
						|
    assert result2.get("errors") == {"base": reason}
 | 
						|
 | 
						|
    assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1
 | 
						|
    assert len(mock_lametric_config_flow.device.mock_calls) == 1
 | 
						|
    assert len(mock_lametric_config_flow.notify.mock_calls) == 0
 | 
						|
    assert len(mock_setup_entry.mock_calls) == 0
 | 
						|
 | 
						|
    mock_lametric_config_flow.device.side_effect = None
 | 
						|
    result3 = await hass.config_entries.flow.async_configure(
 | 
						|
        flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"}
 | 
						|
    )
 | 
						|
 | 
						|
    assert result3.get("type") == FlowResultType.CREATE_ENTRY
 | 
						|
    assert result3.get("title") == "Frenck's LaMetric"
 | 
						|
    assert result3.get("data") == {
 | 
						|
        CONF_HOST: "127.0.0.1",
 | 
						|
        CONF_API_KEY: "mock-api-key",
 | 
						|
        CONF_MAC: "AA:BB:CC:DD:EE:FF",
 | 
						|
    }
 | 
						|
    assert "result" in result3
 | 
						|
    assert result3["result"].unique_id == "SA110405124500W00BS9"
 | 
						|
 | 
						|
    assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1
 | 
						|
    assert len(mock_lametric_config_flow.device.mock_calls) == 2
 | 
						|
    assert len(mock_lametric_config_flow.notify.mock_calls) == 1
 | 
						|
    assert len(mock_setup_entry.mock_calls) == 1
 | 
						|
 | 
						|
 | 
						|
async def test_dhcp_discovery_updates_entry(
 | 
						|
    hass: HomeAssistant,
 | 
						|
    mock_config_entry: MockConfigEntry,
 | 
						|
) -> None:
 | 
						|
    """Test DHCP discovery updates config entries."""
 | 
						|
    mock_config_entry.add_to_hass(hass)
 | 
						|
 | 
						|
    result = await hass.config_entries.flow.async_init(
 | 
						|
        DOMAIN,
 | 
						|
        context={"source": SOURCE_DHCP},
 | 
						|
        data=DhcpServiceInfo(
 | 
						|
            hostname="lametric",
 | 
						|
            ip="127.0.0.42",
 | 
						|
            macaddress="aa:bb:cc:dd:ee:ff",
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    assert result.get("type") == FlowResultType.ABORT
 | 
						|
    assert result.get("reason") == "already_configured"
 | 
						|
    assert mock_config_entry.data == {
 | 
						|
        CONF_API_KEY: "mock-from-fixture",
 | 
						|
        CONF_HOST: "127.0.0.42",
 | 
						|
        CONF_MAC: "AA:BB:CC:DD:EE:FF",
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
async def test_dhcp_unknown_device(
 | 
						|
    hass: HomeAssistant,
 | 
						|
    mock_config_entry: MockConfigEntry,
 | 
						|
) -> None:
 | 
						|
    """Test unknown DHCP discovery aborts flow."""
 | 
						|
    mock_config_entry.add_to_hass(hass)
 | 
						|
 | 
						|
    result = await hass.config_entries.flow.async_init(
 | 
						|
        DOMAIN,
 | 
						|
        context={"source": SOURCE_DHCP},
 | 
						|
        data=DhcpServiceInfo(
 | 
						|
            hostname="lametric",
 | 
						|
            ip="127.0.0.42",
 | 
						|
            macaddress="aa:bb:cc:dd:ee:00",
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    assert result.get("type") == FlowResultType.ABORT
 | 
						|
    assert result.get("reason") == "unknown"
 |