Add ConfigFlow for seventeentrack integration ()

* Add config flow to 17Track

* Import config from configuration.yaml

* 1. move import to async_setup_platform
2. add USERNAME (email) in title for uniqueness

* Add options flow

* Add tests

* Add CONF_SHOW_ARCHIVED  and CONF_SHOW_DELIVERED to data from options

* Update homeassistant/components/seventeentrack/__init__.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/seventeentrack/__init__.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/seventeentrack/config_flow.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/seventeentrack/manifest.json

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/seventeentrack/config_flow.py

Co-authored-by: Christopher Fenner <9592452+CFenner@users.noreply.github.com>

* Update homeassistant/components/seventeentrack/__init__.py

Co-authored-by: Christopher Fenner <9592452+CFenner@users.noreply.github.com>

* Update homeassistant/components/seventeentrack/sensor.py

Co-authored-by: Christopher Fenner <9592452+CFenner@users.noreply.github.com>

* 1. Added repair issues
2. _async_validate_input inlined
3. added unique id
4. take default scan interval

* fix

* 1. move async_create_issue to async_setup_platform
2. fix tests
3. black + pylint

* combine USER_SCHEMA and OPTIONS_SCHEMA

* small fix

* remove async_setup

* fix tests and add 100% coverage

* 1. remove CONFIG_SCHEMA
2. remove error log
3. add issue with more description when import issues happen
4. some linting

* Update homeassistant/components/seventeentrack/config_flow.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/seventeentrack/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/seventeentrack/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/seventeentrack/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* use freezer
use AsyncMock
fix tests

* add test_flow_fails
parametrize tests where needed
test_import_flow_already_configured - where a unique id already configured (abort flow)

* lint

* fix rebase issues

* some more fix

* 17Track revert tests and put them in a different PR

* adapt tests to MockConfigEntry

* Update tests/components/seventeentrack/test_sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/seventeentrack/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/seventeentrack/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update tests/components/seventeentrack/__init__.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* 1. create fixture for config and another with options
2. set options with default values
3. remove CONFIG_SCHEMA

* Update tests/components/seventeentrack/conftest.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update tests/components/seventeentrack/conftest.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* 1. get options from import data and default if not present
2. rename mock_config_entry_no_options -> mock_config_entry_with_default_options

* move ACCOUNT_ID to mock_seventeentrack_api.return_value.profile.account_id

* Apply suggestions from code review

* Update tests/components/seventeentrack/test_config_flow.py

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Christopher Fenner <9592452+CFenner@users.noreply.github.com>
pull/112987/head^2
Shai Ungar 2024-03-11 13:47:39 +02:00 committed by GitHub
parent 564c31e846
commit 3c06fbbd82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 714 additions and 143 deletions

View File

@ -1190,6 +1190,8 @@ build.json @home-assistant/supervisor
/tests/components/senz/ @milanmeu
/homeassistant/components/serial/ @fabaff
/homeassistant/components/seven_segments/ @fabaff
/homeassistant/components/seventeentrack/ @shaiu
/tests/components/seventeentrack/ @shaiu
/homeassistant/components/sfr_box/ @epenet
/tests/components/sfr_box/ @epenet
/homeassistant/components/sharkiq/ @JeffResc @funkybunch

View File

