Skip to content

Commit

Permalink
Add Google Photos reauth support (#124933)
Browse files Browse the repository at this point in the history
* Add Google Photos reauth support

* Update tests/components/google_photos/test_config_flow.py

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

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
  • Loading branch information
allenporter and joostlek committed Aug 30, 2024
1 parent 28c24e5 commit cb742a6
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 67 deletions.
10 changes: 8 additions & 2 deletions homeassistant/components/google_photos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow

from . import api
Expand All @@ -32,7 +32,13 @@ async def async_setup_entry(
auth = api.AsyncConfigEntryAuth(hass, session)
try:
await auth.async_get_access_token()
except (ClientResponseError, ClientError) as err:
except ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(
"OAuth session is not valid, reauth required"
) from err
raise ConfigEntryNotReady from err
except ClientError as err:
raise ConfigEntryNotReady from err
entry.runtime_data = auth
return True
Expand Down
30 changes: 29 additions & 1 deletion homeassistant/components/google_photos/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""Config flow for Google Photos."""

from collections.abc import Mapping
import logging
from typing import Any

from homeassistant.config_entries import ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.helpers import config_entry_oauth2_flow

from . import api
from . import GooglePhotosConfigEntry, api
from .const import DOMAIN, OAUTH2_SCOPES
from .exceptions import GooglePhotosApiError

Expand All @@ -19,6 +20,8 @@ class OAuth2FlowHandler(

DOMAIN = DOMAIN

reauth_entry: GooglePhotosConfigEntry | None = None

@property
def logger(self) -> logging.Logger:
"""Return logger."""
Expand Down Expand Up @@ -49,6 +52,31 @@ async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResu
self.logger.exception("Unknown error occurred")
return self.async_abort(reason="unknown")
user_id = user_resource_info["id"]

if self.reauth_entry:
if self.reauth_entry.unique_id == user_id:
return self.async_update_reload_and_abort(
self.reauth_entry, unique_id=user_id, data=data
)
return self.async_abort(reason="wrong_account")

await self.async_set_unique_id(user_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=user_resource_info["name"], data=data)

async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
self.reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_reauth_confirm()

async def async_step_reauth_confirm(
self, user_input: Mapping[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauth dialog."""
if user_input is None:
return self.async_show_form(step_id="reauth_confirm")
return await self.async_step_user()
31 changes: 25 additions & 6 deletions tests/components/google_photos/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,18 @@
from tests.common import MockConfigEntry, load_json_array_fixture

USER_IDENTIFIER = "user-identifier-1"
CONFIG_ENTRY_ID = "user-identifier-1"
CLIENT_ID = "1234"
CLIENT_SECRET = "5678"
FAKE_ACCESS_TOKEN = "some-access-token"
FAKE_REFRESH_TOKEN = "some-refresh-token"
EXPIRES_IN = 3600


@pytest.fixture(name="expires_at")
def mock_expires_at() -> int:
"""Fixture to set the oauth token expiration time."""
return time.time() + 3600
return time.time() + EXPIRES_IN


@pytest.fixture(name="token_entry")
Expand All @@ -37,17 +39,26 @@ def mock_token_entry(expires_at: int) -> dict[str, Any]:
"access_token": FAKE_ACCESS_TOKEN,
"refresh_token": FAKE_REFRESH_TOKEN,
"scope": " ".join(OAUTH2_SCOPES),
"token_type": "Bearer",
"type": "Bearer",
"expires_at": expires_at,
"expires_in": EXPIRES_IN,
}


@pytest.fixture(name="config_entry_id")
def mock_config_entry_id() -> str | None:
"""Provide a json fixture file to load for list media item api responses."""
return CONFIG_ENTRY_ID


@pytest.fixture(name="config_entry")
def mock_config_entry(token_entry: dict[str, Any]) -> MockConfigEntry:
def mock_config_entry(
config_entry_id: str, token_entry: dict[str, Any]
) -> MockConfigEntry:
"""Fixture for a config entry."""
return MockConfigEntry(
domain=DOMAIN,
unique_id="config-entry-id-123",
unique_id=config_entry_id,
data={
"auth_implementation": DOMAIN,
"token": token_entry,
Expand All @@ -73,12 +84,20 @@ def mock_fixture_name() -> str | None:
return None


@pytest.fixture(name="user_identifier")
def mock_user_identifier() -> str | None:
"""Provide a json fixture file to load for list media item api responses."""
return USER_IDENTIFIER


@pytest.fixture(name="setup_api")
def mock_setup_api(fixture_name: str) -> Generator[Mock, None, None]:
def mock_setup_api(
fixture_name: str, user_identifier: str
) -> Generator[Mock, None, None]:
"""Set up fake Google Photos API responses from fixtures."""
with patch("homeassistant.components.google_photos.api.build") as mock:
mock.return_value.userinfo.return_value.get.return_value.execute.return_value = {
"id": USER_IDENTIFIER,
"id": user_identifier,
"name": "Test Name",
}

Expand Down
Loading

0 comments on commit cb742a6

Please sign in to comment.