core/tests/components/fitbit/test_config_flow.py
Allen Porter bd2fee289d
Update Fitbit integration to allow UI based configuration (#100897)
* Cleanup fitbit sensor API parsing

* Remove API code that is not used yet

* Configuration flow for fitbit

* Code cleanup after manual review

* Streamline code for review

* Use scopes to determine which entities to enable

* Use set for entity comparisons

* Apply fitbit string pr feedback

* Improve fitbit configuration flow error handling

* Apply suggestions from code review

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

* Fix typo in more places

* Revert typing import

* Revert custom domain back to default

* Add additional config flow tests

* Add         breaks_in_ha_version to repair issues

* Update homeassistant/components/fitbit/api.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Increase test coverage for token refresh success case

* Add breaks_in_ha_version for sensor issue

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Simplify translations, issue keys, and token refresh

* Config flow test improvements

* Simplify repair issue creation on fitbit import

* Remove unused strings

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-09-30 16:56:39 -07:00

316 lines
10 KiB
Python

"""Test the fitbit config flow."""
from collections.abc import Awaitable, Callable
from http import HTTPStatus
from unittest.mock import patch
from requests_mock.mocker import Mocker
from homeassistant import config_entries
from homeassistant.components.fitbit.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_entry_oauth2_flow, issue_registry as ir
from .conftest import (
CLIENT_ID,
FAKE_ACCESS_TOKEN,
FAKE_AUTH_IMPL,
FAKE_REFRESH_TOKEN,
PROFILE_API_URL,
PROFILE_USER_ID,
SERVER_ACCESS_TOKEN,
)
from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker
from tests.typing import ClientSessionGenerator
REDIRECT_URL = "https://example.com/auth/external/callback"
async def test_full_flow(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
current_request_with_host: None,
profile: None,
setup_credentials: None,
) -> None:
"""Check full flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
state = config_entry_oauth2_flow._encode_jwt(
hass,
{
"flow_id": result["flow_id"],
"redirect_uri": REDIRECT_URL,
},
)
assert result["type"] == FlowResultType.EXTERNAL_STEP
assert result["url"] == (
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URL}"
f"&state={state}"
"&scope=activity+heartrate+nutrition+profile+settings+sleep+weight&prompt=consent"
)
client = await hass_client_no_auth()
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
assert resp.status == 200
assert resp.headers["content-type"] == "text/html; charset=utf-8"
aioclient_mock.post(
OAUTH2_TOKEN,
json=SERVER_ACCESS_TOKEN,
)
with patch(
"homeassistant.components.fitbit.async_setup_entry", return_value=True
) as mock_setup:
await hass.config_entries.flow.async_configure(result["flow_id"])
assert len(mock_setup.mock_calls) == 1
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
config_entry = entries[0]
assert config_entry.title == "My name"
assert config_entry.unique_id == PROFILE_USER_ID
data = dict(config_entry.data)
assert "token" in data
del data["token"]["expires_at"]
assert dict(config_entry.data) == {
"auth_implementation": FAKE_AUTH_IMPL,
"token": SERVER_ACCESS_TOKEN,
}
async def test_api_failure(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
current_request_with_host: None,
requests_mock: Mocker,
setup_credentials: None,
) -> None:
"""Test a failure to fetch the profile during the setup flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
state = config_entry_oauth2_flow._encode_jwt(
hass,
{
"flow_id": result["flow_id"],
"redirect_uri": REDIRECT_URL,
},
)
assert result["type"] == FlowResultType.EXTERNAL_STEP
assert result["url"] == (
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URL}"
f"&state={state}"
"&scope=activity+heartrate+nutrition+profile+settings+sleep+weight&prompt=consent"
)
client = await hass_client_no_auth()
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
assert resp.status == 200
assert resp.headers["content-type"] == "text/html; charset=utf-8"
aioclient_mock.post(
OAUTH2_TOKEN,
json=SERVER_ACCESS_TOKEN,
)
requests_mock.register_uri(
"GET", PROFILE_API_URL, status_code=HTTPStatus.INTERNAL_SERVER_ERROR
)
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result.get("type") == FlowResultType.ABORT
assert result.get("reason") == "cannot_connect"
async def test_config_entry_already_exists(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
current_request_with_host: None,
requests_mock: Mocker,
setup_credentials: None,
integration_setup: Callable[[], Awaitable[bool]],
config_entry: MockConfigEntry,
) -> None:
"""Test that an account may only be configured once."""
# Verify existing config entry
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
state = config_entry_oauth2_flow._encode_jwt(
hass,
{
"flow_id": result["flow_id"],
"redirect_uri": REDIRECT_URL,
},
)
assert result["type"] == FlowResultType.EXTERNAL_STEP
assert result["url"] == (
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URL}"
f"&state={state}"
"&scope=activity+heartrate+nutrition+profile+settings+sleep+weight&prompt=consent"
)
client = await hass_client_no_auth()
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
assert resp.status == 200
assert resp.headers["content-type"] == "text/html; charset=utf-8"
aioclient_mock.post(
OAUTH2_TOKEN,
json=SERVER_ACCESS_TOKEN,
)
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result.get("type") == FlowResultType.ABORT
assert result.get("reason") == "already_configured"
async def test_import_fitbit_config(
hass: HomeAssistant,
fitbit_config_setup: None,
sensor_platform_setup: Callable[[], Awaitable[bool]],
issue_registry: ir.IssueRegistry,
) -> None:
"""Test that platform configuration is imported successfully."""
with patch(
"homeassistant.components.fitbit.async_setup_entry", return_value=True
) as mock_setup:
await sensor_platform_setup()
assert len(mock_setup.mock_calls) == 1
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
# Verify valid profile can be fetched from the API
config_entry = entries[0]
assert config_entry.title == "My name"
assert config_entry.unique_id == PROFILE_USER_ID
data = dict(config_entry.data)
assert "token" in data
del data["token"]["expires_at"]
# Verify imported values from fitbit.conf and configuration.yaml
assert dict(config_entry.data) == {
"auth_implementation": DOMAIN,
"clock_format": "24H",
"monitored_resources": ["activities/steps"],
"token": {
"access_token": FAKE_ACCESS_TOKEN,
"refresh_token": FAKE_REFRESH_TOKEN,
},
"unit_system": "default",
}
# Verify an issue is raised for deprecated configuration.yaml
issue = issue_registry.issues.get((DOMAIN, "deprecated_yaml"))
assert issue
assert issue.translation_key == "deprecated_yaml_import"
async def test_import_fitbit_config_failure_cannot_connect(
hass: HomeAssistant,
fitbit_config_setup: None,
sensor_platform_setup: Callable[[], Awaitable[bool]],
issue_registry: ir.IssueRegistry,
requests_mock: Mocker,
) -> None:
"""Test platform configuration fails to import successfully."""
requests_mock.register_uri(
"GET", PROFILE_API_URL, status_code=HTTPStatus.INTERNAL_SERVER_ERROR
)
with patch(
"homeassistant.components.fitbit.async_setup_entry", return_value=True
) as mock_setup:
await sensor_platform_setup()
assert len(mock_setup.mock_calls) == 0
# Verify an issue is raised that we were unable to import configuration
issue = issue_registry.issues.get((DOMAIN, "deprecated_yaml"))
assert issue
assert issue.translation_key == "deprecated_yaml_import_issue_cannot_connect"
async def test_import_fitbit_config_already_exists(
hass: HomeAssistant,
config_entry: MockConfigEntry,
setup_credentials: None,
integration_setup: Callable[[], Awaitable[bool]],
fitbit_config_setup: None,
sensor_platform_setup: Callable[[], Awaitable[bool]],
issue_registry: ir.IssueRegistry,
) -> None:
"""Test that platform configuration is not imported if it already exists."""
# Verify existing config entry
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
with patch(
"homeassistant.components.fitbit.async_setup_entry", return_value=True
) as mock_config_entry_setup:
await integration_setup()
assert len(mock_config_entry_setup.mock_calls) == 1
with patch(
"homeassistant.components.fitbit.async_setup_entry", return_value=True
) as mock_import_setup:
await sensor_platform_setup()
assert len(mock_import_setup.mock_calls) == 0
# Still one config entry
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
# Verify an issue is raised for deprecated configuration.yaml
issue = issue_registry.issues.get((DOMAIN, "deprecated_yaml"))
assert issue
assert issue.translation_key == "deprecated_yaml_import"
async def test_platform_setup_without_import(
hass: HomeAssistant,
sensor_platform_setup: Callable[[], Awaitable[bool]],
issue_registry: ir.IssueRegistry,
) -> None:
"""Test platform configuration.yaml but no existing fitbit.conf credentials."""
with patch(
"homeassistant.components.fitbit.async_setup_entry", return_value=True
) as mock_setup:
await sensor_platform_setup()
# Verify no configuration entry is imported since the integration is not
# fully setup properly
assert len(mock_setup.mock_calls) == 0
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 0
# Verify an issue is raised for deprecated configuration.yaml
assert len(issue_registry.issues) == 1
issue = issue_registry.issues.get((DOMAIN, "deprecated_yaml"))
assert issue
assert issue.translation_key == "deprecated_yaml_no_import"