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
This commit is contained in:
jjlawren 2019-10-01 10:20:30 -05:00 committed by Martin Hjelmare
parent c1851a2d94
commit 571ab5a978
10 changed files with 267 additions and 77 deletions

View File

@ -2,10 +2,14 @@
import copy import copy
import logging import logging
from aiohttp import web_response
import plexapi.exceptions import plexapi.exceptions
from plexauth import PlexAuth
import requests.exceptions import requests.exceptions
import voluptuous as vol 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 import config_entries
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.const import ( from homeassistant.const import (
@ -20,6 +24,8 @@ from homeassistant.core import callback
from homeassistant.util.json import load_json from homeassistant.util.json import load_json
from .const import ( # pylint: disable=unused-import from .const import ( # pylint: disable=unused-import
AUTH_CALLBACK_NAME,
AUTH_CALLBACK_PATH,
CONF_SERVER, CONF_SERVER,
CONF_SERVER_IDENTIFIER, CONF_SERVER_IDENTIFIER,
CONF_USE_EPISODE_ART, CONF_USE_EPISODE_ART,
@ -30,13 +36,15 @@ from .const import ( # pylint: disable=unused-import
DOMAIN, DOMAIN,
PLEX_CONFIG_FILE, PLEX_CONFIG_FILE,
PLEX_SERVER_CONFIG, PLEX_SERVER_CONFIG,
X_PLEX_DEVICE_NAME,
X_PLEX_VERSION,
X_PLEX_PRODUCT,
X_PLEX_PLATFORM,
) )
from .errors import NoServersFound, ServerNotSpecified from .errors import NoServersFound, ServerNotSpecified
from .server import PlexServer from .server import PlexServer
USER_SCHEMA = vol.Schema( USER_SCHEMA = vol.Schema({vol.Optional("manual_setup"): bool})
{vol.Optional(CONF_TOKEN): str, vol.Optional("manual_setup"): bool}
)
_LOGGER = logging.getLogger(__package__) _LOGGER = logging.getLogger(__package__)
@ -67,6 +75,8 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.current_login = {} self.current_login = {}
self.discovery_info = {} self.discovery_info = {}
self.available_servers = None self.available_servers = None
self.plexauth = None
self.token = None
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user.""" """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 is not None:
if user_input.pop("manual_setup", False): if user_input.pop("manual_setup", False):
return await self.async_step_manual_setup(user_input) return await self.async_step_manual_setup(user_input)
if CONF_TOKEN in user_input:
return await self.async_step_server_validate(user_input) return await self.async_step_plex_website_auth()
errors[CONF_TOKEN] = "no_token"
return self.async_show_form( return self.async_show_form(
step_id="user", data_schema=USER_SCHEMA, errors=errors 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") _LOGGER.debug("Imported Plex configuration")
return await self.async_step_server_validate(import_config) 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): class PlexOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Plex options.""" """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="<script>window.close()</script>Success! This window can be closed",
)

View File

@ -1,4 +1,6 @@
"""Constants for the Plex component.""" """Constants for the Plex component."""
from homeassistant.const import __version__
DOMAIN = "plex" DOMAIN = "plex"
NAME_FORMAT = "Plex {}" NAME_FORMAT = "Plex {}"
@ -18,3 +20,11 @@ CONF_SERVER = "server"
CONF_SERVER_IDENTIFIER = "server_id" CONF_SERVER_IDENTIFIER = "server_id"
CONF_USE_EPISODE_ART = "use_episode_art" CONF_USE_EPISODE_ART = "use_episode_art"
CONF_SHOW_ALL_CONTROLS = "show_all_controls" 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__

View File

@ -4,7 +4,8 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/components/plex", "documentation": "https://www.home-assistant.io/components/plex",
"requirements": [ "requirements": [
"plexapi==3.0.6" "plexapi==3.0.6",
"plexauth==0.0.4"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [ "codeowners": [

View File

@ -11,9 +11,21 @@ from .const import (
CONF_SHOW_ALL_CONTROLS, CONF_SHOW_ALL_CONTROLS,
CONF_USE_EPISODE_ART, CONF_USE_EPISODE_ART,
DEFAULT_VERIFY_SSL, DEFAULT_VERIFY_SSL,
X_PLEX_DEVICE_NAME,
X_PLEX_PLATFORM,
X_PLEX_PRODUCT,
X_PLEX_VERSION,
) )
from .errors import NoServersFound, ServerNotSpecified 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: class PlexServer:
"""Manages a single Plex server connection.""" """Manages a single Plex server connection."""

View File

@ -21,9 +21,8 @@
}, },
"user": { "user": {
"title": "Connect Plex server", "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": { "data": {
"token": "Plex token",
"manual_setup": "Manual setup" "manual_setup": "Manual setup"
} }
} }
@ -31,14 +30,14 @@
"error": { "error": {
"faulty_credentials": "Authorization failed", "faulty_credentials": "Authorization failed",
"no_servers": "No servers linked to account", "no_servers": "No servers linked to account",
"not_found": "Plex server not found", "not_found": "Plex server not found"
"no_token": "Provide a token or select manual setup"
}, },
"abort": { "abort": {
"all_configured": "All linked servers already configured", "all_configured": "All linked servers already configured",
"already_configured": "This Plex server is already configured", "already_configured": "This Plex server is already configured",
"already_in_progress": "Plex is being configured", "already_in_progress": "Plex is being configured",
"invalid_import": "Imported configuration is invalid", "invalid_import": "Imported configuration is invalid",
"token_request_timeout": "Timed out obtaining token",
"unknown": "Failed for unknown reason" "unknown": "Failed for unknown reason"
} }
}, },

View File

@ -964,6 +964,9 @@ pizzapi==0.0.3
# homeassistant.components.plex # homeassistant.components.plex
plexapi==3.0.6 plexapi==3.0.6
# homeassistant.components.plex
plexauth==0.0.4
# homeassistant.components.plum_lightpad # homeassistant.components.plum_lightpad
plumlightpad==0.0.11 plumlightpad==0.0.11

View File

@ -261,6 +261,9 @@ pillow==6.1.0
# homeassistant.components.plex # homeassistant.components.plex
plexapi==3.0.6 plexapi==3.0.6
# homeassistant.components.plex
plexauth==0.0.4
# homeassistant.components.mhz19 # homeassistant.components.mhz19
# homeassistant.components.serial_pm # homeassistant.components.serial_pm
pmsensor==0.4 pmsensor==0.4

View File

@ -113,6 +113,7 @@ TEST_REQUIREMENTS = (
"pilight", "pilight",
"pillow", "pillow",
"plexapi", "plexapi",
"plexauth",
"pmsensor", "pmsensor",
"prometheus_client", "prometheus_client",
"ptvsd", "ptvsd",

View File

@ -1,9 +1,9 @@
"""Mock classes used in tests.""" """Mock classes used in tests."""
MOCK_HOST_1 = "1.2.3.4" MOCK_HOST_1 = "1.2.3.4"
MOCK_PORT_1 = "32400" MOCK_PORT_1 = 32400
MOCK_HOST_2 = "4.3.2.1" MOCK_HOST_2 = "4.3.2.1"
MOCK_PORT_2 = "32400" MOCK_PORT_2 = 32400
class MockAvailableServer: # pylint: disable=too-few-public-methods class MockAvailableServer: # pylint: disable=too-few-public-methods

View File

@ -1,5 +1,7 @@
"""Tests for Plex config flow.""" """Tests for Plex config flow."""
from unittest.mock import MagicMock, Mock, patch, PropertyMock from unittest.mock import MagicMock, Mock, patch, PropertyMock
import asynctest
import plexapi.exceptions import plexapi.exceptions
import requests.exceptions import requests.exceptions
@ -12,6 +14,7 @@ from homeassistant.const import (
CONF_TOKEN, CONF_TOKEN,
CONF_URL, CONF_URL,
) )
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -49,19 +52,28 @@ async def test_bad_credentials(hass):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN, context={"source": "user"} config_flow.DOMAIN, context={"source": "user"}
) )
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "user" assert result["step_id"] == "user"
with patch( result = await hass.config_entries.flow.async_configure(
"plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized 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 = await hass.config_entries.flow.async_configure(
result["flow_id"], 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["type"] == "form"
assert result["step_id"] == "user" assert result["step_id"] == "user"
assert result["errors"]["base"] == "faulty_credentials" assert result["errors"]["base"] == "faulty_credentials"
@ -92,7 +104,6 @@ async def test_import_file_from_discovery(hass):
context={"source": "discovery"}, context={"source": "discovery"},
data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1},
) )
assert result["type"] == "create_entry" assert result["type"] == "create_entry"
assert result["title"] == MOCK_NAME_1 assert result["title"] == MOCK_NAME_1
assert result["data"][config_flow.CONF_SERVER] == 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"}, context={"source": "discovery"},
data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1},
) )
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "user" assert result["step_id"] == "user"
@ -129,7 +139,6 @@ async def test_discovery_while_in_progress(hass):
context={"source": "discovery"}, context={"source": "discovery"},
data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1},
) )
assert result["type"] == "abort" assert result["type"] == "abort"
assert result["reason"] == "already_configured" 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}", CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}",
}, },
) )
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "user" assert result["step_id"] == "user"
assert result["errors"]["base"] == "not_found" assert result["errors"]["base"] == "not_found"
@ -203,15 +211,25 @@ async def test_unknown_exception(hass):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN, context={"source": "user"} config_flow.DOMAIN, context={"source": "user"}
) )
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "user" assert result["step_id"] == "user"
with patch("plexapi.myplex.MyPlexAccount", side_effect=Exception): result = await hass.config_entries.flow.async_configure(
result = await hass.config_entries.flow.async_init( result["flow_id"], user_input={"manual_setup": True}
config_flow.DOMAIN, )
context={"source": "user"}, assert result["type"] == "form"
data={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, 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" assert result["type"] == "abort"
@ -221,23 +239,32 @@ async def test_unknown_exception(hass):
async def test_no_servers_found(hass): async def test_no_servers_found(hass):
"""Test when no servers are on an account.""" """Test when no servers are on an account."""
await async_setup_component(hass, "http", {"http": {}})
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN, context={"source": "user"} config_flow.DOMAIN, context={"source": "user"}
) )
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "user" assert result["step_id"] == "user"
mm_plex_account = MagicMock() mm_plex_account = MagicMock()
mm_plex_account.resources = Mock(return_value=[]) 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 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"], user_input={"manual_setup": False}
user_input={CONF_TOKEN: MOCK_TOKEN, "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["type"] == "form"
assert result["step_id"] == "user" assert result["step_id"] == "user"
assert result["errors"]["base"] == "no_servers" assert result["errors"]["base"] == "no_servers"
@ -246,10 +273,11 @@ async def test_no_servers_found(hass):
async def test_single_available_server(hass): async def test_single_available_server(hass):
"""Test creating an entry with one server available.""" """Test creating an entry with one server available."""
await async_setup_component(hass, "http", {"http": {}})
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN, context={"source": "user"} config_flow.DOMAIN, context={"source": "user"}
) )
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "user" 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( with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch(
"plexapi.server.PlexServer" "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( type(mock_plex_server.return_value).machineIdentifier = PropertyMock(
return_value=MOCK_SERVER_1.clientIdentifier 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) )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri)
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"], user_input={"manual_setup": False}
user_input={CONF_TOKEN: MOCK_TOKEN, "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["type"] == "create_entry"
assert result["title"] == MOCK_SERVER_1.name assert result["title"] == MOCK_SERVER_1.name
assert result["data"][config_flow.CONF_SERVER] == 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): async def test_multiple_servers_with_selection(hass):
"""Test creating an entry with multiple servers available.""" """Test creating an entry with multiple servers available."""
await async_setup_component(hass, "http", {"http": {}})
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN, context={"source": "user"} config_flow.DOMAIN, context={"source": "user"}
) )
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "user" 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( with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch(
"plexapi.server.PlexServer" "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( type(mock_plex_server.return_value).machineIdentifier = PropertyMock(
return_value=MOCK_SERVER_1.clientIdentifier 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) )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri)
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"], user_input={"manual_setup": False}
user_input={CONF_TOKEN: MOCK_TOKEN, "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["type"] == "form"
assert result["step_id"] == "select_server" assert result["step_id"] == "select_server"
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={config_flow.CONF_SERVER: MOCK_SERVER_1.name} result["flow_id"], user_input={config_flow.CONF_SERVER: MOCK_SERVER_1.name}
) )
assert result["type"] == "create_entry" assert result["type"] == "create_entry"
assert result["title"] == MOCK_SERVER_1.name assert result["title"] == MOCK_SERVER_1.name
assert result["data"][config_flow.CONF_SERVER] == 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): async def test_adding_last_unconfigured_server(hass):
"""Test automatically adding last unconfigured server when multiple servers on account.""" """Test automatically adding last unconfigured server when multiple servers on account."""
await async_setup_component(hass, "http", {"http": {}})
MockConfigEntry( MockConfigEntry(
domain=config_flow.DOMAIN, domain=config_flow.DOMAIN,
data={ data={
@ -359,7 +405,6 @@ async def test_adding_last_unconfigured_server(hass):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN, context={"source": "user"} config_flow.DOMAIN, context={"source": "user"}
) )
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "user" 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( with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch(
"plexapi.server.PlexServer" "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( type(mock_plex_server.return_value).machineIdentifier = PropertyMock(
return_value=MOCK_SERVER_1.clientIdentifier 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) )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri)
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"], user_input={"manual_setup": False}
user_input={CONF_TOKEN: MOCK_TOKEN, "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["type"] == "create_entry"
assert result["title"] == MOCK_SERVER_1.name assert result["title"] == MOCK_SERVER_1.name
assert result["data"][config_flow.CONF_SERVER] == 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.resources = Mock(return_value=[MOCK_SERVER_1])
mm_plex_account.resource = Mock(return_value=mock_connections) 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( type(mock_plex_server.return_value).machineIdentifier = PropertyMock(
return_value=MOCK_SERVER_1.clientIdentifier return_value=MOCK_SERVER_1.clientIdentifier
) )
@ -424,10 +479,10 @@ async def test_already_configured(hass):
type( # pylint: disable=protected-access type( # pylint: disable=protected-access
mock_plex_server.return_value mock_plex_server.return_value
)._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri)
result = await flow.async_step_import( result = await flow.async_step_import(
{CONF_TOKEN: MOCK_TOKEN, CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}"} {CONF_TOKEN: MOCK_TOKEN, CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}"}
) )
assert result["type"] == "abort" assert result["type"] == "abort"
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
@ -435,6 +490,8 @@ async def test_already_configured(hass):
async def test_all_available_servers_configured(hass): async def test_all_available_servers_configured(hass):
"""Test when all available servers are already configured.""" """Test when all available servers are already configured."""
await async_setup_component(hass, "http", {"http": {}})
MockConfigEntry( MockConfigEntry(
domain=config_flow.DOMAIN, domain=config_flow.DOMAIN,
data={ data={
@ -454,7 +511,6 @@ async def test_all_available_servers_configured(hass):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN, context={"source": "user"} config_flow.DOMAIN, context={"source": "user"}
) )
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "user" 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.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2])
mm_plex_account.resource = Mock(return_value=mock_connections) 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 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"], user_input={"manual_setup": False}
user_input={CONF_TOKEN: MOCK_TOKEN, "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["type"] == "abort"
assert result["reason"] == "all_configured" assert result["reason"] == "all_configured"
@ -480,14 +544,12 @@ async def test_manual_config(hass):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN, context={"source": "user"} config_flow.DOMAIN, context={"source": "user"}
) )
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "user" assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure( 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["type"] == "form"
assert result["step_id"] == "manual_setup" assert result["step_id"] == "manual_setup"
@ -508,13 +570,12 @@ async def test_manual_config(hass):
result["flow_id"], result["flow_id"],
user_input={ user_input={
CONF_HOST: MOCK_HOST_1, CONF_HOST: MOCK_HOST_1,
CONF_PORT: int(MOCK_PORT_1), CONF_PORT: MOCK_PORT_1,
CONF_SSL: True, CONF_SSL: True,
CONF_VERIFY_SSL: True, CONF_VERIFY_SSL: True,
CONF_TOKEN: MOCK_TOKEN, CONF_TOKEN: MOCK_TOKEN,
}, },
) )
assert result["type"] == "create_entry" assert result["type"] == "create_entry"
assert result["title"] == MOCK_SERVER_1.name assert result["title"] == MOCK_SERVER_1.name
assert result["data"][config_flow.CONF_SERVER] == 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 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): async def test_option_flow(hass):
"""Test config flow selection of one of two bridges.""" """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( result = await hass.config_entries.options.flow.async_init(
entry.entry_id, context={"source": "test"}, data=None entry.entry_id, context={"source": "test"}, data=None
) )
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "plex_mp_settings" assert result["step_id"] == "plex_mp_settings"
@ -575,3 +616,57 @@ async def test_option_flow(hass):
config_flow.CONF_SHOW_ALL_CONTROLS: True, 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