core/tests/components/fitbit/test_config_flow.py
Allen Porter 4e4b8de448
Add reauth support in fitbit (#101178)
* Add reauth support in fitbit

* Update tests/components/fitbit/test_config_flow.py

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

* Improve http status error code handling

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-10-02 08:09:08 +02:00

462 lines
15 KiB
Python

"""Test the fitbit config flow."""
from collections.abc import Awaitable, Callable
from http import HTTPStatus
from unittest.mock import patch
import pytest
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"
async def test_reauth_flow(
hass: HomeAssistant,
config_entry: MockConfigEntry,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
current_request_with_host: None,
profile: None,
setup_credentials: None,
) -> None:
"""Test OAuth reauthentication flow will update existing config entry."""
config_entry.add_to_hass(hass)
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
# config_entry.req initiates reauth
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": config_entry.entry_id,
},
)
assert result["type"] == "form"
assert result["step_id"] == "reauth_confirm"
result = await hass.config_entries.flow.async_configure(
flow_id=result["flow_id"],
user_input={},
)
assert result["type"] == FlowResultType.EXTERNAL_STEP
state = config_entry_oauth2_flow._encode_jwt(
hass,
{
"flow_id": result["flow_id"],
"redirect_uri": REDIRECT_URL,
},
)
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=none"
)
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={
"refresh_token": "updated-refresh-token",
"access_token": "updated-access-token",
"type": "Bearer",
"expires_in": 60,
},
)
with patch(
"homeassistant.components.fitbit.async_setup_entry", return_value=True
) as mock_setup:
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result.get("type") == FlowResultType.ABORT
assert result.get("reason") == "reauth_successful"
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert len(mock_setup.mock_calls) == 1
assert config_entry.data["token"]["refresh_token"] == "updated-refresh-token"
@pytest.mark.parametrize("profile_id", ["other-user-id"])
async def test_reauth_wrong_user_id(
hass: HomeAssistant,
config_entry: MockConfigEntry,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
current_request_with_host: None,
profile: None,
setup_credentials: None,
) -> None:
"""Test OAuth reauthentication where the wrong user is selected."""
config_entry.add_to_hass(hass)
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_REAUTH,
"entry_id": config_entry.entry_id,
},
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
result = await hass.config_entries.flow.async_configure(
flow_id=result["flow_id"],
user_input={},
)
assert result["type"] == FlowResultType.EXTERNAL_STEP
state = config_entry_oauth2_flow._encode_jwt(
hass,
{
"flow_id": result["flow_id"],
"redirect_uri": REDIRECT_URL,
},
)
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=none"
)
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={
"refresh_token": "updated-refresh-token",
"access_token": "updated-access-token",
"type": "Bearer",
"expires_in": 60,
},
)
with patch(
"homeassistant.components.fitbit.async_setup_entry", return_value=True
) as mock_setup:
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result.get("type") == FlowResultType.ABORT
assert result.get("reason") == "wrong_account"
assert len(mock_setup.mock_calls) == 0