From 96a19d61ab3ef94f0d6cce05eb4e827ce15d82c9 Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
Date: Sun, 12 Nov 2023 10:27:02 -0800
Subject: [PATCH] Fix bug in Fitbit config flow, and switch to prefer display
 name (#103869)

---
 homeassistant/components/fitbit/api.py        |  2 +-
 .../components/fitbit/config_flow.py          |  2 +-
 homeassistant/components/fitbit/model.py      |  4 +-
 tests/components/fitbit/conftest.py           | 39 +++++++++---
 tests/components/fitbit/test_config_flow.py   | 63 ++++++++++++++++++-
 5 files changed, 96 insertions(+), 14 deletions(-)

diff --git a/homeassistant/components/fitbit/api.py b/homeassistant/components/fitbit/api.py
index ceb619c4385..49e51a0fd98 100644
--- a/homeassistant/components/fitbit/api.py
+++ b/homeassistant/components/fitbit/api.py
@@ -69,7 +69,7 @@ class FitbitApi(ABC):
             profile = response["user"]
             self._profile = FitbitProfile(
                 encoded_id=profile["encodedId"],
-                full_name=profile["fullName"],
+                display_name=profile["displayName"],
                 locale=profile.get("locale"),
             )
         return self._profile
diff --git a/homeassistant/components/fitbit/config_flow.py b/homeassistant/components/fitbit/config_flow.py
index dd7e79e2c65..7ef6ecbfa28 100644
--- a/homeassistant/components/fitbit/config_flow.py
+++ b/homeassistant/components/fitbit/config_flow.py
@@ -90,7 +90,7 @@ class OAuth2FlowHandler(
 
         await self.async_set_unique_id(profile.encoded_id)
         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:
         """Handle import from YAML."""
diff --git a/homeassistant/components/fitbit/model.py b/homeassistant/components/fitbit/model.py
index 38b1d0bb786..cd8ece163a4 100644
--- a/homeassistant/components/fitbit/model.py
+++ b/homeassistant/components/fitbit/model.py
@@ -14,8 +14,8 @@ class FitbitProfile:
     encoded_id: str
     """The ID representing the Fitbit user."""
 
-    full_name: str
-    """The first name value specified in the user's account settings."""
+    display_name: str
+    """The name shown when the user's friends look at their Fitbit profile."""
 
     locale: str | None
     """The locale defined in the user's Fitbit account settings."""
diff --git a/tests/components/fitbit/conftest.py b/tests/components/fitbit/conftest.py
index 682fb0edd3b..a076be7f63d 100644
--- a/tests/components/fitbit/conftest.py
+++ b/tests/components/fitbit/conftest.py
@@ -32,6 +32,15 @@ PROFILE_USER_ID = "fitbit-api-user-id-1"
 FAKE_ACCESS_TOKEN = "some-access-token"
 FAKE_REFRESH_TOKEN = "some-refresh-token"
 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"
 DEVICES_API_URL = "https://api.fitbit.com/1/user/-/devices.json"
@@ -214,20 +223,34 @@ def mock_profile_locale() -> str:
     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)
-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."""
     requests_mock.register_uri(
         "GET",
         PROFILE_API_URL,
         status_code=HTTPStatus.OK,
-        json={
-            "user": {
-                "encodedId": profile_id,
-                "fullName": "My name",
-                "locale": profile_locale,
-            },
-        },
+        json=profile_response,
     )
 
 
diff --git a/tests/components/fitbit/test_config_flow.py b/tests/components/fitbit/test_config_flow.py
index d51379c9adc..78d20b0fb58 100644
--- a/tests/components/fitbit/test_config_flow.py
+++ b/tests/components/fitbit/test_config_flow.py
@@ -17,8 +17,10 @@ from homeassistant.helpers import config_entry_oauth2_flow, issue_registry as ir
 
 from .conftest import (
     CLIENT_ID,
+    DISPLAY_NAME,
     FAKE_AUTH_IMPL,
     PROFILE_API_URL,
+    PROFILE_DATA,
     PROFILE_USER_ID,
     SERVER_ACCESS_TOKEN,
 )
@@ -76,7 +78,7 @@ async def test_full_flow(
     entries = hass.config_entries.async_entries(DOMAIN)
     assert len(entries) == 1
     config_entry = entries[0]
-    assert config_entry.title == "My name"
+    assert config_entry.title == DISPLAY_NAME
     assert config_entry.unique_id == PROFILE_USER_ID
 
     data = dict(config_entry.data)
@@ -286,7 +288,7 @@ async def test_import_fitbit_config(
 
     # Verify valid profile can be fetched from the API
     config_entry = entries[0]
-    assert config_entry.title == "My name"
+    assert config_entry.title == DISPLAY_NAME
     assert config_entry.unique_id == PROFILE_USER_ID
 
     data = dict(config_entry.data)
@@ -598,3 +600,60 @@ async def test_reauth_wrong_user_id(
     assert result.get("reason") == "wrong_account"
 
     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
-- 
GitLab