Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions homeassistant/components/data_grand_lyon/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Config flow for the Data Grand Lyon integration."""

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

Expand Down Expand Up @@ -72,6 +73,36 @@ async def async_step_user(
errors=errors,
)

async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication."""
return await self.async_step_reauth_confirm()

async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm re-authentication with new credentials."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()

if user_input is not None:
if error := await self._test_connection(user_input):
errors["base"] = error
else:
return self.async_update_reload_and_abort(
Comment thread
zweckj marked this conversation as resolved.
reauth_entry, data_updates=user_input
)

return self.async_show_form(
step_id="reauth_confirm",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA,
{CONF_USERNAME: reauth_entry.data[CONF_USERNAME]},
),
errors=errors,
)

async def _test_connection(self, user_input: dict[str, Any]) -> str | None:
"""Test connectivity by making a dummy API call.

Expand Down
9 changes: 9 additions & 0 deletions homeassistant/components/data_grand_lyon/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import asyncio
from datetime import timedelta

from aiohttp import ClientResponseError
from data_grand_lyon_ha import DataGrandLyonClient, TclPassage

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import CONF_LINE, CONF_STOP_ID, DOMAIN, LOGGER, SUBENTRY_TYPE_STOP
Expand Down Expand Up @@ -57,6 +59,13 @@ async def _async_update_data(self) -> dict[str, list[TclPassage]]:
for i, subentry in enumerate(stop_subentries):
result = stop_results[i]
if isinstance(result, BaseException):
if isinstance(result, ClientResponseError) and result.status in (
401,
403,
):
raise ConfigEntryAuthFailed(
"Authentication failed for Data Grand Lyon"
) from result
LOGGER.warning(
"Error fetching departures for stop %s: %s",
subentry.subentry_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ rules:
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
reauthentication-flow: done
test-coverage: done

# Gold
Expand Down
13 changes: 12 additions & 1 deletion homeassistant/components/data_grand_lyon/strings.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"password": "[%key:component::data_grand_lyon::config::step::user::data_description::password%]",
"username": "[%key:component::data_grand_lyon::config::step::user::data_description::username%]"
}
},
"user": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
Expand Down
79 changes: 79 additions & 0 deletions tests/components/data_grand_lyon/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,85 @@ async def test_form_error_recovers(
assert result["type"] is FlowResultType.CREATE_ENTRY


async def test_reauth_flow(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the reauth flow updates credentials on success."""
mock_config_entry.add_to_hass(hass)

result = await mock_config_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"

with patch(
"homeassistant.components.data_grand_lyon.config_flow.DataGrandLyonClient.get_tcl_passages",
return_value=[],
):
Comment on lines +106 to +109
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because the rest of the file already uses that syntax, I will let it slide, but it would be better to use a fixture for that instead, to reduce indent and save code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the feedback, I’ll clean that up in a future PR.

result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: "new-user", CONF_PASSWORD: "new-pass"},
)

assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert mock_config_entry.data == {
CONF_USERNAME: "new-user",
CONF_PASSWORD: "new-pass",
}


@pytest.mark.parametrize(
("side_effect", "error"),
[
(ClientConnectionError(), "cannot_connect"),
(ClientResponseError(None, None, status=401), "invalid_auth"),
(ClientResponseError(None, None, status=500), "cannot_connect"),
(RuntimeError("unexpected"), "unknown"),
],
ids=["connection-error", "auth-401", "http-500", "unknown"],
)
async def test_reauth_flow_errors(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
side_effect: Exception,
error: str,
) -> None:
"""Test the reauth flow shows errors and recovers."""
mock_config_entry.add_to_hass(hass)

result = await mock_config_entry.start_reauth_flow(hass)

with patch(
"homeassistant.components.data_grand_lyon.config_flow.DataGrandLyonClient.get_tcl_passages",
side_effect=side_effect,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: "new-user", CONF_PASSWORD: "new-pass"},
)

assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
assert result["errors"] == {"base": error}

with patch(
"homeassistant.components.data_grand_lyon.config_flow.DataGrandLyonClient.get_tcl_passages",
return_value=[],
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: "new-user", CONF_PASSWORD: "new-pass"},
)

assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert mock_config_entry.data == {
CONF_USERNAME: "new-user",
CONF_PASSWORD: "new-pass",
}


async def test_form_already_configured(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
Expand Down
25 changes: 24 additions & 1 deletion tests/components/data_grand_lyon/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
from types import MappingProxyType
from unittest.mock import AsyncMock

from aiohttp import ClientResponseError

from homeassistant.components.data_grand_lyon.const import (
CONF_LINE,
CONF_STOP_ID,
DOMAIN,
SUBENTRY_TYPE_STOP,
)
from homeassistant.config_entries import ConfigSubentry
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState, ConfigSubentry
from homeassistant.core import HomeAssistant

from tests.common import MockConfigEntry
Expand Down Expand Up @@ -42,3 +45,23 @@ async def test_subentry_added_reloads(

assert mock_tcl_client.get_tcl_passages.call_count > initial_call_count
assert hass.states.get("sensor.t1_stop_200_next_departure_1") is not None


async def test_setup_triggers_reauth_on_auth_failure(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_tcl_client: AsyncMock,
) -> None:
"""Test that an auth failure during setup triggers a reauth flow."""
mock_tcl_client.get_tcl_passages.side_effect = ClientResponseError(
None, None, status=401
)

mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()

assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR

flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN)
assert any(flow["context"].get("source") == SOURCE_REAUTH for flow in flows)
Loading