Improve flume test coverage (#120851)

* Add Flume init tests

* Increase test coverage

* Improve readability

* Fix pydoc for tests

* Use pytest.mark.usefixtures
pull/120941/head
Allen Porter 2024-07-01 07:41:47 -07:00 committed by GitHub
parent c9911fa8ce
commit 2506acc095
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 392 additions and 88 deletions

View File

@ -441,7 +441,6 @@ omit =
homeassistant/components/flick_electric/__init__.py homeassistant/components/flick_electric/__init__.py
homeassistant/components/flick_electric/sensor.py homeassistant/components/flick_electric/sensor.py
homeassistant/components/flock/notify.py homeassistant/components/flock/notify.py
homeassistant/components/flume/__init__.py
homeassistant/components/flume/binary_sensor.py homeassistant/components/flume/binary_sensor.py
homeassistant/components/flume/coordinator.py homeassistant/components/flume/coordinator.py
homeassistant/components/flume/entity.py homeassistant/components/flume/entity.py

View File

@ -0,0 +1,167 @@
"""Flume test fixtures."""
from collections.abc import Generator
import datetime
from http import HTTPStatus
import json
from unittest.mock import mock_open, patch
import jwt
import pytest
import requests
from requests_mock.mocker import Mocker
from homeassistant.components.flume.const import DOMAIN
from homeassistant.const import (
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_PASSWORD,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
USER_ID = "test-user-id"
REFRESH_TOKEN = "refresh-token"
TOKEN_URL = "https://api.flumetech.com/oauth/token"
DEVICE_LIST_URL = (
"https://api.flumetech.com/users/test-user-id/devices?user=true&location=true"
)
BRIDGE_DEVICE = {
"id": "1234",
"type": 1, # Bridge
"location": {
"name": "Bridge Location",
},
"name": "Flume Bridge",
"connected": True,
}
SENSOR_DEVICE = {
"id": "1234",
"type": 2, # Sensor
"location": {
"name": "Sensor Location",
},
"name": "Flume Sensor",
"connected": True,
}
DEVICE_LIST = [BRIDGE_DEVICE, SENSOR_DEVICE]
NOTIFICATIONS_URL = "https://api.flumetech.com/users/test-user-id/notifications?limit=50&offset=0&sort_direction=ASC"
NOTIFICATION = {
"id": 111111,
"device_id": "6248148189204194987",
"user_id": USER_ID,
"type": 1,
"message": "Low Flow Leak triggered at Home. Water has been running for 2 hours averaging 0.43 gallons every minute.",
"created_datetime": "2020-01-15T16:33:39.000Z",
"title": "Potential Leak Detected!",
"read": True,
"extra": {
"query": {
"request_id": "SYSTEM_TRIGGERED_USAGE_ALERT",
"since_datetime": "2020-01-15 06:33:59",
"until_datetime": "2020-01-15 08:33:59",
"tz": "America/Los_Angeles",
"bucket": "MIN",
"raw": False,
"group_multiplier": 2,
"device_id": ["6248148189204194987"],
}
},
"event_rule": "Low Flow Leak",
}
NOTIFICATIONS_LIST = [NOTIFICATION]
@pytest.fixture(name="config_entry")
def config_entry_fixture(hass: HomeAssistant) -> MockConfigEntry:
"""Fixture to create a config entry."""
config_entry = MockConfigEntry(
domain=DOMAIN,
title="test-username",
unique_id="test-username",
data={
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
CONF_CLIENT_ID: "client_id",
CONF_CLIENT_SECRET: "client_secret",
},
)
config_entry.add_to_hass(hass)
return config_entry
def encode_access_token() -> str:
"""Encode the payload of the access token."""
expiration_time = datetime.datetime.now() + datetime.timedelta(hours=12)
payload = {
"user_id": USER_ID,
"exp": int(expiration_time.timestamp()),
}
return jwt.encode(payload, key="secret")
@pytest.fixture(name="access_token")
def access_token_fixture(requests_mock: Mocker) -> Generator[None, None, None]:
"""Fixture to setup the access token."""
token_response = {
"refresh_token": REFRESH_TOKEN,
"access_token": encode_access_token(),
}
requests_mock.register_uri(
"POST",
TOKEN_URL,
status_code=HTTPStatus.OK,
json={"data": [token_response]},
)
with patch("builtins.open", mock_open(read_data=json.dumps(token_response))):
yield
@pytest.fixture(name="device_list")
def device_list_fixture(requests_mock: Mocker) -> None:
"""Fixture to setup the device list API response access token."""
requests_mock.register_uri(
"GET",
DEVICE_LIST_URL,
status_code=HTTPStatus.OK,
json={
"data": DEVICE_LIST,
},
)
@pytest.fixture(name="device_list_timeout")
def device_list_timeout_fixture(requests_mock: Mocker) -> None:
"""Fixture to test a timeout when connecting to the device list url."""
requests_mock.register_uri(
"GET",
DEVICE_LIST_URL,
exc=requests.exceptions.ConnectTimeout,
)
@pytest.fixture(name="device_list_unauthorized")
def device_list_unauthorized_fixture(requests_mock: Mocker) -> None:
"""Fixture to test an authorized error from the device list url."""
requests_mock.register_uri(
"GET",
DEVICE_LIST_URL,
status_code=HTTPStatus.UNAUTHORIZED,
json={},
)
@pytest.fixture(name="notifications_list")
def notifications_list_fixture(requests_mock: Mocker) -> None:
"""Fixture to setup the device list API response access token."""
requests_mock.register_uri(
"GET",
NOTIFICATIONS_URL,
status_code=HTTPStatus.OK,
json={
"data": NOTIFICATIONS_LIST,
},
)

View File

@ -1,8 +1,11 @@
"""Test the flume config flow.""" """Test the flume config flow."""
from unittest.mock import MagicMock, patch from http import HTTPStatus
from unittest.mock import patch
import pytest
import requests.exceptions import requests.exceptions
from requests_mock.mocker import Mocker
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.flume.const import DOMAIN from homeassistant.components.flume.const import DOMAIN
@ -15,15 +18,12 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from .conftest import DEVICE_LIST, DEVICE_LIST_URL
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
def _get_mocked_flume_device_list(): @pytest.mark.usefixtures("access_token", "device_list")
flume_device_list_mock = MagicMock()
type(flume_device_list_mock).device_list = ["mock"]
return flume_device_list_mock
async def test_form(hass: HomeAssistant) -> None: async def test_form(hass: HomeAssistant) -> None:
"""Test we get the form and can setup from user input.""" """Test we get the form and can setup from user input."""
@ -33,17 +33,7 @@ async def test_form(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["errors"] == {} assert result["errors"] == {}
mock_flume_device_list = _get_mocked_flume_device_list()
with ( with (
patch(
"homeassistant.components.flume.config_flow.FlumeAuth",
return_value=True,
),
patch(
"homeassistant.components.flume.config_flow.FlumeDeviceList",
return_value=mock_flume_device_list,
),
patch( patch(
"homeassistant.components.flume.async_setup_entry", "homeassistant.components.flume.async_setup_entry",
return_value=True, return_value=True,
@ -71,66 +61,57 @@ async def test_form(hass: HomeAssistant) -> None:
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_form_invalid_auth(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("access_token")
async def test_form_invalid_auth(hass: HomeAssistant, requests_mock: Mocker) -> None:
"""Test we handle invalid auth.""" """Test we handle invalid auth."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
with ( requests_mock.register_uri(
patch( "GET",
"homeassistant.components.flume.config_flow.FlumeAuth", DEVICE_LIST_URL,
return_value=True, status_code=HTTPStatus.UNAUTHORIZED,
), json={"message": "Failure"},
patch( )
"homeassistant.components.flume.config_flow.FlumeDeviceList",
side_effect=Exception, result2 = await hass.config_entries.flow.async_configure(
), result["flow_id"],
): {
result2 = await hass.config_entries.flow.async_configure( CONF_USERNAME: "test-username",
result["flow_id"], CONF_PASSWORD: "test-password",
{ CONF_CLIENT_ID: "client_id",
CONF_USERNAME: "test-username", CONF_CLIENT_SECRET: "client_secret",
CONF_PASSWORD: "test-password", },
CONF_CLIENT_ID: "client_id", )
CONF_CLIENT_SECRET: "client_secret",
},
)
assert result2["type"] is FlowResultType.FORM assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"password": "invalid_auth"} assert result2["errors"] == {"password": "invalid_auth"}
@pytest.mark.usefixtures("access_token", "device_list_timeout")
async def test_form_cannot_connect(hass: HomeAssistant) -> None: async def test_form_cannot_connect(hass: HomeAssistant) -> None:
"""Test we handle cannot connect error.""" """Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
with (
patch( result2 = await hass.config_entries.flow.async_configure(
"homeassistant.components.flume.config_flow.FlumeAuth", result["flow_id"],
return_value=True, {
), CONF_USERNAME: "test-username",
patch( CONF_PASSWORD: "test-password",
"homeassistant.components.flume.config_flow.FlumeDeviceList", CONF_CLIENT_ID: "client_id",
side_effect=requests.exceptions.ConnectionError(), CONF_CLIENT_SECRET: "client_secret",
), },
): )
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
CONF_CLIENT_ID: "client_id",
CONF_CLIENT_SECRET: "client_secret",
},
)
assert result2["type"] is FlowResultType.FORM assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "cannot_connect"} assert result2["errors"] == {"base": "cannot_connect"}
async def test_reauth(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("access_token")
async def test_reauth(hass: HomeAssistant, requests_mock: Mocker) -> None:
"""Test we can reauth.""" """Test we can reauth."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
@ -151,35 +132,28 @@ async def test_reauth(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm" assert result["step_id"] == "reauth_confirm"
with ( result2 = await hass.config_entries.flow.async_configure(
patch( result["flow_id"],
"homeassistant.components.flume.config_flow.FlumeAuth", {
return_value=True, CONF_PASSWORD: "test-password",
), },
patch( )
"homeassistant.components.flume.config_flow.FlumeDeviceList",
side_effect=Exception,
),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_PASSWORD: "test-password",
},
)
assert result2["type"] is FlowResultType.FORM assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"password": "invalid_auth"} assert result2["errors"] == {"password": "invalid_auth"}
requests_mock.register_uri(
"GET",
DEVICE_LIST_URL,
exc=requests.exceptions.ConnectTimeout,
)
with ( with (
patch( patch(
"homeassistant.components.flume.config_flow.FlumeAuth", "homeassistant.components.flume.config_flow.os.path.exists",
return_value=True, return_value=True,
), ),
patch( patch("homeassistant.components.flume.config_flow.os.unlink") as mock_unlink,
"homeassistant.components.flume.config_flow.FlumeDeviceList",
side_effect=requests.exceptions.ConnectionError(),
),
): ):
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], result2["flow_id"],
@ -187,21 +161,22 @@ async def test_reauth(hass: HomeAssistant) -> None:
CONF_PASSWORD: "test-password", CONF_PASSWORD: "test-password",
}, },
) )
# The existing token file was removed
assert len(mock_unlink.mock_calls) == 1
assert result3["type"] is FlowResultType.FORM assert result3["type"] is FlowResultType.FORM
assert result3["errors"] == {"base": "cannot_connect"} assert result3["errors"] == {"base": "cannot_connect"}
mock_flume_device_list = _get_mocked_flume_device_list() requests_mock.register_uri(
"GET",
DEVICE_LIST_URL,
status_code=HTTPStatus.OK,
json={
"data": DEVICE_LIST,
},
)
with ( with (
patch(
"homeassistant.components.flume.config_flow.FlumeAuth",
return_value=True,
),
patch(
"homeassistant.components.flume.config_flow.FlumeDeviceList",
return_value=mock_flume_device_list,
),
patch( patch(
"homeassistant.components.flume.async_setup_entry", "homeassistant.components.flume.async_setup_entry",
return_value=True, return_value=True,
@ -217,3 +192,31 @@ async def test_reauth(hass: HomeAssistant) -> None:
assert mock_setup_entry.called assert mock_setup_entry.called
assert result4["type"] is FlowResultType.ABORT assert result4["type"] is FlowResultType.ABORT
assert result4["reason"] == "reauth_successful" assert result4["reason"] == "reauth_successful"
@pytest.mark.usefixtures("access_token")
async def test_form_no_devices(hass: HomeAssistant, requests_mock: Mocker) -> None:
"""Test a device list response that contains no values will raise an error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
requests_mock.register_uri(
"GET",
DEVICE_LIST_URL,
status_code=HTTPStatus.OK,
json={"data": []},
)
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
CONF_CLIENT_ID: "client_id",
CONF_CLIENT_SECRET: "client_secret",
},
)
assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "cannot_connect"}

View File

@ -0,0 +1,135 @@
"""Test the flume init."""
from collections.abc import Generator
from unittest.mock import patch
import pytest
from requests_mock.mocker import Mocker
from homeassistant import config_entries
from homeassistant.components.flume.const import DOMAIN
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .conftest import USER_ID
from tests.common import MockConfigEntry
@pytest.fixture(autouse=True)
def platforms_fixture() -> Generator[list[str]]:
"""Return the platforms to be loaded for this test."""
# Arbitrary platform to ensure notifications are loaded
with patch("homeassistant.components.flume.PLATFORMS", [Platform.BINARY_SENSOR]):
yield
@pytest.mark.usefixtures("access_token", "device_list")
async def test_setup_config_entry(
hass: HomeAssistant,
requests_mock: Mocker,
config_entry: MockConfigEntry,
) -> None:
"""Test load and unload of a ConfigEntry."""
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is config_entries.ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(config_entry.entry_id)
assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED
@pytest.mark.usefixtures("access_token", "device_list_timeout")
async def test_device_list_timeout(
hass: HomeAssistant,
requests_mock: Mocker,
config_entry: MockConfigEntry,
) -> None:
"""Test error handling for a timeout when listing devices."""
assert not await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is config_entries.ConfigEntryState.SETUP_RETRY
@pytest.mark.usefixtures("access_token", "device_list_unauthorized")
async def test_reauth_when_unauthorized(
hass: HomeAssistant,
requests_mock: Mocker,
config_entry: MockConfigEntry,
) -> None:
"""Test error handling for an authentication error when listing devices."""
assert not await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "reauth_confirm"
@pytest.mark.usefixtures("access_token", "device_list", "notifications_list")
async def test_list_notifications_service(
hass: HomeAssistant,
requests_mock: Mocker,
config_entry: MockConfigEntry,
) -> None:
"""Test the list notifications service."""
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is config_entries.ConfigEntryState.LOADED
response = await hass.services.async_call(
DOMAIN,
"list_notifications",
{},
target={
"config_entry": config_entry.entry_id,
},
blocking=True,
return_response=True,
)
notifications = response.get("notifications")
assert notifications
assert len(notifications) == 1
assert notifications[0].get("user_id") == USER_ID
@pytest.mark.usefixtures("access_token", "device_list", "notifications_list")
async def test_list_notifications_service_config_entry_errors(
hass: HomeAssistant,
requests_mock: Mocker,
config_entry: MockConfigEntry,
) -> None:
"""Test error handling for notification service with invalid config entries."""
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is config_entries.ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(config_entry.entry_id)
assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED
with pytest.raises(ValueError, match="Config entry not loaded"):
await hass.services.async_call(
DOMAIN,
"list_notifications",
{},
target={
"config_entry": config_entry.entry_id,
},
blocking=True,
return_response=True,
)
with pytest.raises(ValueError, match="Invalid config entry: does-not-exist"):
await hass.services.async_call(
DOMAIN,
"list_notifications",
{},
target={
"config_entry": "does-not-exist",
},
blocking=True,
return_response=True,
)