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:
Allen Porter 2023-11-11 02:02:51 -08:00 committed by GitHub
parent 667a453a35
commit 787fb3b954
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 395 additions and 124 deletions

View File

@ -17,7 +17,10 @@
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "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%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", "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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"
@ -25,17 +28,9 @@
}, },
"entity": { "entity": {
"sensor": { "sensor": {
"hopfreepowerstart": { "hopfreepowerstart": { "name": "Hour of free power start" },
"name": "Hour of free power start" "hopfreepowerend": { "name": "Hour of free power end" }
},
"hopfreepowerend": {
"name": "Hour of free power end"
}
}, },
"select": { "select": { "hopselector": { "name": "Hour of free power" } }
"hopselector": {
"name": "Hour of free power"
}
}
} }
} }

View File

@ -22,7 +22,10 @@
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unknown": "[%key:common::config_flow::error::unknown%]", "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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"

View File

@ -16,7 +16,10 @@
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "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%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"
@ -24,21 +27,11 @@
}, },
"entity": { "entity": {
"sensor": { "sensor": {
"find_count": { "find_count": { "name": "Total finds" },
"name": "Total finds" "hide_count": { "name": "Total hides" },
}, "favorite_points": { "name": "Favorite points" },
"hide_count": { "souvenir_count": { "name": "Total souvenirs" },
"name": "Total hides" "awarded_favorite_points": { "name": "Awarded favorite points" }
},
"favorite_points": {
"name": "Favorite points"
},
"souvenir_count": {
"name": "Total souvenirs"
},
"awarded_favorite_points": {
"name": "Awarded favorite points"
}
} }
} }
} }

View File

@ -22,7 +22,10 @@
"code_expired": "Authentication code expired or credential setup is invalid, please try again.", "code_expired": "Authentication code expired or credential setup is invalid, please try again.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "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%]",
"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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"

View File

@ -21,7 +21,10 @@
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "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%]",
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", "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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"

View File

@ -22,7 +22,10 @@
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "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%]",
"unknown": "[%key:common::config_flow::error::unknown%]", "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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"

View File

@ -4,9 +4,7 @@
"pick_implementation": { "pick_implementation": {
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
}, },
"auth": { "auth": { "title": "Link Google Account" },
"title": "Link Google Account"
},
"reauth_confirm": { "reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]", "title": "[%key:common::config_flow::title::reauth%]",
"description": "The Google Sheets integration needs to re-authenticate your account" "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%]", "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%]",
"create_spreadsheet_failure": "Error while creating spreadsheet, see error log for details", "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": { "create_entry": {
"default": "Successfully authenticated and spreadsheet created at: {url}" "default": "Successfully authenticated and spreadsheet created at: {url}"

View File

@ -17,7 +17,10 @@
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
"access_not_configured": "Unable to access the Google API:\n\n{message}", "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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"

View File

@ -7,7 +7,11 @@
}, },
"abort": { "abort": {
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"
@ -22,22 +26,13 @@
"name": "Device ID", "name": "Device ID",
"description": "Id of the device." "description": "Id of the device."
}, },
"program": { "program": { "name": "Program", "description": "Program to select." },
"name": "Program", "key": { "name": "Option key", "description": "Key of the option." },
"description": "Program to select."
},
"key": {
"name": "Option key",
"description": "Key of the option."
},
"value": { "value": {
"name": "Option value", "name": "Option value",
"description": "Value of the option." "description": "Value of the option."
}, },
"unit": { "unit": { "name": "Option unit", "description": "Unit for the option." }
"name": "Option unit",
"description": "Unit for the option."
}
} }
}, },
"select_program": { "select_program": {
@ -130,14 +125,8 @@
"name": "Device ID", "name": "Device ID",
"description": "[%key:component::home_connect::services::start_program::fields::device_id::description%]" "description": "[%key:component::home_connect::services::start_program::fields::device_id::description%]"
}, },
"key": { "key": { "name": "Key", "description": "Key of the setting." },
"name": "Key", "value": { "name": "Value", "description": "Value of the setting." }
"description": "Key of the setting."
},
"value": {
"name": "Value",
"description": "Value of the setting."
}
} }
} }
} }

View File

@ -11,7 +11,11 @@
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "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%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"

View File

@ -41,7 +41,11 @@
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "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_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%]", "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": { "entity": {

View File

@ -12,7 +12,11 @@
"abort": { "abort": {
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"

View File

@ -13,7 +13,11 @@
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "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%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"

View File

@ -49,7 +49,11 @@
"unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]", "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%]", "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%]",
"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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"

View File

@ -14,7 +14,11 @@
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "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%]",
"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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"

View File

@ -7,7 +7,11 @@
}, },
"abort": { "abort": {
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"

View File

@ -11,7 +11,10 @@
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "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%]",
"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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"

View File

@ -29,7 +29,11 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"invalid_mdns": "Unsupported device for the Smappee integration.", "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]"
} }
} }
} }

View File

@ -13,7 +13,11 @@
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"missing_configuration": "The Spotify integration is not configured. Please follow the documentation.", "missing_configuration": "The Spotify integration is not configured. Please follow the documentation.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "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": { "create_entry": {
"default": "Successfully authenticated with Spotify." "default": "Successfully authenticated with Spotify."

View File

@ -18,7 +18,11 @@
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"no_agreements": "This account has no Toon displays.", "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": { "services": {

View File

@ -10,7 +10,11 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unknown": "[%key:common::config_flow::error::unknown%]", "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": { "issues": {

View File

@ -17,7 +17,10 @@
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"already_configured": "Configuration updated for profile.", "already_configured": "Configuration updated for profile.",
"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_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": { "create_entry": {
"default": "Successfully authenticated with Withings." "default": "Successfully authenticated with Withings."

View File

@ -8,7 +8,11 @@
"abort": { "abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"

View File

@ -16,7 +16,10 @@
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "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%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "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": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"
@ -36,21 +39,11 @@
}, },
"entity": { "entity": {
"switch": { "switch": {
"usb_ports": { "usb_ports": { "name": "USB ports" },
"name": "USB ports" "plug_1": { "name": "Plug 1" },
}, "plug_2": { "name": "Plug 2" },
"plug_1": { "plug_3": { "name": "Plug 3" },
"name": "Plug 1" "plug_4": { "name": "Plug 4" }
},
"plug_2": {
"name": "Plug 2"
},
"plug_3": {
"name": "Plug 3"
},
"plug_4": {
"name": "Plug 4"
}
}, },
"sensor": { "sensor": {
"power_failure_alarm": { "power_failure_alarm": {
@ -63,18 +56,11 @@
}, },
"power_failure_alarm_mute": { "power_failure_alarm_mute": {
"name": "Power failure alarm mute", "name": "Power failure alarm mute",
"state": { "state": { "muted": "Muted", "unmuted": "Unmuted" }
"muted": "Muted",
"unmuted": "Unmuted"
}
}, },
"power_failure_alarm_volume": { "power_failure_alarm_volume": {
"name": "Power failure alarm volume", "name": "Power failure alarm volume",
"state": { "state": { "low": "Low", "medium": "Medium", "high": "High" }
"low": "Low",
"medium": "Medium",
"high": "High"
}
}, },
"power_failure_alarm_beep": { "power_failure_alarm_beep": {
"name": "Power failure alarm beep", "name": "Power failure alarm beep",

View File

@ -6,7 +6,11 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"no_subscriptions": "You need to be subscribed to YouTube channels in order to add them.", "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": { "error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
@ -15,9 +19,7 @@
"step": { "step": {
"channels": { "channels": {
"description": "Select the channels you want to add.", "description": "Select the channels you want to add.",
"data": { "data": { "channels": "YouTube channels" }
"channels": "YouTube channels"
}
}, },
"reauth_confirm": { "reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]", "title": "[%key:common::config_flow::title::reauth%]",
@ -40,17 +42,11 @@
"latest_upload": { "latest_upload": {
"name": "Latest upload", "name": "Latest upload",
"state_attributes": { "state_attributes": {
"video_id": { "video_id": { "name": "Video ID" },
"name": "Video ID" "published_at": { "name": "Published at" }
},
"published_at": {
"name": "Published at"
}
} }
}, },
"subscribers": { "subscribers": { "name": "Subscribers" }
"name": "Subscribers"
}
} }
} }
} }

View File

@ -10,12 +10,14 @@ from __future__ import annotations
from abc import ABC, ABCMeta, abstractmethod from abc import ABC, ABCMeta, abstractmethod
import asyncio import asyncio
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from http import HTTPStatus
from json import JSONDecodeError
import logging import logging
import secrets import secrets
import time import time
from typing import Any, cast from typing import Any, cast
from aiohttp import client, web from aiohttp import ClientError, ClientResponseError, client, web
import jwt import jwt
import voluptuous as vol import voluptuous as vol
from yarl import URL from yarl import URL
@ -199,12 +201,15 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation):
_LOGGER.debug("Sending token request to %s", self.token_url) _LOGGER.debug("Sending token request to %s", self.token_url)
resp = await session.post(self.token_url, data=data) resp = await session.post(self.token_url, data=data)
if resp.status >= 400 and _LOGGER.isEnabledFor(logging.DEBUG): if resp.status >= 400:
body = await resp.text() try:
_LOGGER.debug( error_response = await resp.json()
"Token request failed with status=%s, body=%s", except (ClientError, JSONDecodeError):
resp.status, error_response = {}
body, 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() resp.raise_for_status()
return cast(dict, await resp.json()) return cast(dict, await resp.json())
@ -317,7 +322,14 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta):
) )
except asyncio.TimeoutError as err: except asyncio.TimeoutError as err:
_LOGGER.error("Timeout resolving OAuth token: %s", 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: if "expires_in" not in token:
_LOGGER.warning("Invalid token: %s", token) _LOGGER.warning("Invalid token: %s", token)

View File

@ -126,6 +126,8 @@
"oauth2_authorize_url_timeout": "Timeout generating authorize URL.", "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_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_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", "reauth_successful": "Re-authentication was successful",
"unknown_authorize_url_generation": "Unknown error generating an authorize URL.", "unknown_authorize_url_generation": "Unknown error generating an authorize URL.",
"cloud_not_connected": "Not connected to Home Assistant Cloud." "cloud_not_connected": "Not connected to Home Assistant Cloud."

View File

@ -185,6 +185,9 @@ def _custom_tasks(template, info: Info) -> None:
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", "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%]", "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%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",

View File

@ -1,6 +1,7 @@
"""Test the Google Nest Device Access config flow.""" """Test the Google Nest Device Access config flow."""
from __future__ import annotations from __future__ import annotations
from http import HTTPStatus
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import patch
@ -27,7 +28,9 @@ from .common import (
SUBSCRIBER_ID, SUBSCRIBER_ID,
TEST_CONFIG_APP_CREDS, TEST_CONFIG_APP_CREDS,
TEST_CONFIGFLOW_APP_CREDS, TEST_CONFIGFLOW_APP_CREDS,
FakeSubscriber,
NestTestConfig, NestTestConfig,
PlatformSetup,
) )
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -92,8 +95,6 @@ class OAuthFixture:
assert resp.status == 200 assert resp.status == 200
assert resp.headers["content-type"] == "text/html; charset=utf-8" 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: async def async_reauth(self, config_entry: ConfigEntry) -> dict:
"""Initiate a reuath flow.""" """Initiate a reuath flow."""
config_entry.async_start_reauth(self.hass) config_entry.async_start_reauth(self.hass)
@ -137,7 +138,7 @@ class OAuthFixture:
"&access_type=offline&prompt=consent" "&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.""" """Finish the OAuth flow exchanging auth token for refresh token."""
self.aioclient_mock.post( self.aioclient_mock.post(
OAUTH2_TOKEN, OAUTH2_TOKEN,
@ -202,6 +203,7 @@ async def test_app_credentials(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
await oauth.async_app_creds_flow(result) await oauth.async_app_creds_flow(result)
oauth.async_mock_refresh()
entry = await oauth.async_finish_setup(result) entry = await oauth.async_finish_setup(result)
@ -235,6 +237,7 @@ async def test_config_flow_restart(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
await oauth.async_app_creds_flow(result) await oauth.async_app_creds_flow(result)
oauth.async_mock_refresh()
# At this point, we should have a valid auth implementation configured. # At this point, we should have a valid auth implementation configured.
# Simulate aborting the flow and starting over to ensure we get prompted # 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"}) result = await oauth.async_configure(result, {"project_id": "new-project-id"})
await oauth.async_oauth_web_flow(result, "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"}) 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}) result = await oauth.async_configure(result, {"project_id": PROJECT_ID})
await oauth.async_oauth_web_flow(result) await oauth.async_oauth_web_flow(result)
await hass.async_block_till_done() await hass.async_block_till_done()
oauth.async_mock_refresh()
entry = await oauth.async_finish_setup(result, {"code": "1234"}) 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} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
await oauth.async_app_creds_flow(result) await oauth.async_app_creds_flow(result)
oauth.async_mock_refresh()
mock_subscriber.create_subscription.side_effect = ConfigurationException mock_subscriber.create_subscription.side_effect = ConfigurationException
result = await oauth.async_configure(result, {"code": "1234"}) 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} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
await oauth.async_app_creds_flow(result) await oauth.async_app_creds_flow(result)
oauth.async_mock_refresh()
mock_subscriber.create_subscription.side_effect = SubscriberException() mock_subscriber.create_subscription.side_effect = SubscriberException()
result = await oauth.async_configure(result, {"code": "1234"}) 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} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
await oauth.async_app_creds_flow(result, project_id="project-id-2") await oauth.async_app_creds_flow(result, project_id="project-id-2")
oauth.async_mock_refresh()
entry = await oauth.async_finish_setup(result) entry = await oauth.async_finish_setup(result)
assert entry.title == "Mock Title" assert entry.title == "Mock Title"
assert "token" in entry.data assert "token" in entry.data
@ -442,6 +450,7 @@ async def test_reauth_multiple_config_entries(
result = await oauth.async_reauth(config_entry) result = await oauth.async_reauth(config_entry)
await oauth.async_oauth_web_flow(result) await oauth.async_oauth_web_flow(result)
oauth.async_mock_refresh()
await oauth.async_finish_setup(result) await oauth.async_finish_setup(result)
@ -479,6 +488,7 @@ async def test_pubsub_subscription_strip_whitespace(
await oauth.async_app_creds_flow( await oauth.async_app_creds_flow(
result, cloud_project_id=" " + CLOUD_PROJECT_ID + " " result, cloud_project_id=" " + CLOUD_PROJECT_ID + " "
) )
oauth.async_mock_refresh()
entry = await oauth.async_finish_setup(result, {"code": "1234"}) entry = await oauth.async_finish_setup(result, {"code": "1234"})
assert entry.title == "Import from configuration.yaml" 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() mock_subscriber.create_subscription.side_effect = AuthException()
await oauth.async_app_creds_flow(result) await oauth.async_app_creds_flow(result)
oauth.async_mock_refresh()
result = await oauth.async_configure(result, {"code": "1234"}) result = await oauth.async_configure(result, {"code": "1234"})
assert result["type"] == "abort" assert result["type"] == "abort"
@ -527,6 +538,7 @@ async def test_pubsub_subscriber_config_entry_reauth(
result = await oauth.async_reauth(config_entry) result = await oauth.async_reauth(config_entry)
await oauth.async_oauth_web_flow(result) await oauth.async_oauth_web_flow(result)
oauth.async_mock_refresh()
# Entering an updated access token refreshes the config entry. # Entering an updated access token refreshes the config entry.
entry = await oauth.async_finish_setup(result, {"code": "1234"}) 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} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
await oauth.async_app_creds_flow(result) await oauth.async_app_creds_flow(result)
oauth.async_mock_refresh()
entry = await oauth.async_finish_setup(result, {"code": "1234"}) entry = await oauth.async_finish_setup(result, {"code": "1234"})
assert entry.title == "Example Home" assert entry.title == "Example Home"
@ -613,6 +626,7 @@ async def test_config_entry_title_multiple_homes(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
await oauth.async_app_creds_flow(result) await oauth.async_app_creds_flow(result)
oauth.async_mock_refresh()
entry = await oauth.async_finish_setup(result, {"code": "1234"}) entry = await oauth.async_finish_setup(result, {"code": "1234"})
assert entry.title == "Example Home #1, Example Home #2" 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} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
await oauth.async_app_creds_flow(result) await oauth.async_app_creds_flow(result)
oauth.async_mock_refresh()
mock_subscriber.async_get_device_manager.side_effect = AuthException() mock_subscriber.async_get_device_manager.side_effect = AuthException()
entry = await oauth.async_finish_setup(result, {"code": "1234"}) 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} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
await oauth.async_app_creds_flow(result) await oauth.async_app_creds_flow(result)
oauth.async_mock_refresh()
entry = await oauth.async_finish_setup(result, {"code": "1234"}) entry = await oauth.async_finish_setup(result, {"code": "1234"})
# Fallback to default name # 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}) result = await oauth.async_configure(result, {"project_id": PROJECT_ID})
await oauth.async_oauth_web_flow(result) await oauth.async_oauth_web_flow(result)
oauth.async_mock_refresh()
entry = await oauth.async_finish_setup(result, {"code": "1234"}) entry = await oauth.async_finish_setup(result, {"code": "1234"})
await hass.async_block_till_done() await hass.async_block_till_done()
@ -726,3 +743,36 @@ async def test_dhcp_discovery_with_creds(
"type": "Bearer", "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

View File

@ -1,7 +1,9 @@
"""Tests for the Somfy config flow.""" """Tests for the Somfy config flow."""
import asyncio import asyncio
from http import HTTPStatus
import logging import logging
import time import time
from typing import Any
from unittest.mock import patch from unittest.mock import patch
import aiohttp 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"]) result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == data_entry_flow.FlowResultType.ABORT 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: 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" 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( async def test_abort_discovered_existing_entries(
hass: HomeAssistant, flow_handler, local_impl hass: HomeAssistant, flow_handler, local_impl
) -> None: ) -> None:

View File

@ -7,7 +7,11 @@ from unittest import mock
from urllib.parse import parse_qs from urllib.parse import parse_qs
from aiohttp import ClientSession 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 aiohttp.streams import StreamReader
from multidict import CIMultiDict from multidict import CIMultiDict
from yarl import URL from yarl import URL
@ -53,6 +57,7 @@ class AiohttpClientMocker:
exc=None, exc=None,
cookies=None, cookies=None,
side_effect=None, side_effect=None,
closing=None,
): ):
"""Mock a request.""" """Mock a request."""
if not isinstance(url, RETYPE): if not isinstance(url, RETYPE):
@ -72,6 +77,7 @@ class AiohttpClientMocker:
exc=exc, exc=exc,
headers=headers, headers=headers,
side_effect=side_effect, side_effect=side_effect,
closing=closing,
) )
) )
@ -165,6 +171,7 @@ class AiohttpClientMockResponse:
exc=None, exc=None,
headers=None, headers=None,
side_effect=None, side_effect=None,
closing=None,
): ):
"""Initialize a fake response.""" """Initialize a fake response."""
if json is not None: if json is not None:
@ -178,9 +185,10 @@ class AiohttpClientMockResponse:
self.method = method self.method = method
self._url = url self._url = url
self.status = status self.status = status
self.response = response self._response = response
self.exc = exc self.exc = exc
self.side_effect = side_effect self.side_effect = side_effect
self.closing = closing
self._headers = CIMultiDict(headers or {}) self._headers = CIMultiDict(headers or {})
self._cookies = {} self._cookies = {}
@ -272,6 +280,13 @@ class AiohttpClientMockResponse:
def close(self): def close(self):
"""Mock close.""" """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 @contextmanager
def mock_aiohttp_client(): def mock_aiohttp_client():