Add config-flow to NextBus (#92149)

This commit is contained in:
Ian
2023-09-19 17:10:29 +02:00
committed by GitHub
parent d9227a7e3d
commit c3f74ae022
12 changed files with 710 additions and 137 deletions
+36
View File
@@ -0,0 +1,36 @@
"""Test helpers for NextBus tests."""
from unittest.mock import MagicMock
import pytest
@pytest.fixture
def mock_nextbus_lists(mock_nextbus: MagicMock) -> MagicMock:
"""Mock all list functions in nextbus to test validate logic."""
instance = mock_nextbus.return_value
instance.get_agency_list.return_value = {
"agency": [{"tag": "sf-muni", "title": "San Francisco Muni"}]
}
instance.get_route_list.return_value = {
"route": [{"tag": "F", "title": "F - Market & Wharves"}]
}
instance.get_route_config.return_value = {
"route": {
"stop": [
{"tag": "5650", "title": "Market St & 7th St"},
{"tag": "5651", "title": "Market St & 7th St"},
],
"direction": [
{
"name": "Outbound",
"stop": [{"tag": "5650"}],
},
{
"name": "Inbound",
"stop": [{"tag": "5651"}],
},
],
}
}
return instance
@@ -0,0 +1,162 @@
"""Test the NextBus config flow."""
from collections.abc import Generator
from unittest.mock import MagicMock, patch
import pytest
from homeassistant import config_entries, setup
from homeassistant.components.nextbus.const import (
CONF_AGENCY,
CONF_ROUTE,
CONF_STOP,
DOMAIN,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
@pytest.fixture
def mock_setup_entry() -> Generator[MagicMock, None, None]:
"""Create a mock for the nextbus component setup."""
with patch(
"homeassistant.components.nextbus.async_setup_entry",
return_value=True,
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_nextbus() -> Generator[MagicMock, None, None]:
"""Create a mock py_nextbus module."""
with patch("homeassistant.components.nextbus.config_flow.NextBusClient") as client:
yield client
async def test_import_config(
hass: HomeAssistant, mock_setup_entry: MagicMock, mock_nextbus_lists: MagicMock
) -> None:
"""Test config is imported and component set up."""
await setup.async_setup_component(hass, "persistent_notification", {})
data = {
CONF_AGENCY: "sf-muni",
CONF_ROUTE: "F",
CONF_STOP: "5650",
}
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=data,
)
await hass.async_block_till_done()
assert result.get("type") == FlowResultType.CREATE_ENTRY
assert (
result.get("title")
== "San Francisco Muni F - Market & Wharves Market St & 7th St (Outbound)"
)
assert result.get("data") == {CONF_NAME: "sf-muni F", **data}
assert len(mock_setup_entry.mock_calls) == 1
# Check duplicate entries are aborted
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=data,
)
await hass.async_block_till_done()
assert result.get("type") == FlowResultType.ABORT
assert result.get("reason") == "already_configured"
@pytest.mark.parametrize(
("override", "expected_reason"),
(
({CONF_AGENCY: "not muni"}, "invalid_agency"),
({CONF_ROUTE: "not F"}, "invalid_route"),
({CONF_STOP: "not 5650"}, "invalid_stop"),
),
)
async def test_import_config_invalid(
hass: HomeAssistant,
mock_setup_entry: MagicMock,
mock_nextbus_lists: MagicMock,
override: dict[str, str],
expected_reason: str,
) -> None:
"""Test user is redirected to user setup flow because they have invalid config."""
await setup.async_setup_component(hass, "persistent_notification", {})
data = {
CONF_AGENCY: "sf-muni",
CONF_ROUTE: "F",
CONF_STOP: "5650",
**override,
}
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=data,
)
await hass.async_block_till_done()
assert result.get("type") == FlowResultType.ABORT
assert result.get("reason") == expected_reason
async def test_user_config(
hass: HomeAssistant, mock_setup_entry: MagicMock, mock_nextbus_lists: MagicMock
) -> None:
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result.get("type") == FlowResultType.FORM
assert result.get("step_id") == "agency"
# Select agency
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_AGENCY: "sf-muni",
},
)
await hass.async_block_till_done()
assert result.get("type") == "form"
assert result.get("step_id") == "route"
# Select route
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_ROUTE: "F",
},
)
await hass.async_block_till_done()
assert result.get("type") == FlowResultType.FORM
assert result.get("step_id") == "stop"
# Select stop
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_STOP: "5650",
},
)
await hass.async_block_till_done()
assert result.get("type") == FlowResultType.CREATE_ENTRY
assert result.get("data") == {
"agency": "sf-muni",
"route": "F",
"stop": "5650",
}
assert len(mock_setup_entry.mock_calls) == 1
+139 -80
View File
@@ -1,15 +1,24 @@
"""The tests for the nexbus sensor component."""
from collections.abc import Generator
from copy import deepcopy
from unittest.mock import patch
from unittest.mock import MagicMock, patch
import pytest
import homeassistant.components.nextbus.sensor as nextbus
import homeassistant.components.sensor as sensor
from homeassistant.core import HomeAssistant
from homeassistant.components import sensor
from homeassistant.components.nextbus.const import (
CONF_AGENCY,
CONF_ROUTE,
CONF_STOP,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_NAME
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
from tests.common import assert_setup_component
from tests.common import MockConfigEntry
VALID_AGENCY = "sf-muni"
VALID_ROUTE = "F"
@@ -17,24 +26,34 @@ VALID_STOP = "5650"
VALID_AGENCY_TITLE = "San Francisco Muni"
VALID_ROUTE_TITLE = "F-Market & Wharves"
VALID_STOP_TITLE = "Market St & 7th St"
SENSOR_ID_SHORT = "sensor.sf_muni_f"
SENSOR_ID = "sensor.san_francisco_muni_f_market_wharves_market_st_7th_st"
CONFIG_BASIC = {
"sensor": {
"platform": "nextbus",
"agency": VALID_AGENCY,
"route": VALID_ROUTE,
"stop": VALID_STOP,
}
PLATFORM_CONFIG = {
sensor.DOMAIN: {
"platform": DOMAIN,
CONF_AGENCY: VALID_AGENCY,
CONF_ROUTE: VALID_ROUTE,
CONF_STOP: VALID_STOP,
},
}
CONFIG_INVALID_MISSING = {"sensor": {"platform": "nextbus"}}
CONFIG_BASIC = {
DOMAIN: {
CONF_AGENCY: VALID_AGENCY,
CONF_ROUTE: VALID_ROUTE,
CONF_STOP: VALID_STOP,
}
}
BASIC_RESULTS = {
"predictions": {
"agencyTitle": VALID_AGENCY_TITLE,
"agencyTag": VALID_AGENCY,
"routeTitle": VALID_ROUTE_TITLE,
"routeTag": VALID_ROUTE,
"stopTitle": VALID_STOP_TITLE,
"stopTag": VALID_STOP,
"direction": {
"title": "Outbound",
"prediction": [
@@ -48,24 +67,19 @@ BASIC_RESULTS = {
}
async def assert_setup_sensor(hass, config, count=1):
"""Set up the sensor and assert it's been created."""
with assert_setup_component(count):
assert await async_setup_component(hass, sensor.DOMAIN, config)
await hass.async_block_till_done()
@pytest.fixture
def mock_nextbus():
def mock_nextbus() -> Generator[MagicMock, None, None]:
"""Create a mock py_nextbus module."""
with patch(
"homeassistant.components.nextbus.sensor.NextBusClient"
) as NextBusClient:
yield NextBusClient
"homeassistant.components.nextbus.sensor.NextBusClient",
) as client:
yield client
@pytest.fixture
def mock_nextbus_predictions(mock_nextbus):
def mock_nextbus_predictions(
mock_nextbus: MagicMock,
) -> Generator[MagicMock, None, None]:
"""Create a mock of NextBusClient predictions."""
instance = mock_nextbus.return_value
instance.get_predictions_for_multi_stops.return_value = BASIC_RESULTS
@@ -73,63 +87,69 @@ def mock_nextbus_predictions(mock_nextbus):
return instance.get_predictions_for_multi_stops
@pytest.fixture
def mock_nextbus_lists(mock_nextbus):
"""Mock all list functions in nextbus to test validate logic."""
instance = mock_nextbus.return_value
instance.get_agency_list.return_value = {
"agency": [{"tag": "sf-muni", "title": "San Francisco Muni"}]
}
instance.get_route_list.return_value = {
"route": [{"tag": "F", "title": "F - Market & Wharves"}]
}
instance.get_route_config.return_value = {
"route": {"stop": [{"tag": "5650", "title": "Market St & 7th St"}]}
}
async def assert_setup_sensor(
hass: HomeAssistant,
config: dict[str, str],
expected_state=ConfigEntryState.LOADED,
) -> MockConfigEntry:
"""Set up the sensor and assert it's been created."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data=config[DOMAIN],
title=f"{VALID_AGENCY_TITLE} {VALID_ROUTE_TITLE} {VALID_STOP_TITLE}",
unique_id=f"{VALID_AGENCY}_{VALID_ROUTE}_{VALID_STOP}",
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is expected_state
return config_entry
async def test_legacy_yaml_setup(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test config setup and yaml deprecation."""
with patch(
"homeassistant.components.nextbus.config_flow.NextBusClient",
) as NextBusClient:
NextBusClient.return_value.get_predictions_for_multi_stops.return_value = (
BASIC_RESULTS
)
await async_setup_component(hass, sensor.DOMAIN, PLATFORM_CONFIG)
await hass.async_block_till_done()
issue = issue_registry.async_get_issue(
HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}"
)
assert issue
async def test_valid_config(
hass: HomeAssistant, mock_nextbus, mock_nextbus_lists
hass: HomeAssistant, mock_nextbus: MagicMock, mock_nextbus_lists: MagicMock
) -> None:
"""Test that sensor is set up properly with valid config."""
await assert_setup_sensor(hass, CONFIG_BASIC)
async def test_invalid_config(
hass: HomeAssistant, mock_nextbus, mock_nextbus_lists
) -> None:
"""Checks that component is not setup when missing information."""
await assert_setup_sensor(hass, CONFIG_INVALID_MISSING, count=0)
async def test_validate_tags(
hass: HomeAssistant, mock_nextbus, mock_nextbus_lists
) -> None:
"""Test that additional validation against the API is successful."""
# with self.subTest('Valid everything'):
assert nextbus.validate_tags(mock_nextbus(), VALID_AGENCY, VALID_ROUTE, VALID_STOP)
# with self.subTest('Invalid agency'):
assert not nextbus.validate_tags(
mock_nextbus(), "not-valid", VALID_ROUTE, VALID_STOP
)
# with self.subTest('Invalid route'):
assert not nextbus.validate_tags(mock_nextbus(), VALID_AGENCY, "0", VALID_STOP)
# with self.subTest('Invalid stop'):
assert not nextbus.validate_tags(mock_nextbus(), VALID_AGENCY, VALID_ROUTE, 0)
async def test_verify_valid_state(
hass: HomeAssistant, mock_nextbus, mock_nextbus_lists, mock_nextbus_predictions
hass: HomeAssistant,
mock_nextbus: MagicMock,
mock_nextbus_lists: MagicMock,
mock_nextbus_predictions: MagicMock,
) -> None:
"""Verify all attributes are set from a valid response."""
await assert_setup_sensor(hass, CONFIG_BASIC)
mock_nextbus_predictions.assert_called_once_with(
[{"stop_tag": VALID_STOP, "route_tag": VALID_ROUTE}], VALID_AGENCY
)
state = hass.states.get(SENSOR_ID_SHORT)
state = hass.states.get(SENSOR_ID)
assert state is not None
assert state.state == "2019-03-28T21:09:31+00:00"
assert state.attributes["agency"] == VALID_AGENCY_TITLE
@@ -140,14 +160,20 @@ async def test_verify_valid_state(
async def test_message_dict(
hass: HomeAssistant, mock_nextbus, mock_nextbus_lists, mock_nextbus_predictions
hass: HomeAssistant,
mock_nextbus: MagicMock,
mock_nextbus_lists: MagicMock,
mock_nextbus_predictions: MagicMock,
) -> None:
"""Verify that a single dict message is rendered correctly."""
mock_nextbus_predictions.return_value = {
"predictions": {
"agencyTitle": VALID_AGENCY_TITLE,
"agencyTag": VALID_AGENCY,
"routeTitle": VALID_ROUTE_TITLE,
"routeTag": VALID_ROUTE,
"stopTitle": VALID_STOP_TITLE,
"stopTag": VALID_STOP,
"message": {"text": "Message"},
"direction": {
"title": "Outbound",
@@ -162,20 +188,26 @@ async def test_message_dict(
await assert_setup_sensor(hass, CONFIG_BASIC)
state = hass.states.get(SENSOR_ID_SHORT)
state = hass.states.get(SENSOR_ID)
assert state is not None
assert state.attributes["message"] == "Message"
async def test_message_list(
hass: HomeAssistant, mock_nextbus, mock_nextbus_lists, mock_nextbus_predictions
hass: HomeAssistant,
mock_nextbus: MagicMock,
mock_nextbus_lists: MagicMock,
mock_nextbus_predictions: MagicMock,
) -> None:
"""Verify that a list of messages are rendered correctly."""
mock_nextbus_predictions.return_value = {
"predictions": {
"agencyTitle": VALID_AGENCY_TITLE,
"agencyTag": VALID_AGENCY,
"routeTitle": VALID_ROUTE_TITLE,
"routeTag": VALID_ROUTE,
"stopTitle": VALID_STOP_TITLE,
"stopTag": VALID_STOP,
"message": [{"text": "Message 1"}, {"text": "Message 2"}],
"direction": {
"title": "Outbound",
@@ -190,20 +222,26 @@ async def test_message_list(
await assert_setup_sensor(hass, CONFIG_BASIC)
state = hass.states.get(SENSOR_ID_SHORT)
state = hass.states.get(SENSOR_ID)
assert state is not None
assert state.attributes["message"] == "Message 1 -- Message 2"
async def test_direction_list(
hass: HomeAssistant, mock_nextbus, mock_nextbus_lists, mock_nextbus_predictions
hass: HomeAssistant,
mock_nextbus: MagicMock,
mock_nextbus_lists: MagicMock,
mock_nextbus_predictions: MagicMock,
) -> None:
"""Verify that a list of messages are rendered correctly."""
mock_nextbus_predictions.return_value = {
"predictions": {
"agencyTitle": VALID_AGENCY_TITLE,
"agencyTag": VALID_AGENCY,
"routeTitle": VALID_ROUTE_TITLE,
"routeTag": VALID_ROUTE,
"stopTitle": VALID_STOP_TITLE,
"stopTag": VALID_STOP,
"message": [{"text": "Message 1"}, {"text": "Message 2"}],
"direction": [
{
@@ -224,7 +262,7 @@ async def test_direction_list(
await assert_setup_sensor(hass, CONFIG_BASIC)
state = hass.states.get(SENSOR_ID_SHORT)
state = hass.states.get(SENSOR_ID)
assert state is not None
assert state.state == "2019-03-28T21:09:31+00:00"
assert state.attributes["agency"] == VALID_AGENCY_TITLE
@@ -235,46 +273,67 @@ async def test_direction_list(
async def test_custom_name(
hass: HomeAssistant, mock_nextbus, mock_nextbus_lists, mock_nextbus_predictions
hass: HomeAssistant,
mock_nextbus: MagicMock,
mock_nextbus_lists: MagicMock,
mock_nextbus_predictions: MagicMock,
) -> None:
"""Verify that a custom name can be set via config."""
config = deepcopy(CONFIG_BASIC)
config["sensor"]["name"] = "Custom Name"
config[DOMAIN][CONF_NAME] = "Custom Name"
await assert_setup_sensor(hass, config)
state = hass.states.get("sensor.custom_name")
assert state is not None
assert state.name == "Custom Name"
@pytest.mark.parametrize(
"prediction_results",
(
{},
{"Error": "Failed"},
),
)
async def test_no_predictions(
hass: HomeAssistant, mock_nextbus, mock_nextbus_predictions, mock_nextbus_lists
hass: HomeAssistant,
mock_nextbus: MagicMock,
mock_nextbus_predictions: MagicMock,
mock_nextbus_lists: MagicMock,
prediction_results: dict[str, str],
) -> None:
"""Verify there are no exceptions when no predictions are returned."""
mock_nextbus_predictions.return_value = {}
mock_nextbus_predictions.return_value = prediction_results
await assert_setup_sensor(hass, CONFIG_BASIC)
state = hass.states.get(SENSOR_ID_SHORT)
state = hass.states.get(SENSOR_ID)
assert state is not None
assert state.state == "unknown"
async def test_verify_no_upcoming(
hass: HomeAssistant, mock_nextbus, mock_nextbus_lists, mock_nextbus_predictions
hass: HomeAssistant,
mock_nextbus: MagicMock,
mock_nextbus_lists: MagicMock,
mock_nextbus_predictions: MagicMock,
) -> None:
"""Verify attributes are set despite no upcoming times."""
mock_nextbus_predictions.return_value = {
"predictions": {
"agencyTitle": VALID_AGENCY_TITLE,
"agencyTag": VALID_AGENCY,
"routeTitle": VALID_ROUTE_TITLE,
"routeTag": VALID_ROUTE,
"stopTitle": VALID_STOP_TITLE,
"stopTag": VALID_STOP,
"direction": {"title": "Outbound", "prediction": []},
}
}
await assert_setup_sensor(hass, CONFIG_BASIC)
state = hass.states.get(SENSOR_ID_SHORT)
state = hass.states.get(SENSOR_ID)
assert state is not None
assert state.state == "unknown"
assert state.attributes["upcoming"] == "No upcoming predictions"
+34
View File
@@ -0,0 +1,34 @@
"""Test NextBus util functions."""
from typing import Any
import pytest
from homeassistant.components.nextbus.util import listify, maybe_first
@pytest.mark.parametrize(
("input", "expected"),
(
("foo", ["foo"]),
(["foo"], ["foo"]),
(None, []),
),
)
def test_listify(input: Any, expected: list[Any]) -> None:
"""Test input listification."""
assert listify(input) == expected
@pytest.mark.parametrize(
("input", "expected"),
(
([], []),
(None, None),
("test", "test"),
(["test"], "test"),
(["test", "second"], "test"),
),
)
def test_maybe_first(input: list[Any] | None, expected: Any) -> None:
"""Test maybe getting the first thing from a list."""
assert maybe_first(input) == expected