@ -1 +1,32 @@
"""The seventeentrack component."""
from py17track import Client as SeventeenTrackClient
from py17track.errors import SeventeenTrackError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up 17Track from a config entry."""
session = async_get_clientsession(hass)
client = SeventeenTrackClient(session=session)
try:
await client.profile.login(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD])
except SeventeenTrackError as err:
raise ConfigEntryNotReady from err
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = client
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True

View File

@ -0,0 +1,137 @@
"""Adds config flow for 17track.net."""
from __future__ import annotations
import logging
from typing import Any
from py17track import Client as SeventeenTrackClient
from py17track.errors import SeventeenTrackError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.schema_config_entry_flow import (
SchemaFlowFormStep,
SchemaOptionsFlowHandler,
)
from .const import (
CONF_SHOW_ARCHIVED,
CONF_SHOW_DELIVERED,
DEFAULT_SHOW_ARCHIVED,
DEFAULT_SHOW_DELIVERED,
DOMAIN,
)
CONF_SHOW = {
vol.Optional(CONF_SHOW_ARCHIVED, default=DEFAULT_SHOW_ARCHIVED): bool,
vol.Optional(CONF_SHOW_DELIVERED, default=DEFAULT_SHOW_DELIVERED): bool,
}
_LOGGER = logging.getLogger(__name__)
OPTIONS_SCHEMA = vol.Schema(CONF_SHOW)
OPTIONS_FLOW = {
"init": SchemaFlowFormStep(OPTIONS_SCHEMA),
}
USER_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
class SeventeenTrackConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""17track config flow."""
VERSION = 1
@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> SchemaOptionsFlowHandler:
"""Get options flow for this handler."""
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors = {}
if user_input:
client = await self._get_client()
try:
if not await client.profile.login(
user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
):
errors["base"] = "invalid_auth"
except SeventeenTrackError as err:
_LOGGER.error("There was an error while logging in: %s", err)
errors["base"] = "cannot_connect"
if not errors:
account_id = client.profile.account_id
await self.async_set_unique_id(account_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_USERNAME],
data=user_input,
options={
CONF_SHOW_ARCHIVED: DEFAULT_SHOW_ARCHIVED,
CONF_SHOW_DELIVERED: DEFAULT_SHOW_DELIVERED,
},
)
return self.async_show_form(
step_id="user",
data_schema=USER_SCHEMA,
errors=errors,
)
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Import 17Track config from configuration.yaml."""
client = await self._get_client()
try:
login_result = await client.profile.login(
import_data[CONF_USERNAME], import_data[CONF_PASSWORD]
)
except SeventeenTrackError as err:
_LOGGER.error("There was an error while logging in: %s", err)
return self.async_abort(reason="cannot_connect")
if not login_result:
_LOGGER.error("Invalid username and password provided")
return self.async_abort(reason="invalid_credentials")
account_id = client.profile.account_id
await self.async_set_unique_id(account_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=import_data[CONF_USERNAME],
data=import_data,
options={
CONF_SHOW_ARCHIVED: import_data.get(
CONF_SHOW_ARCHIVED, DEFAULT_SHOW_ARCHIVED
),
CONF_SHOW_DELIVERED: import_data.get(
CONF_SHOW_DELIVERED, DEFAULT_SHOW_DELIVERED
),
},
)
async def _get_client(self):
session = aiohttp_client.async_get_clientsession(self.hass)
return SeventeenTrackClient(session=session)

View File

@ -0,0 +1,39 @@
"""Constants for the 17track.net component."""
from datetime import timedelta
ATTR_DESTINATION_COUNTRY = "destination_country"
ATTR_INFO_TEXT = "info_text"
ATTR_TIMESTAMP = "timestamp"
ATTR_ORIGIN_COUNTRY = "origin_country"
ATTR_PACKAGES = "packages"
ATTR_PACKAGE_TYPE = "package_type"
ATTR_STATUS = "status"
ATTR_TRACKING_INFO_LANGUAGE = "tracking_info_language"
ATTR_TRACKING_NUMBER = "tracking_number"
CONF_SHOW_ARCHIVED = "show_archived"
CONF_SHOW_DELIVERED = "show_delivered"
DEFAULT_SHOW_ARCHIVED = False
DEFAULT_SHOW_DELIVERED = False
DOMAIN = "seventeentrack"
DATA_PACKAGES = "package_data"
DATA_SUMMARY = "summary_data"
ATTRIBUTION = "Data provided by 17track.net"
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
UNIQUE_ID_TEMPLATE = "package_{0}_{1}"
ENTITY_ID_TEMPLATE = "sensor.seventeentrack_package_{0}"
NOTIFICATION_DELIVERED_ID = "package_delivered_{0}"
NOTIFICATION_DELIVERED_TITLE = "Package {0} delivered"
NOTIFICATION_DELIVERED_MESSAGE = (
"Package Delivered: {0}<br />Visit 17.track for more information: "
"https://t.17track.net/track#nums={1}"
)
VALUE_DELIVERED = "Delivered"

View File

@ -1,8 +1,10 @@
{
"domain": "seventeentrack",
"name": "17TRACK",
"codeowners": [],
"codeowners": ["@shaiu"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/seventeentrack",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["py17track"],
"requirements": ["py17track==2021.12.2"]

View File

@ -2,66 +2,53 @@
from __future__ import annotations
from datetime import timedelta
import logging
from py17track import Client as SeventeenTrackClient
from py17track.errors import SeventeenTrackError
from py17track.package import Package
import voluptuous as vol
from homeassistant.components import persistent_notification
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
ATTR_FRIENDLY_NAME,
ATTR_LOCATION,
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import (
aiohttp_client,
config_validation as cv,
entity,
entity_registry as er,
)
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_validation as cv, entity, entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from homeassistant.util import Throttle, slugify
_LOGGER = logging.getLogger(__name__)
ATTR_DESTINATION_COUNTRY = "destination_country"
ATTR_INFO_TEXT = "info_text"
ATTR_TIMESTAMP = "timestamp"
ATTR_ORIGIN_COUNTRY = "origin_country"
ATTR_PACKAGES = "packages"
ATTR_PACKAGE_TYPE = "package_type"
ATTR_STATUS = "status"
ATTR_TRACKING_INFO_LANGUAGE = "tracking_info_language"
ATTR_TRACKING_NUMBER = "tracking_number"
CONF_SHOW_ARCHIVED = "show_archived"
CONF_SHOW_DELIVERED = "show_delivered"
DATA_PACKAGES = "package_data"
DATA_SUMMARY = "summary_data"
ATTRIBUTION = "Data provided by 17track.net"
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
UNIQUE_ID_TEMPLATE = "package_{0}_{1}"
ENTITY_ID_TEMPLATE = "sensor.seventeentrack_package_{0}"
NOTIFICATION_DELIVERED_ID = "package_delivered_{0}"
NOTIFICATION_DELIVERED_TITLE = "Package {0} delivered"
NOTIFICATION_DELIVERED_MESSAGE = (
"Package Delivered: {0}<br />Visit 17.track for more information: "
"https://t.17track.net/track#nums={1}"
from .const import (
ATTR_DESTINATION_COUNTRY,
ATTR_INFO_TEXT,
ATTR_ORIGIN_COUNTRY,
ATTR_PACKAGE_TYPE,
ATTR_PACKAGES,
ATTR_STATUS,
ATTR_TIMESTAMP,
ATTR_TRACKING_INFO_LANGUAGE,
ATTR_TRACKING_NUMBER,
ATTRIBUTION,
CONF_SHOW_ARCHIVED,
CONF_SHOW_DELIVERED,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
ENTITY_ID_TEMPLATE,
NOTIFICATION_DELIVERED_MESSAGE,
NOTIFICATION_DELIVERED_TITLE,
UNIQUE_ID_TEMPLATE,
VALUE_DELIVERED,
)
VALUE_DELIVERED = "Delivered"
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@ -72,6 +59,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
}
)
ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=seventeentrack"}
async def async_setup_platform(
hass: HomeAssistant,
@ -79,32 +68,57 @@ async def async_setup_platform(
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Configure the platform and add the sensors."""
"""Initialize 17Track import from config."""
session = aiohttp_client.async_get_clientsession(hass)
client = SeventeenTrackClient(session=session)
try:
login_result = await client.profile.login(
config[CONF_USERNAME], config[CONF_PASSWORD]
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
)
if (
result["type"] == FlowResultType.CREATE_ENTRY
or result["reason"] == "already_configured"
):
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
is_fixable=False,
breaks_in_ha_version="2024.10.0",
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "17Track",
},
)
else:
async_create_issue(
hass,
DOMAIN,
f"deprecated_yaml_import_issue_${result['reason']}",
breaks_in_ha_version="2024.10.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key=f"deprecated_yaml_import_issue_${result['reason']}",
translation_placeholders=ISSUE_PLACEHOLDER,
)
if not login_result:
_LOGGER.error("Invalid username and password provided")
return
except SeventeenTrackError as err:
_LOGGER.error("There was an error while logging in: %s", err)
return
scan_interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a 17Track sensor entry."""
client = hass.data[DOMAIN][config_entry.entry_id]
data = SeventeenTrackData(
client,
async_add_entities,
scan_interval,
config[CONF_SHOW_ARCHIVED],
config[CONF_SHOW_DELIVERED],
DEFAULT_SCAN_INTERVAL,
config_entry.options[CONF_SHOW_ARCHIVED],
config_entry.options[CONF_SHOW_DELIVERED],
str(hass.config.time_zone),
)
await data.async_update()
@ -117,7 +131,7 @@ class SeventeenTrackSummarySensor(SensorEntity):
_attr_icon = "mdi:package"
_attr_native_unit_of_measurement = "packages"
def __init__(self, data, status, initial_state):
def __init__(self, data, status, initial_state) -> None:
"""Initialize."""
self._attr_extra_state_attributes = {}
self._data = data
@ -127,12 +141,12 @@ class SeventeenTrackSummarySensor(SensorEntity):
self._attr_unique_id = f"summary_{data.account_id}_{slugify(status)}"
@property
def available(self):
def available(self) -> bool:
"""Return whether the entity is available."""
return self._state is not None
@property
def native_value(self):
def native_value(self) -> StateType:
"""Return the state."""
return self._state
@ -169,7 +183,7 @@ class SeventeenTrackPackageSensor(SensorEntity):
_attr_attribution = ATTRIBUTION
_attr_icon = "mdi:package"
def __init__(self, data, package):
def __init__(self, data, package) -> None:
"""Initialize."""
self._attr_extra_state_attributes = {
ATTR_DESTINATION_COUNTRY: package.destination_country,
@ -196,14 +210,14 @@ class SeventeenTrackPackageSensor(SensorEntity):
return self._data.packages.get(self._tracking_number) is not None
@property
def name(self):
def name(self) -> str:
"""Return the name."""
if not (name := self._friendly_name):
name = self._tracking_number
return f"Seventeentrack Package: {name}"
@property
def native_value(self):
def native_value(self) -> StateType:
"""Return the state."""
return self._state
@ -278,18 +292,17 @@ class SeventeenTrackData:
show_archived,
show_delivered,
timezone,
):
) -> None:
"""Initialize."""
self._async_add_entities = async_add_entities
self._client = client
self._scan_interval = scan_interval
self._show_archived = show_archived
self.account_id = client.profile.account_id
self.packages = {}
self.packages: dict[str, Package] = {}
self.show_delivered = show_delivered
self.timezone = timezone
self.summary = {}
self.summary: dict[str, int] = {}
self.async_update = Throttle(self._scan_interval)(self._async_update)
self.first_update = True

View File

@ -0,0 +1,40 @@
{
"config": {
"step": {
"user": {
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"options": {
"step": {
"init": {
"description": "Configure general settings",
"data": {
"show_archived": "Whether sensors should be created for archived packages",
"show_delivered": "Whether sensors should be created for delivered packages"
}
}
}
},
"issues": {
"deprecated_yaml_import_issue_cannot_connect": {
"title": "The 17Track YAML configuration import cannot connect to server",
"description": "Configuring 17Track using YAML is being removed but there was a connection error importing your YAML configuration.\n\nThings you can try:\nMake sure your home assistant can reach the web.\n\nThen restart Home Assistant to try importing this integration again.\n\nAlternatively, you may remove the 17Track configuration from your YAML configuration entirely, restart Home Assistant, and add the 17Track integration manually."
},
"deprecated_yaml_import_issue_invalid_credentials": {
"title": "The 17Track YAML configuration import request failed due to invalid credentials",
"description": "Configuring 17Track using YAML is being removed but there were invalid credentials provided while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your 17Track credentials are correct and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the 17Track configuration from your YAML configuration entirely, restart Home Assistant, and add the 17Track integration manually."
}
}
}

View File

@ -458,6 +458,7 @@ FLOWS = {
"sensorpush",
"sentry",
"senz",
"seventeentrack",
"sfr_box",
"sharkiq",
"shelly",

View File

@ -5271,8 +5271,8 @@
},
"seventeentrack": {
"name": "17TRACK",
"integration_type": "hub",
"config_flow": false,
"integration_type": "service",
"config_flow": true,
"iot_class": "cloud_polling"
},
"sfr_box": {

View File

@ -6,15 +6,18 @@ from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.seventeentrack.sensor import DEFAULT_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import async_setup_component
from tests.common import async_fire_time_changed
from tests.common import MockConfigEntry, async_fire_time_changed
async def init_integration(hass: HomeAssistant, config: ConfigType):
"""Set up the seventeentrack integration in Home Assistant."""
assert await async_setup_component(hass, "sensor", config)
async def init_integration(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Set up the 17Track integration in Home Assistant."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

View File

@ -1,46 +1,23 @@
"""Configuration for 17Track tests."""
from collections.abc import Generator
from typing import Optional
from unittest.mock import AsyncMock, patch
from py17track.package import Package
import pytest
from homeassistant.components.seventeentrack.const import (
DEFAULT_SHOW_ARCHIVED,
DEFAULT_SHOW_DELIVERED,
)
from homeassistant.components.seventeentrack.sensor import (
CONF_SHOW_ARCHIVED,
CONF_SHOW_DELIVERED,
)
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
VALID_CONFIG_MINIMAL = {
"sensor": {
"platform": "seventeentrack",
CONF_USERNAME: "test",
CONF_PASSWORD: "test",
}
}
INVALID_CONFIG = {"sensor": {"platform": "seventeentrack", "boom": "test"}}
VALID_CONFIG_FULL = {
"sensor": {
"platform": "seventeentrack",
CONF_USERNAME: "test",
CONF_PASSWORD: "test",
CONF_SHOW_ARCHIVED: True,
CONF_SHOW_DELIVERED: True,
}
}
VALID_CONFIG_FULL_NO_DELIVERED = {
"sensor": {
"platform": "seventeentrack",
CONF_USERNAME: "test",
CONF_PASSWORD: "test",
CONF_SHOW_ARCHIVED: False,
CONF_SHOW_DELIVERED: False,
}
}
from tests.common import MockConfigEntry
DEFAULT_SUMMARY = {
"Not Found": 0,
@ -52,6 +29,8 @@ DEFAULT_SUMMARY = {
"Returned": 0,
}
ACCOUNT_ID = "1234"
NEW_SUMMARY_DATA = {
"Not Found": 1,
"In Transit": 1,
@ -62,6 +41,67 @@ NEW_SUMMARY_DATA = {
"Returned": 1,
}
VALID_CONFIG = {
CONF_USERNAME: "test",
CONF_PASSWORD: "test",
}
INVALID_CONFIG = {"notusername": "seventeentrack", "notpassword": "test"}
VALID_OPTIONS = {
CONF_SHOW_ARCHIVED: True,
CONF_SHOW_DELIVERED: True,
}
NO_DELIVERED_OPTIONS = {
CONF_SHOW_ARCHIVED: False,
CONF_SHOW_DELIVERED: False,
}
VALID_PLATFORM_CONFIG_FULL = {
"sensor": {
"platform": "seventeentrack",
CONF_USERNAME: "test",
CONF_PASSWORD: "test",
CONF_SHOW_ARCHIVED: True,
CONF_SHOW_DELIVERED: True,
}
}
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.seventeentrack.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
domain="seventeentrack",
data=VALID_CONFIG,
options=VALID_OPTIONS,
unique_id=ACCOUNT_ID,
)
@pytest.fixture
def mock_config_entry_with_default_options() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
domain="seventeentrack",
data=VALID_CONFIG,
options={
CONF_SHOW_ARCHIVED: DEFAULT_SHOW_ARCHIVED,
CONF_SHOW_DELIVERED: DEFAULT_SHOW_DELIVERED,
},
unique_id=ACCOUNT_ID,
)
@pytest.fixture
def mock_seventeentrack():
@ -69,10 +109,15 @@ def mock_seventeentrack():
mock_seventeentrack_api = AsyncMock()
with (
patch(
"homeassistant.components.seventeentrack.sensor.SeventeenTrackClient",
"homeassistant.components.seventeentrack.SeventeenTrackClient",
return_value=mock_seventeentrack_api,
),
patch(
"homeassistant.components.seventeentrack.config_flow.SeventeenTrackClient",
return_value=mock_seventeentrack_api,
) as mock_seventeentrack_api,
):
mock_seventeentrack_api.return_value.profile.account_id = ACCOUNT_ID
mock_seventeentrack_api.return_value.profile.login.return_value = True
mock_seventeentrack_api.return_value.profile.packages.return_value = []
mock_seventeentrack_api.return_value.profile.summary.return_value = (

View File

@ -0,0 +1,208 @@
"""Define tests for the 17Track config flow."""
from unittest.mock import AsyncMock
from py17track.errors import SeventeenTrackError
import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.seventeentrack import DOMAIN
from homeassistant.components.seventeentrack.const import (
CONF_SHOW_ARCHIVED,
CONF_SHOW_DELIVERED,
)
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
ACCOUNT_ID = "1234"
VALID_CONFIG = {
CONF_USERNAME: "someemail@gmail.com",
CONF_PASSWORD: "edc3eee7330e4fdda04489e3fbc283d0",
}
VALID_CONFIG_OLD = {
CONF_USERNAME: "someemail@gmail.com",
CONF_PASSWORD: "edc3eee7330e4fdda04489e3fbc283d0",
}
async def test_create_entry(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_seventeentrack: AsyncMock
) -> None:
"""Test that the user step works."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
VALID_CONFIG,
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result2["title"] == "someemail@gmail.com"
assert result2["data"] == {
CONF_PASSWORD: "edc3eee7330e4fdda04489e3fbc283d0",
CONF_USERNAME: "someemail@gmail.com",
}
@pytest.mark.parametrize(
("return_value", "side_effect", "error"),
[
(
False,
None,
"invalid_auth",
),
(
True,
SeventeenTrackError(),
"cannot_connect",
),
],
)
async def test_flow_fails(
hass: HomeAssistant,
mock_seventeentrack: AsyncMock,
return_value,
side_effect,
error,
) -> None:
"""Test that the user step fails."""
mock_seventeentrack.return_value.profile.login.return_value = return_value
mock_seventeentrack.return_value.profile.login.side_effect = side_effect
failed_result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
data=VALID_CONFIG,
)
assert failed_result["errors"] == {"base": error}
mock_seventeentrack.return_value.profile.login.return_value = True
mock_seventeentrack.return_value.profile.login.side_effect = None
result = await hass.config_entries.flow.async_configure(
failed_result["flow_id"],
VALID_CONFIG,
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["title"] == "someemail@gmail.com"
assert result["data"] == {
CONF_PASSWORD: "edc3eee7330e4fdda04489e3fbc283d0",
CONF_USERNAME: "someemail@gmail.com",
}
async def test_import_flow(hass: HomeAssistant, mock_seventeentrack: AsyncMock) -> None:
"""Test the import configuration flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=VALID_CONFIG_OLD,
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["title"] == "someemail@gmail.com"
assert result["data"][CONF_USERNAME] == "someemail@gmail.com"
assert result["data"][CONF_PASSWORD] == "edc3eee7330e4fdda04489e3fbc283d0"
@pytest.mark.parametrize(
("return_value", "side_effect", "error"),
[
(
False,
None,
"invalid_credentials",
),
(
True,
SeventeenTrackError(),
"cannot_connect",
),
],
)
async def test_import_flow_cannot_connect_error(
hass: HomeAssistant,
mock_seventeentrack: AsyncMock,
return_value,
side_effect,
error,
) -> None:
"""Test the import configuration flow with error."""
mock_seventeentrack.return_value.profile.login.return_value = return_value
mock_seventeentrack.return_value.profile.login.side_effect = side_effect
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=VALID_CONFIG_OLD,
)
assert result["type"] == data_entry_flow.FlowResultType.ABORT
assert result["reason"] == error
async def test_option_flow(hass: HomeAssistant, mock_seventeentrack: AsyncMock) -> None:
"""Test option flow."""
entry = MockConfigEntry(
domain=DOMAIN,
data=VALID_CONFIG,
options={
CONF_SHOW_ARCHIVED: False,
CONF_SHOW_DELIVERED: False,
},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(entry.entry_id)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={CONF_SHOW_ARCHIVED: True, CONF_SHOW_DELIVERED: False},
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"][CONF_SHOW_ARCHIVED]
assert not result["data"][CONF_SHOW_DELIVERED]
async def test_import_flow_already_configured(
hass: HomeAssistant, mock_seventeentrack: AsyncMock
) -> None:
"""Test the import configuration flow with error."""
entry = MockConfigEntry(
domain=DOMAIN,
data=VALID_CONFIG,
unique_id=ACCOUNT_ID,
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result_aborted = await hass.config_entries.flow.async_configure(
result["flow_id"],
VALID_CONFIG,
)
await hass.async_block_till_done()
assert result_aborted["type"] == data_entry_flow.FlowResultType.ABORT
assert result_aborted["reason"] == "already_configured"

View File

@ -8,61 +8,73 @@ from freezegun.api import FrozenDateTimeFactory
from py17track.errors import SeventeenTrackError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.issue_registry import IssueRegistry
from homeassistant.setup import async_setup_component
from . import goto_future, init_integration
from .conftest import (
DEFAULT_SUMMARY,
INVALID_CONFIG,
NEW_SUMMARY_DATA,
VALID_CONFIG_FULL,
VALID_CONFIG_FULL_NO_DELIVERED,
VALID_CONFIG_MINIMAL,
VALID_PLATFORM_CONFIG_FULL,
get_package,
)
from tests.common import MockConfigEntry
async def test_full_valid_config(
hass: HomeAssistant, mock_seventeentrack: AsyncMock
hass: HomeAssistant,
mock_seventeentrack: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure everything starts correctly."""
await init_integration(hass, VALID_CONFIG_MINIMAL)
await init_integration(hass, mock_config_entry)
assert len(hass.states.async_entity_ids()) == len(DEFAULT_SUMMARY.keys())
async def test_valid_config(
hass: HomeAssistant, mock_seventeentrack: AsyncMock
hass: HomeAssistant,
mock_seventeentrack: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure everything starts correctly."""
await init_integration(hass, VALID_CONFIG_FULL)
await init_integration(hass, mock_config_entry)
assert len(hass.states.async_entity_ids()) == len(DEFAULT_SUMMARY.keys())
async def test_invalid_config(hass: HomeAssistant) -> None:
async def test_invalid_config(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Ensure nothing is created when config is wrong."""
await init_integration(hass, INVALID_CONFIG)
await init_integration(hass, mock_config_entry)
assert not hass.states.async_entity_ids("sensor")
async def test_login_exception(
hass: HomeAssistant, mock_seventeentrack: AsyncMock
hass: HomeAssistant,
mock_seventeentrack: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure everything starts correctly."""
mock_seventeentrack.return_value.profile.login.side_effect = SeventeenTrackError(
"Error"
)
await init_integration(hass, VALID_CONFIG_FULL)
await init_integration(hass, mock_config_entry)
assert not hass.states.async_entity_ids("sensor")
async def test_add_package(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_seventeentrack: AsyncMock
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_seventeentrack: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure package is added correctly when user add a new package."""
package = get_package()
mock_seventeentrack.return_value.profile.packages.return_value = [package]
mock_seventeentrack.return_value.profile.summary.return_value = {}
await init_integration(hass, VALID_CONFIG_FULL)
await init_integration(hass, mock_config_entry)
assert hass.states.get("sensor.seventeentrack_package_456") is not None
assert len(hass.states.async_entity_ids()) == 1
@ -82,14 +94,16 @@ async def test_add_package(
async def test_add_package_default_friendly_name(
hass: HomeAssistant, mock_seventeentrack: AsyncMock
hass: HomeAssistant,
mock_seventeentrack: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure package is added correctly with default friendly name when user add a new package without his own friendly name."""
package = get_package(friendly_name=None)
mock_seventeentrack.return_value.profile.packages.return_value = [package]
mock_seventeentrack.return_value.profile.summary.return_value = {}
await init_integration(hass, VALID_CONFIG_FULL)
await init_integration(hass, mock_config_entry)
state_456 = hass.states.get("sensor.seventeentrack_package_456")
assert state_456 is not None
assert state_456.attributes["friendly_name"] == "Seventeentrack Package: 456"
@ -97,7 +111,10 @@ async def test_add_package_default_friendly_name(
async def test_remove_package(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_seventeentrack: AsyncMock
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_seventeentrack: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure entity is not there anymore if package is not there."""
package1 = get_package()
@ -115,7 +132,7 @@ async def test_remove_package(
]
mock_seventeentrack.return_value.profile.summary.return_value = {}
await init_integration(hass, VALID_CONFIG_FULL)
await init_integration(hass, mock_config_entry)
assert hass.states.get("sensor.seventeentrack_package_456") is not None
assert hass.states.get("sensor.seventeentrack_package_789") is not None
@ -136,7 +153,9 @@ async def test_remove_package(
async def test_package_error(
hass: HomeAssistant, mock_seventeentrack: AsyncMock
hass: HomeAssistant,
mock_seventeentrack: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure package is added correctly when user add a new package."""
mock_seventeentrack.return_value.profile.packages.side_effect = SeventeenTrackError(
@ -144,19 +163,22 @@ async def test_package_error(
)
mock_seventeentrack.return_value.profile.summary.return_value = {}
await init_integration(hass, VALID_CONFIG_FULL)
await init_integration(hass, mock_config_entry)
assert hass.states.get("sensor.seventeentrack_package_456") is None
async def test_friendly_name_changed(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_seventeentrack: AsyncMock
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_seventeentrack: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test friendly name change."""
package = get_package()
mock_seventeentrack.return_value.profile.packages.return_value = [package]
mock_seventeentrack.return_value.profile.summary.return_value = {}
await init_integration(hass, VALID_CONFIG_FULL)
await init_integration(hass, mock_config_entry)
assert hass.states.get("sensor.seventeentrack_package_456") is not None
assert len(hass.states.async_entity_ids()) == 1
@ -175,7 +197,10 @@ async def test_friendly_name_changed(
async def test_delivered_not_shown(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_seventeentrack: AsyncMock
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_seventeentrack: AsyncMock,
mock_config_entry_with_default_options: MockConfigEntry,
) -> None:
"""Ensure delivered packages are not shown."""
package = get_package(status=40)
@ -185,7 +210,7 @@ async def test_delivered_not_shown(
with patch(
"homeassistant.components.seventeentrack.sensor.persistent_notification"
) as persistent_notification_mock:
await init_integration(hass, VALID_CONFIG_FULL_NO_DELIVERED)
await init_integration(hass, mock_config_entry_with_default_options)
await goto_future(hass, freezer)
assert not hass.states.async_entity_ids()
@ -193,7 +218,9 @@ async def test_delivered_not_shown(
async def test_delivered_shown(
hass: HomeAssistant, mock_seventeentrack: AsyncMock
hass: HomeAssistant,
mock_seventeentrack: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure delivered packages are show when user choose to show them."""
package = get_package(status=40)
@ -203,7 +230,7 @@ async def test_delivered_shown(
with patch(
"homeassistant.components.seventeentrack.sensor.persistent_notification"
) as persistent_notification_mock:
await init_integration(hass, VALID_CONFIG_FULL)
await init_integration(hass, mock_config_entry)
assert hass.states.get("sensor.seventeentrack_package_456") is not None
assert len(hass.states.async_entity_ids()) == 1
@ -211,14 +238,17 @@ async def test_delivered_shown(
async def test_becomes_delivered_not_shown_notification(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_seventeentrack: AsyncMock
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_seventeentrack: AsyncMock,
mock_config_entry_with_default_options: MockConfigEntry,
) -> None:
"""Ensure notification is triggered when package becomes delivered."""
package = get_package()
mock_seventeentrack.return_value.profile.packages.return_value = [package]
mock_seventeentrack.return_value.profile.summary.return_value = {}
await init_integration(hass, VALID_CONFIG_FULL_NO_DELIVERED)
await init_integration(hass, mock_config_entry_with_default_options)
assert hass.states.get("sensor.seventeentrack_package_456") is not None
assert len(hass.states.async_entity_ids()) == 1
@ -237,14 +267,17 @@ async def test_becomes_delivered_not_shown_notification(
async def test_summary_correctly_updated(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_seventeentrack: AsyncMock
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_seventeentrack: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure summary entities are not duplicated."""
package = get_package(status=30)
mock_seventeentrack.return_value.profile.packages.return_value = [package]
mock_seventeentrack.return_value.profile.summary.return_value = DEFAULT_SUMMARY
await init_integration(hass, VALID_CONFIG_FULL)
await init_integration(hass, mock_config_entry)
assert len(hass.states.async_entity_ids()) == 8
@ -272,7 +305,9 @@ async def test_summary_correctly_updated(
async def test_summary_error(
hass: HomeAssistant, mock_seventeentrack: AsyncMock
hass: HomeAssistant,
mock_seventeentrack: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test summary empty if error."""
package = get_package(status=30)
@ -281,7 +316,7 @@ async def test_summary_error(
"Error"
)
await init_integration(hass, VALID_CONFIG_FULL)
await init_integration(hass, mock_config_entry)
assert len(hass.states.async_entity_ids()) == 1
@ -291,7 +326,9 @@ async def test_summary_error(
async def test_utc_timestamp(
hass: HomeAssistant, mock_seventeentrack: AsyncMock
hass: HomeAssistant,
mock_seventeentrack: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure package timestamp is converted correctly from HA-defined time zone to UTC."""
@ -299,7 +336,7 @@ async def test_utc_timestamp(
mock_seventeentrack.return_value.profile.packages.return_value = [package]
mock_seventeentrack.return_value.profile.summary.return_value = {}
await init_integration(hass, VALID_CONFIG_FULL)
await init_integration(hass, mock_config_entry)
assert hass.states.get("sensor.seventeentrack_package_456") is not None
assert len(hass.states.async_entity_ids()) == 1
@ -313,5 +350,18 @@ async def test_non_valid_platform_config(
) -> None:
"""Test if login fails."""
mock_seventeentrack.return_value.profile.login.return_value = False
await init_integration(hass, VALID_CONFIG_FULL)
assert await async_setup_component(hass, "sensor", VALID_PLATFORM_CONFIG_FULL)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids()) == 0
async def test_full_valid_platform_config(
hass: HomeAssistant,
mock_seventeentrack: AsyncMock,
issue_registry: IssueRegistry,
) -> None:
"""Ensure everything starts correctly."""
assert await async_setup_component(hass, "sensor", VALID_PLATFORM_CONFIG_FULL)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids()) == len(DEFAULT_SUMMARY.keys())
assert len(issue_registry.issues) == 1