diff --git a/homeassistant/components/electric_kiwi/strings.json b/homeassistant/components/electric_kiwi/strings.json index 81de5cef896..4a67bd5211b 100644 --- a/homeassistant/components/electric_kiwi/strings.json +++ b/homeassistant/components/electric_kiwi/strings.json @@ -17,7 +17,10 @@ "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" @@ -25,17 +28,9 @@ }, "entity": { "sensor": { - "hopfreepowerstart": { - "name": "Hour of free power start" - }, - "hopfreepowerend": { - "name": "Hour of free power end" - } + "hopfreepowerstart": { "name": "Hour of free power start" }, + "hopfreepowerend": { "name": "Hour of free power end" } }, - "select": { - "hopselector": { - "name": "Hour of free power" - } - } + "select": { "hopselector": { "name": "Hour of free power" } } } } diff --git a/homeassistant/components/fitbit/strings.json b/homeassistant/components/fitbit/strings.json index 7e85e232099..d941121e4da 100644 --- a/homeassistant/components/fitbit/strings.json +++ b/homeassistant/components/fitbit/strings.json @@ -22,7 +22,10 @@ "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]", - "wrong_account": "The user credentials provided do not match this Fitbit account." + "wrong_account": "The user credentials provided do not match this Fitbit account.", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/geocaching/strings.json b/homeassistant/components/geocaching/strings.json index 6dc2fe8ec1c..fd431860cd2 100644 --- a/homeassistant/components/geocaching/strings.json +++ b/homeassistant/components/geocaching/strings.json @@ -16,7 +16,10 @@ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" @@ -24,21 +27,11 @@ }, "entity": { "sensor": { - "find_count": { - "name": "Total finds" - }, - "hide_count": { - "name": "Total hides" - }, - "favorite_points": { - "name": "Favorite points" - }, - "souvenir_count": { - "name": "Total souvenirs" - }, - "awarded_favorite_points": { - "name": "Awarded favorite points" - } + "find_count": { "name": "Total finds" }, + "hide_count": { "name": "Total hides" }, + "favorite_points": { "name": "Favorite points" }, + "souvenir_count": { "name": "Total souvenirs" }, + "awarded_favorite_points": { "name": "Awarded favorite points" } } } } diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index b3594f31510..9327009bda3 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -22,7 +22,10 @@ "code_expired": "Authentication code expired or credential setup is invalid, please try again.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", - "api_disabled": "You must enable the Google Calendar API in the Google Cloud Console" + "api_disabled": "You must enable the Google Calendar API in the Google Cloud Console", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/google_assistant_sdk/strings.json b/homeassistant/components/google_assistant_sdk/strings.json index e9e2b7d4c09..fa86e207a9c 100644 --- a/homeassistant/components/google_assistant_sdk/strings.json +++ b/homeassistant/components/google_assistant_sdk/strings.json @@ -21,7 +21,10 @@ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/google_mail/strings.json b/homeassistant/components/google_mail/strings.json index 2bd70750ff9..3ed1c2377d5 100644 --- a/homeassistant/components/google_mail/strings.json +++ b/homeassistant/components/google_mail/strings.json @@ -22,7 +22,10 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", "unknown": "[%key:common::config_flow::error::unknown%]", - "wrong_account": "Wrong account: Please authenticate with {email}." + "wrong_account": "Wrong account: Please authenticate with {email}.", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/google_sheets/strings.json b/homeassistant/components/google_sheets/strings.json index b2cba19031e..ea327097d88 100644 --- a/homeassistant/components/google_sheets/strings.json +++ b/homeassistant/components/google_sheets/strings.json @@ -4,9 +4,7 @@ "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, - "auth": { - "title": "Link Google Account" - }, + "auth": { "title": "Link Google Account" }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The Google Sheets integration needs to re-authenticate your account" @@ -23,7 +21,10 @@ "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", "unknown": "[%key:common::config_flow::error::unknown%]", "create_spreadsheet_failure": "Error while creating spreadsheet, see error log for details", - "open_spreadsheet_failure": "Error while opening spreadsheet, see error log for details" + "open_spreadsheet_failure": "Error while opening spreadsheet, see error log for details", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "Successfully authenticated and spreadsheet created at: {url}" diff --git a/homeassistant/components/google_tasks/strings.json b/homeassistant/components/google_tasks/strings.json index f15c31f42d4..d730f4cb770 100644 --- a/homeassistant/components/google_tasks/strings.json +++ b/homeassistant/components/google_tasks/strings.json @@ -17,7 +17,10 @@ "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", "access_not_configured": "Unable to access the Google API:\n\n{message}", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 091f0c18232..7ee44089b28 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -7,7 +7,11 @@ }, "abort": { "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" @@ -22,22 +26,13 @@ "name": "Device ID", "description": "Id of the device." }, - "program": { - "name": "Program", - "description": "Program to select." - }, - "key": { - "name": "Option key", - "description": "Key of the option." - }, + "program": { "name": "Program", "description": "Program to select." }, + "key": { "name": "Option key", "description": "Key of the option." }, "value": { "name": "Option value", "description": "Value of the option." }, - "unit": { - "name": "Option unit", - "description": "Unit for the option." - } + "unit": { "name": "Option unit", "description": "Unit for the option." } } }, "select_program": { @@ -130,14 +125,8 @@ "name": "Device ID", "description": "[%key:component::home_connect::services::start_program::fields::device_id::description%]" }, - "key": { - "name": "Key", - "description": "Key of the setting." - }, - "value": { - "name": "Value", - "description": "Value of the setting." - } + "key": { "name": "Key", "description": "Key of the setting." }, + "value": { "name": "Value", "description": "Value of the setting." } } } } diff --git a/homeassistant/components/home_plus_control/strings.json b/homeassistant/components/home_plus_control/strings.json index 280a92055bd..c35650a5183 100644 --- a/homeassistant/components/home_plus_control/strings.json +++ b/homeassistant/components/home_plus_control/strings.json @@ -11,7 +11,11 @@ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/lametric/strings.json b/homeassistant/components/lametric/strings.json index e7bfc059674..4069cb41bdd 100644 --- a/homeassistant/components/lametric/strings.json +++ b/homeassistant/components/lametric/strings.json @@ -41,7 +41,11 @@ "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "reauth_device_not_found": "The device you are trying to re-authenticate is not found in this LaMetric account", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" } }, "entity": { diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index 219530a9747..6b594654dfa 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -12,7 +12,11 @@ "abort": { "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/neato/strings.json b/homeassistant/components/neato/strings.json index d611abb83b0..3dcceecd1e3 100644 --- a/homeassistant/components/neato/strings.json +++ b/homeassistant/components/neato/strings.json @@ -13,7 +13,11 @@ "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 717ce5075f7..a54ac82a9a7 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -49,7 +49,11 @@ "unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]" + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index 593320827fd..99f780dbe3e 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -14,7 +14,11 @@ "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/ondilo_ico/strings.json b/homeassistant/components/ondilo_ico/strings.json index 3843670bc50..7b049e66ae2 100644 --- a/homeassistant/components/ondilo_ico/strings.json +++ b/homeassistant/components/ondilo_ico/strings.json @@ -7,7 +7,11 @@ }, "abort": { "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]" + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/senz/strings.json b/homeassistant/components/senz/strings.json index 316f7234f9b..693cfe3415b 100644 --- a/homeassistant/components/senz/strings.json +++ b/homeassistant/components/senz/strings.json @@ -11,7 +11,10 @@ "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/smappee/strings.json b/homeassistant/components/smappee/strings.json index 58abeb57186..9322170dfdf 100644 --- a/homeassistant/components/smappee/strings.json +++ b/homeassistant/components/smappee/strings.json @@ -29,7 +29,11 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "invalid_mdns": "Unsupported device for the Smappee integration.", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" } } } diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index ec2721aba8b..b53b600d5ba 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -13,7 +13,11 @@ "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "The Spotify integration is not configured. Please follow the documentation.", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", - "reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication." + "reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication.", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "Successfully authenticated with Spotify." diff --git a/homeassistant/components/toon/strings.json b/homeassistant/components/toon/strings.json index 620a7f51113..fb05a15db00 100644 --- a/homeassistant/components/toon/strings.json +++ b/homeassistant/components/toon/strings.json @@ -18,7 +18,11 @@ "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "no_agreements": "This account has no Toon displays.", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" } }, "services": { diff --git a/homeassistant/components/twitch/strings.json b/homeassistant/components/twitch/strings.json index 45f88747128..3bda5284c0f 100644 --- a/homeassistant/components/twitch/strings.json +++ b/homeassistant/components/twitch/strings.json @@ -10,7 +10,11 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]", - "wrong_account": "Wrong account: Please authenticate with {username}." + "wrong_account": "Wrong account: Please authenticate with {username}.", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" } }, "issues": { diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index fb447f3578e..645ab135300 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -17,7 +17,10 @@ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "already_configured": "Configuration updated for profile.", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", - "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]" + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "Successfully authenticated with Withings." diff --git a/homeassistant/components/xbox/strings.json b/homeassistant/components/xbox/strings.json index accd6775941..e011194dc7c 100644 --- a/homeassistant/components/xbox/strings.json +++ b/homeassistant/components/xbox/strings.json @@ -8,7 +8,11 @@ "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]" + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/yolink/strings.json b/homeassistant/components/yolink/strings.json index b1cd8d87a75..07df1008653 100644 --- a/homeassistant/components/yolink/strings.json +++ b/homeassistant/components/yolink/strings.json @@ -16,7 +16,10 @@ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" @@ -36,21 +39,11 @@ }, "entity": { "switch": { - "usb_ports": { - "name": "USB ports" - }, - "plug_1": { - "name": "Plug 1" - }, - "plug_2": { - "name": "Plug 2" - }, - "plug_3": { - "name": "Plug 3" - }, - "plug_4": { - "name": "Plug 4" - } + "usb_ports": { "name": "USB ports" }, + "plug_1": { "name": "Plug 1" }, + "plug_2": { "name": "Plug 2" }, + "plug_3": { "name": "Plug 3" }, + "plug_4": { "name": "Plug 4" } }, "sensor": { "power_failure_alarm": { @@ -63,18 +56,11 @@ }, "power_failure_alarm_mute": { "name": "Power failure alarm mute", - "state": { - "muted": "Muted", - "unmuted": "Unmuted" - } + "state": { "muted": "Muted", "unmuted": "Unmuted" } }, "power_failure_alarm_volume": { "name": "Power failure alarm volume", - "state": { - "low": "Low", - "medium": "Medium", - "high": "High" - } + "state": { "low": "Low", "medium": "Medium", "high": "High" } }, "power_failure_alarm_beep": { "name": "Power failure alarm beep", diff --git a/homeassistant/components/youtube/strings.json b/homeassistant/components/youtube/strings.json index 1b9ecbc1cb3..0bd62a42314 100644 --- a/homeassistant/components/youtube/strings.json +++ b/homeassistant/components/youtube/strings.json @@ -6,7 +6,11 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "no_subscriptions": "You need to be subscribed to YouTube channels in order to add them.", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]" }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", @@ -15,9 +19,7 @@ "step": { "channels": { "description": "Select the channels you want to add.", - "data": { - "channels": "YouTube channels" - } + "data": { "channels": "YouTube channels" } }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", @@ -40,17 +42,11 @@ "latest_upload": { "name": "Latest upload", "state_attributes": { - "video_id": { - "name": "Video ID" - }, - "published_at": { - "name": "Published at" - } + "video_id": { "name": "Video ID" }, + "published_at": { "name": "Published at" } } }, - "subscribers": { - "name": "Subscribers" - } + "subscribers": { "name": "Subscribers" } } } } diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 1a106364566..5b4b803a8d4 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -10,12 +10,14 @@ from __future__ import annotations from abc import ABC, ABCMeta, abstractmethod import asyncio from collections.abc import Awaitable, Callable +from http import HTTPStatus +from json import JSONDecodeError import logging import secrets import time from typing import Any, cast -from aiohttp import client, web +from aiohttp import ClientError, ClientResponseError, client, web import jwt import voluptuous as vol from yarl import URL @@ -199,12 +201,15 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation): _LOGGER.debug("Sending token request to %s", self.token_url) resp = await session.post(self.token_url, data=data) - if resp.status >= 400 and _LOGGER.isEnabledFor(logging.DEBUG): - body = await resp.text() - _LOGGER.debug( - "Token request failed with status=%s, body=%s", - resp.status, - body, + if resp.status >= 400: + try: + error_response = await resp.json() + except (ClientError, JSONDecodeError): + error_response = {} + error_code = error_response.get("error", "unknown") + error_description = error_response.get("error_description", "unknown error") + _LOGGER.error( + "Token request failed (%s): %s", error_code, error_description ) resp.raise_for_status() return cast(dict, await resp.json()) @@ -317,7 +322,14 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): ) except asyncio.TimeoutError as err: _LOGGER.error("Timeout resolving OAuth token: %s", err) - return self.async_abort(reason="oauth2_timeout") + return self.async_abort(reason="oauth_timeout") + except (ClientResponseError, ClientError) as err: + if ( + isinstance(err, ClientResponseError) + and err.status == HTTPStatus.UNAUTHORIZED + ): + return self.async_abort(reason="oauth_unauthorized") + return self.async_abort(reason="oauth_failed") if "expires_in" not in token: _LOGGER.warning("Invalid token: %s", token) diff --git a/homeassistant/strings.json b/homeassistant/strings.json index f41380fc9e5..6e6499e0d19 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -126,6 +126,8 @@ "oauth2_authorize_url_timeout": "Timeout generating authorize URL.", "oauth2_no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "oauth2_user_rejected_authorize": "Account linking rejected: {error}", + "oauth2_unauthorized": "OAuth authorization error while obtaining access token.", + "oauth2_failed": "Error while obtaining access token.", "reauth_successful": "Re-authentication was successful", "unknown_authorize_url_generation": "Unknown error generating an authorize URL.", "cloud_not_connected": "Not connected to Home Assistant Cloud." diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index e31df7ecf0f..ff503bc12db 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -185,6 +185,9 @@ def _custom_tasks(template, info: Info) -> None: "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index 7ab4a6dafc1..b483b752e77 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Google Nest Device Access config flow.""" from __future__ import annotations +from http import HTTPStatus from typing import Any from unittest.mock import patch @@ -27,7 +28,9 @@ from .common import ( SUBSCRIBER_ID, TEST_CONFIG_APP_CREDS, TEST_CONFIGFLOW_APP_CREDS, + FakeSubscriber, NestTestConfig, + PlatformSetup, ) from tests.common import MockConfigEntry @@ -92,8 +95,6 @@ class OAuthFixture: assert resp.status == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" - await self.async_mock_refresh(result) - async def async_reauth(self, config_entry: ConfigEntry) -> dict: """Initiate a reuath flow.""" config_entry.async_start_reauth(self.hass) @@ -137,7 +138,7 @@ class OAuthFixture: "&access_type=offline&prompt=consent" ) - async def async_mock_refresh(self, result, user_input: dict = None) -> None: + def async_mock_refresh(self) -> None: """Finish the OAuth flow exchanging auth token for refresh token.""" self.aioclient_mock.post( OAUTH2_TOKEN, @@ -202,6 +203,7 @@ async def test_app_credentials( DOMAIN, context={"source": config_entries.SOURCE_USER} ) await oauth.async_app_creds_flow(result) + oauth.async_mock_refresh() entry = await oauth.async_finish_setup(result) @@ -235,6 +237,7 @@ async def test_config_flow_restart( DOMAIN, context={"source": config_entries.SOURCE_USER} ) await oauth.async_app_creds_flow(result) + oauth.async_mock_refresh() # At this point, we should have a valid auth implementation configured. # Simulate aborting the flow and starting over to ensure we get prompted @@ -254,6 +257,7 @@ async def test_config_flow_restart( result = await oauth.async_configure(result, {"project_id": "new-project-id"}) await oauth.async_oauth_web_flow(result, "new-project-id") + oauth.async_mock_refresh() entry = await oauth.async_finish_setup(result, {"code": "1234"}) @@ -305,6 +309,7 @@ async def test_config_flow_wrong_project_id( result = await oauth.async_configure(result, {"project_id": PROJECT_ID}) await oauth.async_oauth_web_flow(result) await hass.async_block_till_done() + oauth.async_mock_refresh() entry = await oauth.async_finish_setup(result, {"code": "1234"}) @@ -341,6 +346,7 @@ async def test_config_flow_pubsub_configuration_error( DOMAIN, context={"source": config_entries.SOURCE_USER} ) await oauth.async_app_creds_flow(result) + oauth.async_mock_refresh() mock_subscriber.create_subscription.side_effect = ConfigurationException result = await oauth.async_configure(result, {"code": "1234"}) @@ -360,6 +366,7 @@ async def test_config_flow_pubsub_subscriber_error( DOMAIN, context={"source": config_entries.SOURCE_USER} ) await oauth.async_app_creds_flow(result) + oauth.async_mock_refresh() mock_subscriber.create_subscription.side_effect = SubscriberException() result = await oauth.async_configure(result, {"code": "1234"}) @@ -384,6 +391,7 @@ async def test_multiple_config_entries( DOMAIN, context={"source": config_entries.SOURCE_USER} ) await oauth.async_app_creds_flow(result, project_id="project-id-2") + oauth.async_mock_refresh() entry = await oauth.async_finish_setup(result) assert entry.title == "Mock Title" assert "token" in entry.data @@ -442,6 +450,7 @@ async def test_reauth_multiple_config_entries( result = await oauth.async_reauth(config_entry) await oauth.async_oauth_web_flow(result) + oauth.async_mock_refresh() await oauth.async_finish_setup(result) @@ -479,6 +488,7 @@ async def test_pubsub_subscription_strip_whitespace( await oauth.async_app_creds_flow( result, cloud_project_id=" " + CLOUD_PROJECT_ID + " " ) + oauth.async_mock_refresh() entry = await oauth.async_finish_setup(result, {"code": "1234"}) assert entry.title == "Import from configuration.yaml" @@ -508,6 +518,7 @@ async def test_pubsub_subscription_auth_failure( mock_subscriber.create_subscription.side_effect = AuthException() await oauth.async_app_creds_flow(result) + oauth.async_mock_refresh() result = await oauth.async_configure(result, {"code": "1234"}) assert result["type"] == "abort" @@ -527,6 +538,7 @@ async def test_pubsub_subscriber_config_entry_reauth( result = await oauth.async_reauth(config_entry) await oauth.async_oauth_web_flow(result) + oauth.async_mock_refresh() # Entering an updated access token refreshes the config entry. entry = await oauth.async_finish_setup(result, {"code": "1234"}) @@ -568,6 +580,7 @@ async def test_config_entry_title_from_home( DOMAIN, context={"source": config_entries.SOURCE_USER} ) await oauth.async_app_creds_flow(result) + oauth.async_mock_refresh() entry = await oauth.async_finish_setup(result, {"code": "1234"}) assert entry.title == "Example Home" @@ -613,6 +626,7 @@ async def test_config_entry_title_multiple_homes( DOMAIN, context={"source": config_entries.SOURCE_USER} ) await oauth.async_app_creds_flow(result) + oauth.async_mock_refresh() entry = await oauth.async_finish_setup(result, {"code": "1234"}) assert entry.title == "Example Home #1, Example Home #2" @@ -628,6 +642,7 @@ async def test_title_failure_fallback( DOMAIN, context={"source": config_entries.SOURCE_USER} ) await oauth.async_app_creds_flow(result) + oauth.async_mock_refresh() mock_subscriber.async_get_device_manager.side_effect = AuthException() entry = await oauth.async_finish_setup(result, {"code": "1234"}) @@ -659,6 +674,7 @@ async def test_structure_missing_trait( DOMAIN, context={"source": config_entries.SOURCE_USER} ) await oauth.async_app_creds_flow(result) + oauth.async_mock_refresh() entry = await oauth.async_finish_setup(result, {"code": "1234"}) # Fallback to default name @@ -705,6 +721,7 @@ async def test_dhcp_discovery_with_creds( result = await oauth.async_configure(result, {"project_id": PROJECT_ID}) await oauth.async_oauth_web_flow(result) + oauth.async_mock_refresh() entry = await oauth.async_finish_setup(result, {"code": "1234"}) await hass.async_block_till_done() @@ -726,3 +743,36 @@ async def test_dhcp_discovery_with_creds( "type": "Bearer", }, } + + +@pytest.mark.parametrize( + ("status_code", "error_reason"), + [ + (HTTPStatus.UNAUTHORIZED, "oauth_unauthorized"), + (HTTPStatus.NOT_FOUND, "oauth_failed"), + (HTTPStatus.INTERNAL_SERVER_ERROR, "oauth_failed"), + ], +) +async def test_token_error( + hass: HomeAssistant, + oauth: OAuthFixture, + subscriber: FakeSubscriber, + setup_platform: PlatformSetup, + status_code: HTTPStatus, + error_reason: str, +) -> None: + """Check full flow.""" + await setup_platform() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await oauth.async_app_creds_flow(result) + oauth.aioclient_mock.post( + OAUTH2_TOKEN, + status=status_code, + ) + + result = await oauth.async_configure(result, user_input=None) + assert result.get("type") == "abort" + assert result.get("reason") == error_reason diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index c36b62f66c0..8c78b7dadc6 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -1,7 +1,9 @@ """Tests for the Somfy config flow.""" import asyncio +from http import HTTPStatus import logging import time +from typing import Any from unittest.mock import patch import aiohttp @@ -339,7 +341,7 @@ async def test_abort_on_oauth_timeout_error( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "oauth2_timeout" + assert result["reason"] == "oauth_timeout" async def test_step_discovery(hass: HomeAssistant, flow_handler, local_impl) -> None: @@ -387,6 +389,164 @@ async def test_abort_discovered_multiple( assert result["reason"] == "already_in_progress" +@pytest.mark.parametrize( + ("status_code", "error_body", "error_reason", "error_log"), + [ + ( + HTTPStatus.UNAUTHORIZED, + {}, + "oauth_unauthorized", + "Token request failed (unknown): unknown", + ), + ( + HTTPStatus.NOT_FOUND, + {}, + "oauth_failed", + "Token request failed (unknown): unknown", + ), + ( + HTTPStatus.INTERNAL_SERVER_ERROR, + {}, + "oauth_failed", + "Token request failed (unknown): unknown", + ), + ( + HTTPStatus.BAD_REQUEST, + { + "error": "invalid_request", + "error_description": "Request was missing the 'redirect_uri' parameter.", + "error_uri": "See the full API docs at https://authorization-server.com/docs/access_token", + }, + "oauth_failed", + "Token request failed (invalid_request): Request was missing the", + ), + ], +) +async def test_abort_if_oauth_token_error( + hass: HomeAssistant, + flow_handler, + local_impl, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + status_code: HTTPStatus, + error_body: dict[str, Any], + error_reason: str, + error_log: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Check error when obtaining an oauth token.""" + flow_handler.async_register_implementation(hass, local_impl) + config_entry_oauth2_flow.async_register_implementation( + hass, TEST_DOMAIN, MockOAuth2Implementation() + ) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "pick_implementation" + + # Pick implementation + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"implementation": TEST_DOMAIN} + ) + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["url"] == ( + f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=read+write" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + TOKEN_URL, + status=status_code, + json=error_body, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == error_reason + assert error_log in caplog.text + + +async def test_abort_if_oauth_token_closing_error( + hass: HomeAssistant, + flow_handler, + local_impl, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Check error when obtaining an oauth token.""" + flow_handler.async_register_implementation(hass, local_impl) + config_entry_oauth2_flow.async_register_implementation( + hass, TEST_DOMAIN, MockOAuth2Implementation() + ) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "pick_implementation" + + # Pick implementation + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"implementation": TEST_DOMAIN} + ) + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["url"] == ( + f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=read+write" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + TOKEN_URL, + status=HTTPStatus.UNAUTHORIZED, + closing=True, + ) + + with caplog.at_level(logging.DEBUG): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert "Token request failed (unknown): unknown" in caplog.text + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "oauth_unauthorized" + + async def test_abort_discovered_existing_entries( hass: HomeAssistant, flow_handler, local_impl ) -> None: diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 356240dc37a..ac874fcc45c 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -7,7 +7,11 @@ from unittest import mock from urllib.parse import parse_qs from aiohttp import ClientSession -from aiohttp.client_exceptions import ClientError, ClientResponseError +from aiohttp.client_exceptions import ( + ClientConnectionError, + ClientError, + ClientResponseError, +) from aiohttp.streams import StreamReader from multidict import CIMultiDict from yarl import URL @@ -53,6 +57,7 @@ class AiohttpClientMocker: exc=None, cookies=None, side_effect=None, + closing=None, ): """Mock a request.""" if not isinstance(url, RETYPE): @@ -72,6 +77,7 @@ class AiohttpClientMocker: exc=exc, headers=headers, side_effect=side_effect, + closing=closing, ) ) @@ -165,6 +171,7 @@ class AiohttpClientMockResponse: exc=None, headers=None, side_effect=None, + closing=None, ): """Initialize a fake response.""" if json is not None: @@ -178,9 +185,10 @@ class AiohttpClientMockResponse: self.method = method self._url = url self.status = status - self.response = response + self._response = response self.exc = exc self.side_effect = side_effect + self.closing = closing self._headers = CIMultiDict(headers or {}) self._cookies = {} @@ -272,6 +280,13 @@ class AiohttpClientMockResponse: def close(self): """Mock close.""" + @property + def response(self): + """Property method to expose the response to other read methods.""" + if self.closing: + raise ClientConnectionError("Connection closed") + return self._response + @contextmanager def mock_aiohttp_client():