Update fitbit device fetch to use a data update coordinator (#101619)

* Add a fitbit device update coordinator

* Remove unnecessary debug output

* Update comments

* Update fitbit coordinator exception handling and test coverage

* Handle reauth failures in other sensors

* Fix scope changes after rebase.
This commit is contained in:
Allen Porter 2023-10-08 23:12:59 -07:00 committed by GitHub
parent 6d1876394e
commit d78ee96e2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 389 additions and 132 deletions

View file

@ -4,6 +4,7 @@ from collections.abc import Awaitable, Callable
from http import HTTPStatus
import pytest
from requests_mock.mocker import Mocker
from homeassistant.components.fitbit.const import (
CONF_CLIENT_ID,
@ -16,6 +17,7 @@ from homeassistant.core import HomeAssistant
from .conftest import (
CLIENT_ID,
CLIENT_SECRET,
DEVICES_API_URL,
FAKE_ACCESS_TOKEN,
FAKE_REFRESH_TOKEN,
SERVER_ACCESS_TOKEN,
@ -125,3 +127,50 @@ async def test_token_requires_reauth(
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "reauth_confirm"
async def test_device_update_coordinator_failure(
hass: HomeAssistant,
integration_setup: Callable[[], Awaitable[bool]],
config_entry: MockConfigEntry,
setup_credentials: None,
requests_mock: Mocker,
) -> None:
"""Test case where the device update coordinator fails on the first request."""
assert config_entry.state == ConfigEntryState.NOT_LOADED
requests_mock.register_uri(
"GET",
DEVICES_API_URL,
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
)
assert not await integration_setup()
assert config_entry.state == ConfigEntryState.SETUP_RETRY
async def test_device_update_coordinator_reauth(
hass: HomeAssistant,
integration_setup: Callable[[], Awaitable[bool]],
config_entry: MockConfigEntry,
setup_credentials: None,
requests_mock: Mocker,
) -> None:
"""Test case where the device update coordinator fails on the first request."""
assert config_entry.state == ConfigEntryState.NOT_LOADED
requests_mock.register_uri(
"GET",
DEVICES_API_URL,
status_code=HTTPStatus.UNAUTHORIZED,
json={
"errors": [{"errorType": "invalid_grant"}],
},
)
assert not await integration_setup()
assert config_entry.state == ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "reauth_confirm"

View file

@ -27,6 +27,8 @@ from .conftest import (
timeseries_response,
)
from tests.common import MockConfigEntry
DEVICE_RESPONSE_CHARGE_2 = {
"battery": "Medium",
"batteryLevel": 60,
@ -577,6 +579,43 @@ async def test_sensor_update_failed(
assert state
assert state.state == "unavailable"
# Verify the config entry is in a normal state (no reauth required)
flows = hass.config_entries.flow.async_progress()
assert not flows
@pytest.mark.parametrize(
("scopes"),
[(["heartrate"])],
)
async def test_sensor_update_failed_requires_reauth(
hass: HomeAssistant,
setup_credentials: None,
integration_setup: Callable[[], Awaitable[bool]],
requests_mock: Mocker,
) -> None:
"""Test a sensor update request requires reauth."""
requests_mock.register_uri(
"GET",
TIMESERIES_API_URL_FORMAT.format(resource="activities/heart"),
status_code=HTTPStatus.UNAUTHORIZED,
json={
"errors": [{"errorType": "invalid_grant"}],
},
)
assert await integration_setup()
state = hass.states.get("sensor.resting_heart_rate")
assert state
assert state.state == "unavailable"
# Verify that reauth is required
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "reauth_confirm"
@pytest.mark.parametrize(
("scopes", "mock_devices"),
@ -594,11 +633,6 @@ async def test_device_battery_level_update_failed(
"GET",
DEVICES_API_URL,
[
{
"status_code": HTTPStatus.OK,
"json": [DEVICE_RESPONSE_CHARGE_2],
},
# A second spurious update request on startup
{
"status_code": HTTPStatus.OK,
"json": [DEVICE_RESPONSE_CHARGE_2],
@ -626,7 +660,63 @@ async def test_device_battery_level_update_failed(
# Request an update for the entity which will fail
await async_update_entity(hass, "sensor.charge_2_battery")
await hass.async_block_till_done()
state = hass.states.get("sensor.charge_2_battery")
assert state
assert state.state == "unavailable"
# Verify the config entry is in a normal state (no reauth required)
flows = hass.config_entries.flow.async_progress()
assert not flows
@pytest.mark.parametrize(
("scopes", "mock_devices"),
[(["settings"], None)],
)
async def test_device_battery_level_reauth_required(
hass: HomeAssistant,
setup_credentials: None,
integration_setup: Callable[[], Awaitable[bool]],
config_entry: MockConfigEntry,
requests_mock: Mocker,
) -> None:
"""Test API failure requires reauth."""
requests_mock.register_uri(
"GET",
DEVICES_API_URL,
[
{
"status_code": HTTPStatus.OK,
"json": [DEVICE_RESPONSE_CHARGE_2],
},
# Fail when requesting an update
{
"status_code": HTTPStatus.UNAUTHORIZED,
"json": {
"errors": [{"errorType": "invalid_grant"}],
},
},
],
)
assert await integration_setup()
state = hass.states.get("sensor.charge_2_battery")
assert state
assert state.state == "Medium"
# Request an update for the entity which will fail
await async_update_entity(hass, "sensor.charge_2_battery")
await hass.async_block_till_done()
state = hass.states.get("sensor.charge_2_battery")
assert state
assert state.state == "unavailable"
# Verify that reauth is required
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "reauth_confirm"