From 571ab5a97839a17c41b857fee1220925ce116590 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 1 Oct 2019 10:20:30 -0500 Subject: [PATCH] Plex external config flow (#26936) * Plex external auth config flow * Update requirements_all * Test dependency * Bad await, delay variable * Use hass aiohttp session, bump plexauth * Bump requirements * Bump library version again * Use callback view instead of polling * Update tests for callback view * Reduce timeout with callback * Review feedback * F-string * Wrap sync call * Unused * Revert unnecessary async wrap --- homeassistant/components/plex/config_flow.py | 78 ++++++- homeassistant/components/plex/const.py | 10 + homeassistant/components/plex/manifest.json | 3 +- homeassistant/components/plex/server.py | 12 + homeassistant/components/plex/strings.json | 7 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/plex/mock_classes.py | 4 +- tests/components/plex/test_config_flow.py | 223 +++++++++++++------ 10 files changed, 267 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index cf70b7470cd..dd5401950e9 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -2,10 +2,14 @@ import copy import logging +from aiohttp import web_response import plexapi.exceptions +from plexauth import PlexAuth import requests.exceptions import voluptuous as vol +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import ( @@ -20,6 +24,8 @@ from homeassistant.core import callback from homeassistant.util.json import load_json from .const import ( # pylint: disable=unused-import + AUTH_CALLBACK_NAME, + AUTH_CALLBACK_PATH, CONF_SERVER, CONF_SERVER_IDENTIFIER, CONF_USE_EPISODE_ART, @@ -30,13 +36,15 @@ from .const import ( # pylint: disable=unused-import DOMAIN, PLEX_CONFIG_FILE, PLEX_SERVER_CONFIG, + X_PLEX_DEVICE_NAME, + X_PLEX_VERSION, + X_PLEX_PRODUCT, + X_PLEX_PLATFORM, ) from .errors import NoServersFound, ServerNotSpecified from .server import PlexServer -USER_SCHEMA = vol.Schema( - {vol.Optional(CONF_TOKEN): str, vol.Optional("manual_setup"): bool} -) +USER_SCHEMA = vol.Schema({vol.Optional("manual_setup"): bool}) _LOGGER = logging.getLogger(__package__) @@ -67,6 +75,8 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.current_login = {} self.discovery_info = {} self.available_servers = None + self.plexauth = None + self.token = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" @@ -74,9 +84,8 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: if user_input.pop("manual_setup", False): return await self.async_step_manual_setup(user_input) - if CONF_TOKEN in user_input: - return await self.async_step_server_validate(user_input) - errors[CONF_TOKEN] = "no_token" + + return await self.async_step_plex_website_auth() return self.async_show_form( step_id="user", data_schema=USER_SCHEMA, errors=errors @@ -225,6 +234,43 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug("Imported Plex configuration") return await self.async_step_server_validate(import_config) + async def async_step_plex_website_auth(self): + """Begin external auth flow on Plex website.""" + self.hass.http.register_view(PlexAuthorizationCallbackView) + payload = { + "X-Plex-Device-Name": X_PLEX_DEVICE_NAME, + "X-Plex-Version": X_PLEX_VERSION, + "X-Plex-Product": X_PLEX_PRODUCT, + "X-Plex-Device": self.hass.config.location_name, + "X-Plex-Platform": X_PLEX_PLATFORM, + "X-Plex-Model": "Plex OAuth", + } + session = async_get_clientsession(self.hass) + self.plexauth = PlexAuth(payload, session) + await self.plexauth.initiate_auth() + forward_url = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}?flow_id={self.flow_id}" + auth_url = self.plexauth.auth_url(forward_url) + return self.async_external_step(step_id="obtain_token", url=auth_url) + + async def async_step_obtain_token(self, user_input=None): + """Obtain token after external auth completed.""" + token = await self.plexauth.token(10) + + if not token: + return self.async_external_step_done(next_step_id="timed_out") + + self.token = token + return self.async_external_step_done(next_step_id="use_external_token") + + async def async_step_timed_out(self, user_input=None): + """Abort flow when time expires.""" + return self.async_abort(reason="token_request_timeout") + + async def async_step_use_external_token(self, user_input=None): + """Continue server validation with external token.""" + server_config = {CONF_TOKEN: self.token} + return await self.async_step_server_validate(server_config) + class PlexOptionsFlowHandler(config_entries.OptionsFlow): """Handle Plex options.""" @@ -263,3 +309,23 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow): } ), ) + + +class PlexAuthorizationCallbackView(HomeAssistantView): + """Handle callback from external auth.""" + + url = AUTH_CALLBACK_PATH + name = AUTH_CALLBACK_NAME + requires_auth = False + + async def get(self, request): + """Receive authorization confirmation.""" + hass = request.app["hass"] + await hass.config_entries.flow.async_configure( + flow_id=request.query["flow_id"], user_input=None + ) + + return web_response.Response( + headers={"content-type": "text/html"}, + text="Success! This window can be closed", + ) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 478dd3754e7..0b436c4e208 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -1,4 +1,6 @@ """Constants for the Plex component.""" +from homeassistant.const import __version__ + DOMAIN = "plex" NAME_FORMAT = "Plex {}" @@ -18,3 +20,11 @@ CONF_SERVER = "server" CONF_SERVER_IDENTIFIER = "server_id" CONF_USE_EPISODE_ART = "use_episode_art" CONF_SHOW_ALL_CONTROLS = "show_all_controls" + +AUTH_CALLBACK_PATH = "/auth/plex/callback" +AUTH_CALLBACK_NAME = "auth:plex:callback" + +X_PLEX_DEVICE_NAME = "Home Assistant" +X_PLEX_PLATFORM = "Home Assistant" +X_PLEX_PRODUCT = "Home Assistant" +X_PLEX_VERSION = __version__ diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 94d990952a6..137619b27b0 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,8 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/plex", "requirements": [ - "plexapi==3.0.6" + "plexapi==3.0.6", + "plexauth==0.0.4" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 09274472915..d4393d38c97 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -11,9 +11,21 @@ from .const import ( CONF_SHOW_ALL_CONTROLS, CONF_USE_EPISODE_ART, DEFAULT_VERIFY_SSL, + X_PLEX_DEVICE_NAME, + X_PLEX_PLATFORM, + X_PLEX_PRODUCT, + X_PLEX_VERSION, ) from .errors import NoServersFound, ServerNotSpecified +# Set default headers sent by plexapi +plexapi.X_PLEX_DEVICE_NAME = X_PLEX_DEVICE_NAME +plexapi.X_PLEX_PLATFORM = X_PLEX_PLATFORM +plexapi.X_PLEX_PRODUCT = X_PLEX_PRODUCT +plexapi.X_PLEX_VERSION = X_PLEX_VERSION +plexapi.myplex.BASE_HEADERS = plexapi.reset_base_headers() +plexapi.server.BASE_HEADERS = plexapi.reset_base_headers() + class PlexServer: """Manages a single Plex server connection.""" diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index 812e7b81a7c..6538d8e887e 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -21,9 +21,8 @@ }, "user": { "title": "Connect Plex server", - "description": "Enter a Plex token for automatic setup or manually configure a server.", + "description": "Continue to authorize at plex.tv or manually configure a server.", "data": { - "token": "Plex token", "manual_setup": "Manual setup" } } @@ -31,14 +30,14 @@ "error": { "faulty_credentials": "Authorization failed", "no_servers": "No servers linked to account", - "not_found": "Plex server not found", - "no_token": "Provide a token or select manual setup" + "not_found": "Plex server not found" }, "abort": { "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", "invalid_import": "Imported configuration is invalid", + "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" } }, diff --git a/requirements_all.txt b/requirements_all.txt index ef1b56222b5..ee439d45592 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -964,6 +964,9 @@ pizzapi==0.0.3 # homeassistant.components.plex plexapi==3.0.6 +# homeassistant.components.plex +plexauth==0.0.4 + # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7e4ed37e00..949da3ad402 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -261,6 +261,9 @@ pillow==6.1.0 # homeassistant.components.plex plexapi==3.0.6 +# homeassistant.components.plex +plexauth==0.0.4 + # homeassistant.components.mhz19 # homeassistant.components.serial_pm pmsensor==0.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9991a6bc1f0..e35a83bd24d 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -113,6 +113,7 @@ TEST_REQUIREMENTS = ( "pilight", "pillow", "plexapi", + "plexauth", "pmsensor", "prometheus_client", "ptvsd", diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index d0270878280..87fb6df5971 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -1,9 +1,9 @@ """Mock classes used in tests.""" MOCK_HOST_1 = "1.2.3.4" -MOCK_PORT_1 = "32400" +MOCK_PORT_1 = 32400 MOCK_HOST_2 = "4.3.2.1" -MOCK_PORT_2 = "32400" +MOCK_PORT_2 = 32400 class MockAvailableServer: # pylint: disable=too-few-public-methods diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 37cf0fa200c..753d565a82b 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for Plex config flow.""" from unittest.mock import MagicMock, Mock, patch, PropertyMock + +import asynctest import plexapi.exceptions import requests.exceptions @@ -12,6 +14,7 @@ from homeassistant.const import ( CONF_TOKEN, CONF_URL, ) +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -49,19 +52,28 @@ async def test_bad_credentials(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" - with patch( - "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized - ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"manual_setup": True} + ) + assert result["type"] == "form" + assert result["step_id"] == "manual_setup" + with patch( + "plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + user_input={ + CONF_HOST: MOCK_HOST_1, + CONF_PORT: MOCK_PORT_1, + CONF_SSL: False, + CONF_VERIFY_SSL: False, + CONF_TOKEN: "BAD TOKEN", + }, ) - assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"]["base"] == "faulty_credentials" @@ -92,7 +104,6 @@ async def test_import_file_from_discovery(hass): context={"source": "discovery"}, data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, ) - assert result["type"] == "create_entry" assert result["title"] == MOCK_NAME_1 assert result["data"][config_flow.CONF_SERVER] == MOCK_NAME_1 @@ -112,7 +123,6 @@ async def test_discovery(hass): context={"source": "discovery"}, data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, ) - assert result["type"] == "form" assert result["step_id"] == "user" @@ -129,7 +139,6 @@ async def test_discovery_while_in_progress(hass): context={"source": "discovery"}, data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, ) - assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -191,7 +200,6 @@ async def test_import_bad_hostname(hass): CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}", }, ) - assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"]["base"] == "not_found" @@ -203,15 +211,25 @@ async def test_unknown_exception(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" - with patch("plexapi.myplex.MyPlexAccount", side_effect=Exception): - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": "user"}, - data={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"manual_setup": True} + ) + assert result["type"] == "form" + assert result["step_id"] == "manual_setup" + + with patch("plexapi.server.PlexServer", side_effect=Exception): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: MOCK_HOST_1, + CONF_PORT: MOCK_PORT_1, + CONF_SSL: True, + CONF_VERIFY_SSL: True, + CONF_TOKEN: MOCK_TOKEN, + }, ) assert result["type"] == "abort" @@ -221,23 +239,32 @@ async def test_unknown_exception(hass): async def test_no_servers_found(hass): """Test when no servers are on an account.""" + await async_setup_component(hass, "http", {"http": {}}) + result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" mm_plex_account = MagicMock() mm_plex_account.resources = Mock(return_value=[]) - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account): + with patch( + "plexapi.myplex.MyPlexAccount", return_value=mm_plex_account + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result["flow_id"], user_input={"manual_setup": False} ) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"]["base"] == "no_servers" @@ -246,10 +273,11 @@ async def test_no_servers_found(hass): async def test_single_available_server(hass): """Test creating an entry with one server available.""" + await async_setup_component(hass, "http", {"http": {}}) + result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" @@ -261,7 +289,11 @@ async def test_single_available_server(hass): with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( "plexapi.server.PlexServer" - ) as mock_plex_server: + ) as mock_plex_server, asynctest.patch( + "plexauth.PlexAuth.initiate_auth" + ), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): type(mock_plex_server.return_value).machineIdentifier = PropertyMock( return_value=MOCK_SERVER_1.clientIdentifier ) @@ -273,10 +305,14 @@ async def test_single_available_server(hass): )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result["flow_id"], user_input={"manual_setup": False} ) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "create_entry" assert result["title"] == MOCK_SERVER_1.name assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name @@ -294,10 +330,11 @@ async def test_single_available_server(hass): async def test_multiple_servers_with_selection(hass): """Test creating an entry with multiple servers available.""" + await async_setup_component(hass, "http", {"http": {}}) + result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" @@ -308,7 +345,11 @@ async def test_multiple_servers_with_selection(hass): with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( "plexapi.server.PlexServer" - ) as mock_plex_server: + ) as mock_plex_server, asynctest.patch( + "plexauth.PlexAuth.initiate_auth" + ), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): type(mock_plex_server.return_value).machineIdentifier = PropertyMock( return_value=MOCK_SERVER_1.clientIdentifier ) @@ -320,17 +361,20 @@ async def test_multiple_servers_with_selection(hass): )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result["flow_id"], user_input={"manual_setup": False} ) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" assert result["step_id"] == "select_server" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={config_flow.CONF_SERVER: MOCK_SERVER_1.name} ) - assert result["type"] == "create_entry" assert result["title"] == MOCK_SERVER_1.name assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name @@ -348,6 +392,8 @@ async def test_multiple_servers_with_selection(hass): async def test_adding_last_unconfigured_server(hass): """Test automatically adding last unconfigured server when multiple servers on account.""" + await async_setup_component(hass, "http", {"http": {}}) + MockConfigEntry( domain=config_flow.DOMAIN, data={ @@ -359,7 +405,6 @@ async def test_adding_last_unconfigured_server(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" @@ -370,7 +415,11 @@ async def test_adding_last_unconfigured_server(hass): with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( "plexapi.server.PlexServer" - ) as mock_plex_server: + ) as mock_plex_server, asynctest.patch( + "plexauth.PlexAuth.initiate_auth" + ), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): type(mock_plex_server.return_value).machineIdentifier = PropertyMock( return_value=MOCK_SERVER_1.clientIdentifier ) @@ -382,10 +431,14 @@ async def test_adding_last_unconfigured_server(hass): )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result["flow_id"], user_input={"manual_setup": False} ) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "create_entry" assert result["title"] == MOCK_SERVER_1.name assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name @@ -414,7 +467,9 @@ async def test_already_configured(hass): mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) mm_plex_account.resource = Mock(return_value=mock_connections) - with patch("plexapi.server.PlexServer") as mock_plex_server: + with patch("plexapi.server.PlexServer") as mock_plex_server, asynctest.patch( + "plexauth.PlexAuth.initiate_auth" + ), asynctest.patch("plexauth.PlexAuth.token", return_value=MOCK_TOKEN): type(mock_plex_server.return_value).machineIdentifier = PropertyMock( return_value=MOCK_SERVER_1.clientIdentifier ) @@ -424,10 +479,10 @@ async def test_already_configured(hass): type( # pylint: disable=protected-access mock_plex_server.return_value )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + result = await flow.async_step_import( {CONF_TOKEN: MOCK_TOKEN, CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}"} ) - assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -435,6 +490,8 @@ async def test_already_configured(hass): async def test_all_available_servers_configured(hass): """Test when all available servers are already configured.""" + await async_setup_component(hass, "http", {"http": {}}) + MockConfigEntry( domain=config_flow.DOMAIN, data={ @@ -454,7 +511,6 @@ async def test_all_available_servers_configured(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" @@ -463,13 +519,21 @@ async def test_all_available_servers_configured(hass): mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) mm_plex_account.resource = Mock(return_value=mock_connections) - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account): + with patch( + "plexapi.myplex.MyPlexAccount", return_value=mm_plex_account + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result["flow_id"], user_input={"manual_setup": False} ) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "abort" assert result["reason"] == "all_configured" @@ -480,14 +544,12 @@ async def test_manual_config(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_TOKEN: "", "manual_setup": True} + result["flow_id"], user_input={"manual_setup": True} ) - assert result["type"] == "form" assert result["step_id"] == "manual_setup" @@ -508,13 +570,12 @@ async def test_manual_config(hass): result["flow_id"], user_input={ CONF_HOST: MOCK_HOST_1, - CONF_PORT: int(MOCK_PORT_1), + CONF_PORT: MOCK_PORT_1, CONF_SSL: True, CONF_VERIFY_SSL: True, CONF_TOKEN: MOCK_TOKEN, }, ) - assert result["type"] == "create_entry" assert result["title"] == MOCK_SERVER_1.name assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name @@ -529,25 +590,6 @@ async def test_manual_config(hass): assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN -async def test_no_token(hass): - """Test failing when no token provided.""" - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} - ) - - assert result["type"] == "form" - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) - - assert result["type"] == "form" - assert result["step_id"] == "user" - assert result["errors"][CONF_TOKEN] == "no_token" - - async def test_option_flow(hass): """Test config flow selection of one of two bridges.""" @@ -557,7 +599,6 @@ async def test_option_flow(hass): result = await hass.config_entries.options.flow.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == "form" assert result["step_id"] == "plex_mp_settings" @@ -575,3 +616,57 @@ async def test_option_flow(hass): config_flow.CONF_SHOW_ALL_CONTROLS: True, } } + + +async def test_external_timed_out(hass): + """Test when external flow times out.""" + + await async_setup_component(hass, "http", {"http": {}}) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + + with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=None + ): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"manual_setup": False} + ) + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "abort" + assert result["reason"] == "token_request_timeout" + + +async def test_callback_view(hass, aiohttp_client): + """Test callback view.""" + + await async_setup_component(hass, "http", {"http": {}}) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + + with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"manual_setup": False} + ) + assert result["type"] == "external" + + client = await aiohttp_client(hass.http.app) + forward_url = f'{config_flow.AUTH_CALLBACK_PATH}?flow_id={result["flow_id"]}' + + resp = await client.get(forward_url) + assert resp.status == 200