Fix bug in Fitbit config flow, and switch to prefer display name (#103869)
This commit is contained in:
parent
51b599e1f6
commit
96a19d61ab
5 changed files with 96 additions and 14 deletions
|
@ -69,7 +69,7 @@ class FitbitApi(ABC):
|
||||||
profile = response["user"]
|
profile = response["user"]
|
||||||
self._profile = FitbitProfile(
|
self._profile = FitbitProfile(
|
||||||
encoded_id=profile["encodedId"],
|
encoded_id=profile["encodedId"],
|
||||||
full_name=profile["fullName"],
|
display_name=profile["displayName"],
|
||||||
locale=profile.get("locale"),
|
locale=profile.get("locale"),
|
||||||
)
|
)
|
||||||
return self._profile
|
return self._profile
|
||||||
|
|
|
@ -90,7 +90,7 @@ class OAuth2FlowHandler(
|
||||||
|
|
||||||
await self.async_set_unique_id(profile.encoded_id)
|
await self.async_set_unique_id(profile.encoded_id)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
return self.async_create_entry(title=profile.full_name, data=data)
|
return self.async_create_entry(title=profile.display_name, data=data)
|
||||||
|
|
||||||
async def async_step_import(self, data: dict[str, Any]) -> FlowResult:
|
async def async_step_import(self, data: dict[str, Any]) -> FlowResult:
|
||||||
"""Handle import from YAML."""
|
"""Handle import from YAML."""
|
||||||
|
|
|
@ -14,8 +14,8 @@ class FitbitProfile:
|
||||||
encoded_id: str
|
encoded_id: str
|
||||||
"""The ID representing the Fitbit user."""
|
"""The ID representing the Fitbit user."""
|
||||||
|
|
||||||
full_name: str
|
display_name: str
|
||||||
"""The first name value specified in the user's account settings."""
|
"""The name shown when the user's friends look at their Fitbit profile."""
|
||||||
|
|
||||||
locale: str | None
|
locale: str | None
|
||||||
"""The locale defined in the user's Fitbit account settings."""
|
"""The locale defined in the user's Fitbit account settings."""
|
||||||
|
|
|
@ -32,6 +32,15 @@ PROFILE_USER_ID = "fitbit-api-user-id-1"
|
||||||
FAKE_ACCESS_TOKEN = "some-access-token"
|
FAKE_ACCESS_TOKEN = "some-access-token"
|
||||||
FAKE_REFRESH_TOKEN = "some-refresh-token"
|
FAKE_REFRESH_TOKEN = "some-refresh-token"
|
||||||
FAKE_AUTH_IMPL = "conftest-imported-cred"
|
FAKE_AUTH_IMPL = "conftest-imported-cred"
|
||||||
|
FULL_NAME = "First Last"
|
||||||
|
DISPLAY_NAME = "First L."
|
||||||
|
PROFILE_DATA = {
|
||||||
|
"fullName": FULL_NAME,
|
||||||
|
"displayName": DISPLAY_NAME,
|
||||||
|
"displayNameSetting": "name",
|
||||||
|
"firstName": "First",
|
||||||
|
"lastName": "Last",
|
||||||
|
}
|
||||||
|
|
||||||
PROFILE_API_URL = "https://api.fitbit.com/1/user/-/profile.json"
|
PROFILE_API_URL = "https://api.fitbit.com/1/user/-/profile.json"
|
||||||
DEVICES_API_URL = "https://api.fitbit.com/1/user/-/devices.json"
|
DEVICES_API_URL = "https://api.fitbit.com/1/user/-/devices.json"
|
||||||
|
@ -214,20 +223,34 @@ def mock_profile_locale() -> str:
|
||||||
return "en_US"
|
return "en_US"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="profile_data")
|
||||||
|
def mock_profile_data() -> dict[str, Any]:
|
||||||
|
"""Fixture to return other profile data fields."""
|
||||||
|
return PROFILE_DATA
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="profile_response")
|
||||||
|
def mock_profile_response(
|
||||||
|
profile_id: str, profile_locale: str, profile_data: dict[str, Any]
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Fixture to construct the fake profile API response."""
|
||||||
|
return {
|
||||||
|
"user": {
|
||||||
|
"encodedId": profile_id,
|
||||||
|
"locale": profile_locale,
|
||||||
|
**profile_data,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="profile", autouse=True)
|
@pytest.fixture(name="profile", autouse=True)
|
||||||
def mock_profile(requests_mock: Mocker, profile_id: str, profile_locale: str) -> None:
|
def mock_profile(requests_mock: Mocker, profile_response: dict[str, Any]) -> None:
|
||||||
"""Fixture to setup fake requests made to Fitbit API during config flow."""
|
"""Fixture to setup fake requests made to Fitbit API during config flow."""
|
||||||
requests_mock.register_uri(
|
requests_mock.register_uri(
|
||||||
"GET",
|
"GET",
|
||||||
PROFILE_API_URL,
|
PROFILE_API_URL,
|
||||||
status_code=HTTPStatus.OK,
|
status_code=HTTPStatus.OK,
|
||||||
json={
|
json=profile_response,
|
||||||
"user": {
|
|
||||||
"encodedId": profile_id,
|
|
||||||
"fullName": "My name",
|
|
||||||
"locale": profile_locale,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,10 @@ from homeassistant.helpers import config_entry_oauth2_flow, issue_registry as ir
|
||||||
|
|
||||||
from .conftest import (
|
from .conftest import (
|
||||||
CLIENT_ID,
|
CLIENT_ID,
|
||||||
|
DISPLAY_NAME,
|
||||||
FAKE_AUTH_IMPL,
|
FAKE_AUTH_IMPL,
|
||||||
PROFILE_API_URL,
|
PROFILE_API_URL,
|
||||||
|
PROFILE_DATA,
|
||||||
PROFILE_USER_ID,
|
PROFILE_USER_ID,
|
||||||
SERVER_ACCESS_TOKEN,
|
SERVER_ACCESS_TOKEN,
|
||||||
)
|
)
|
||||||
|
@ -76,7 +78,7 @@ async def test_full_flow(
|
||||||
entries = hass.config_entries.async_entries(DOMAIN)
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
assert len(entries) == 1
|
assert len(entries) == 1
|
||||||
config_entry = entries[0]
|
config_entry = entries[0]
|
||||||
assert config_entry.title == "My name"
|
assert config_entry.title == DISPLAY_NAME
|
||||||
assert config_entry.unique_id == PROFILE_USER_ID
|
assert config_entry.unique_id == PROFILE_USER_ID
|
||||||
|
|
||||||
data = dict(config_entry.data)
|
data = dict(config_entry.data)
|
||||||
|
@ -286,7 +288,7 @@ async def test_import_fitbit_config(
|
||||||
|
|
||||||
# Verify valid profile can be fetched from the API
|
# Verify valid profile can be fetched from the API
|
||||||
config_entry = entries[0]
|
config_entry = entries[0]
|
||||||
assert config_entry.title == "My name"
|
assert config_entry.title == DISPLAY_NAME
|
||||||
assert config_entry.unique_id == PROFILE_USER_ID
|
assert config_entry.unique_id == PROFILE_USER_ID
|
||||||
|
|
||||||
data = dict(config_entry.data)
|
data = dict(config_entry.data)
|
||||||
|
@ -598,3 +600,60 @@ async def test_reauth_wrong_user_id(
|
||||||
assert result.get("reason") == "wrong_account"
|
assert result.get("reason") == "wrong_account"
|
||||||
|
|
||||||
assert len(mock_setup.mock_calls) == 0
|
assert len(mock_setup.mock_calls) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("profile_data", "expected_title"),
|
||||||
|
[
|
||||||
|
(PROFILE_DATA, DISPLAY_NAME),
|
||||||
|
({"displayName": DISPLAY_NAME}, DISPLAY_NAME),
|
||||||
|
],
|
||||||
|
ids=("full_profile_data", "display_name_only"),
|
||||||
|
)
|
||||||
|
async def test_partial_profile_data(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
current_request_with_host: None,
|
||||||
|
profile: None,
|
||||||
|
setup_credentials: None,
|
||||||
|
expected_title: str,
|
||||||
|
) -> 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
|
||||||
|
|
||||||
|
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 == expected_title
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue