mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Improve OAuth error handling in configuration flows (#103157)
* Improve OAuth error handling in configuration flows * Update strings for all integrations that use oauth2 config flow * Remove invalid_auth strings * Revert change to release * Revert close change in aiohttp mock
This commit is contained in:
parent
667a453a35
commit
787fb3b954
@ -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" } }
|
||||
}
|
||||
}
|
||||
|
@ -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%]"
|
||||
|
@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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%]"
|
||||
|
@ -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%]"
|
||||
|
@ -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%]"
|
||||
|
@ -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}"
|
||||
|
@ -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%]"
|
||||
|
@ -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." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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%]"
|
||||
|
@ -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": {
|
||||
|
@ -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%]"
|
||||
|
@ -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%]"
|
||||
|
@ -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%]"
|
||||
|
@ -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%]"
|
||||
|
@ -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%]"
|
||||
|
@ -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%]"
|
||||
|
@ -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]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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."
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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."
|
||||
|
@ -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%]"
|
||||
|
@ -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",
|
||||
|
@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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."
|
||||
|
@ -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%]",
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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():
|
||||
|
Loading…
x
Reference in New Issue
Block a user