mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Add integration step to onboarding (#23732)
* Return an extra oauth2 auth code during onboarding * Areas in const * Add integration step * Lint * Fix tests * Fix test * Verify integration added to done * Verify step is marked as done
This commit is contained in:
parent
07ee3b2eb9
commit
f4016b4aad
@ -97,8 +97,7 @@ class AuthProvidersView(HomeAssistantView):
|
||||
async def get(self, request):
|
||||
"""Get available auth providers."""
|
||||
hass = request.app['hass']
|
||||
|
||||
if not hass.components.onboarding.async_is_onboarded():
|
||||
if not hass.components.onboarding.async_is_user_onboarded():
|
||||
return self.json_message(
|
||||
message='Onboarding not finished',
|
||||
status_code=400,
|
||||
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"area": {
|
||||
"bedroom": "Bedroom",
|
||||
"kitchen": "Kitchen",
|
||||
"living_room": "Living Room"
|
||||
}
|
||||
}
|
@ -1,24 +1,42 @@
|
||||
"""Support to help onboard new users."""
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.storage import Store
|
||||
|
||||
from .const import DOMAIN, STEP_USER, STEPS
|
||||
from .const import DOMAIN, STEP_USER, STEPS, STEP_INTEGRATION
|
||||
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_VERSION = 2
|
||||
|
||||
|
||||
class OnboadingStorage(Store):
|
||||
"""Store onboarding data."""
|
||||
|
||||
async def _async_migrate_func(self, old_version, old_data):
|
||||
"""Migrate to the new version."""
|
||||
# From version 1 -> 2, we automatically mark the integration step done
|
||||
old_data['done'].append(STEP_INTEGRATION)
|
||||
return old_data
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_is_onboarded(hass):
|
||||
"""Return if Home Assistant has been onboarded."""
|
||||
return hass.data.get(DOMAIN, True)
|
||||
data = hass.data.get(DOMAIN)
|
||||
return data is None or data is True
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_is_user_onboarded(hass):
|
||||
"""Return if a user has been created as part of onboarding."""
|
||||
return async_is_onboarded(hass) or STEP_USER in hass.data[DOMAIN]['done']
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the onboarding component."""
|
||||
store = hass.helpers.storage.Store(
|
||||
STORAGE_VERSION, STORAGE_KEY, private=True)
|
||||
store = OnboadingStorage(hass, STORAGE_VERSION, STORAGE_KEY, private=True)
|
||||
data = await store.async_load()
|
||||
|
||||
if data is None:
|
||||
@ -43,7 +61,7 @@ async def async_setup(hass, config):
|
||||
if set(data['done']) == set(STEPS):
|
||||
return True
|
||||
|
||||
hass.data[DOMAIN] = False
|
||||
hass.data[DOMAIN] = data
|
||||
|
||||
from . import views
|
||||
|
||||
|
@ -1,7 +1,15 @@
|
||||
"""Constants for the onboarding component."""
|
||||
DOMAIN = 'onboarding'
|
||||
STEP_USER = 'user'
|
||||
STEP_INTEGRATION = 'integration'
|
||||
|
||||
STEPS = [
|
||||
STEP_USER
|
||||
STEP_USER,
|
||||
STEP_INTEGRATION,
|
||||
]
|
||||
|
||||
DEFAULT_AREAS = (
|
||||
'living_room',
|
||||
'kitchen',
|
||||
'bedroom',
|
||||
)
|
||||
|
7
homeassistant/components/onboarding/strings.json
Normal file
7
homeassistant/components/onboarding/strings.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"area": {
|
||||
"living_room": "Living Room",
|
||||
"bedroom": "Bedroom",
|
||||
"kitchen": "Kitchen"
|
||||
}
|
||||
}
|
@ -7,13 +7,14 @@ from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import DOMAIN, STEP_USER, STEPS
|
||||
from .const import DOMAIN, STEP_USER, STEPS, DEFAULT_AREAS, STEP_INTEGRATION
|
||||
|
||||
|
||||
async def async_setup(hass, data, store):
|
||||
"""Set up the onboarding view."""
|
||||
hass.http.register_view(OnboardingView(data, store))
|
||||
hass.http.register_view(UserOnboardingView(data, store))
|
||||
hass.http.register_view(IntegrationOnboardingView(data, store))
|
||||
|
||||
|
||||
class OnboardingView(HomeAssistantView):
|
||||
@ -41,7 +42,6 @@ class OnboardingView(HomeAssistantView):
|
||||
class _BaseOnboardingView(HomeAssistantView):
|
||||
"""Base class for onboarding."""
|
||||
|
||||
requires_auth = False
|
||||
step = None
|
||||
|
||||
def __init__(self, data, store):
|
||||
@ -60,14 +60,16 @@ class _BaseOnboardingView(HomeAssistantView):
|
||||
self._data['done'].append(self.step)
|
||||
await self._store.async_save(self._data)
|
||||
|
||||
hass.data[DOMAIN] = len(self._data) == len(STEPS)
|
||||
if set(self._data['done']) == set(STEPS):
|
||||
hass.data[DOMAIN] = True
|
||||
|
||||
|
||||
class UserOnboardingView(_BaseOnboardingView):
|
||||
"""View to handle onboarding."""
|
||||
"""View to handle create user onboarding step."""
|
||||
|
||||
url = '/api/onboarding/users'
|
||||
name = 'api:onboarding:users'
|
||||
requires_auth = False
|
||||
step = STEP_USER
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
@ -75,9 +77,10 @@ class UserOnboardingView(_BaseOnboardingView):
|
||||
vol.Required('username'): str,
|
||||
vol.Required('password'): str,
|
||||
vol.Required('client_id'): str,
|
||||
vol.Required('language'): str,
|
||||
}))
|
||||
async def post(self, request, data):
|
||||
"""Return the manifest.json."""
|
||||
"""Handle user creation, area creation."""
|
||||
hass = request.app['hass']
|
||||
|
||||
async with self._lock:
|
||||
@ -100,14 +103,58 @@ class UserOnboardingView(_BaseOnboardingView):
|
||||
data['name'], user_id=user.id
|
||||
)
|
||||
|
||||
# Create default areas using the users supplied language.
|
||||
translations = \
|
||||
await hass.helpers.translation.async_get_translations(
|
||||
data['language'])
|
||||
|
||||
area_registry = \
|
||||
await hass.helpers.area_registry.async_get_registry()
|
||||
|
||||
for area in DEFAULT_AREAS:
|
||||
area_registry.async_create(
|
||||
translations['component.onboarding.area.{}'.format(area)]
|
||||
)
|
||||
|
||||
await self._async_mark_done(hass)
|
||||
|
||||
# Return an authorization code to allow fetching tokens.
|
||||
# Return authorization code for fetching tokens and connect
|
||||
# during onboarding.
|
||||
auth_code = hass.components.auth.create_auth_code(
|
||||
data['client_id'], user
|
||||
)
|
||||
return self.json({
|
||||
'auth_code': auth_code
|
||||
'auth_code': auth_code,
|
||||
})
|
||||
|
||||
|
||||
class IntegrationOnboardingView(_BaseOnboardingView):
|
||||
"""View to finish integration onboarding step."""
|
||||
|
||||
url = '/api/onboarding/integration'
|
||||
name = 'api:onboarding:integration'
|
||||
step = STEP_INTEGRATION
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('client_id'): str,
|
||||
}))
|
||||
async def post(self, request, data):
|
||||
"""Handle user creation, area creation."""
|
||||
hass = request.app['hass']
|
||||
user = request['hass_user']
|
||||
|
||||
async with self._lock:
|
||||
if self._async_is_done():
|
||||
return self.json_message('Integration step already done', 403)
|
||||
|
||||
await self._async_mark_done(hass)
|
||||
|
||||
# Return authorization code so we can redirect user and log them in
|
||||
auth_code = hass.components.auth.create_auth_code(
|
||||
data['client_id'], user
|
||||
)
|
||||
return self.json({
|
||||
'auth_code': auth_code,
|
||||
})
|
||||
|
||||
|
||||
|
@ -21,7 +21,7 @@ async def test_fetch_auth_providers(hass, aiohttp_client):
|
||||
async def test_fetch_auth_providers_onboarding(hass, aiohttp_client):
|
||||
"""Test fetching auth providers."""
|
||||
client = await async_setup_auth(hass, aiohttp_client)
|
||||
with patch('homeassistant.components.onboarding.async_is_onboarded',
|
||||
with patch('homeassistant.components.onboarding.async_is_user_onboarded',
|
||||
return_value=False):
|
||||
resp = await client.get('/auth/providers')
|
||||
assert resp.status == 400
|
||||
|
@ -51,10 +51,28 @@ async def test_is_onboarded():
|
||||
hass.data[onboarding.DOMAIN] = True
|
||||
assert onboarding.async_is_onboarded(hass)
|
||||
|
||||
hass.data[onboarding.DOMAIN] = False
|
||||
hass.data[onboarding.DOMAIN] = {
|
||||
'done': []
|
||||
}
|
||||
assert not onboarding.async_is_onboarded(hass)
|
||||
|
||||
|
||||
async def test_is_user_onboarded():
|
||||
"""Test the is onboarded function."""
|
||||
hass = Mock()
|
||||
hass.data = {}
|
||||
|
||||
assert onboarding.async_is_user_onboarded(hass)
|
||||
|
||||
hass.data[onboarding.DOMAIN] = True
|
||||
assert onboarding.async_is_user_onboarded(hass)
|
||||
|
||||
hass.data[onboarding.DOMAIN] = {
|
||||
'done': []
|
||||
}
|
||||
assert not onboarding.async_is_user_onboarded(hass)
|
||||
|
||||
|
||||
async def test_having_owner_finishes_user_step(hass, hass_storage):
|
||||
"""If owner user already exists, mark user step as complete."""
|
||||
MockUser(is_owner=True).add_to_hass(hass)
|
||||
@ -70,3 +88,15 @@ async def test_having_owner_finishes_user_step(hass, hass_storage):
|
||||
|
||||
done = hass_storage[onboarding.STORAGE_KEY]['data']['done']
|
||||
assert onboarding.STEP_USER in done
|
||||
|
||||
|
||||
async def test_migration(hass, hass_storage):
|
||||
"""Test migrating onboarding to new version."""
|
||||
hass_storage[onboarding.STORAGE_KEY] = {
|
||||
'version': 1,
|
||||
'data': {
|
||||
'done': ["user"]
|
||||
}
|
||||
}
|
||||
assert await async_setup_component(hass, 'onboarding', {})
|
||||
assert onboarding.async_is_onboarded(hass)
|
||||
|
@ -6,7 +6,7 @@ import pytest
|
||||
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.components import onboarding
|
||||
from homeassistant.components.onboarding import views
|
||||
from homeassistant.components.onboarding import const, views
|
||||
|
||||
from tests.common import CLIENT_ID, register_auth_provider
|
||||
|
||||
@ -63,6 +63,7 @@ async def test_onboarding_user_already_done(hass, hass_storage,
|
||||
'name': 'Test Name',
|
||||
'username': 'test-user',
|
||||
'password': 'test-pass',
|
||||
'language': 'en',
|
||||
})
|
||||
|
||||
assert resp.status == 403
|
||||
@ -71,10 +72,6 @@ async def test_onboarding_user_already_done(hass, hass_storage,
|
||||
async def test_onboarding_user(hass, hass_storage, aiohttp_client):
|
||||
"""Test creating a new user."""
|
||||
assert await async_setup_component(hass, 'person', {})
|
||||
mock_storage(hass_storage, {
|
||||
'done': ['hello']
|
||||
})
|
||||
|
||||
assert await async_setup_component(hass, 'onboarding', {})
|
||||
|
||||
client = await aiohttp_client(hass.http.app)
|
||||
@ -84,9 +81,12 @@ async def test_onboarding_user(hass, hass_storage, aiohttp_client):
|
||||
'name': 'Test Name',
|
||||
'username': 'test-user',
|
||||
'password': 'test-pass',
|
||||
'language': 'en',
|
||||
})
|
||||
|
||||
assert resp.status == 200
|
||||
assert const.STEP_USER in hass_storage[const.DOMAIN]['data']['done']
|
||||
|
||||
data = await resp.json()
|
||||
assert 'auth_code' in data
|
||||
|
||||
@ -98,7 +98,7 @@ async def test_onboarding_user(hass, hass_storage, aiohttp_client):
|
||||
assert user.credentials[0].data['username'] == 'test-user'
|
||||
assert len(hass.data['person'].storage_data) == 1
|
||||
|
||||
# Request refresh tokens
|
||||
# Validate refresh token 1
|
||||
resp = await client.post('/auth/token', data={
|
||||
'client_id': CLIENT_ID,
|
||||
'grant_type': 'authorization_code',
|
||||
@ -113,12 +113,20 @@ async def test_onboarding_user(hass, hass_storage, aiohttp_client):
|
||||
is not None
|
||||
)
|
||||
|
||||
# Validate created areas
|
||||
area_registry = await hass.helpers.area_registry.async_get_registry()
|
||||
assert len(area_registry.areas) == 3
|
||||
assert sorted([area.name for area
|
||||
in area_registry.async_list_areas()]) == [
|
||||
'Bedroom', 'Kitchen', 'Living Room'
|
||||
]
|
||||
|
||||
|
||||
async def test_onboarding_user_invalid_name(hass, hass_storage,
|
||||
aiohttp_client):
|
||||
"""Test not providing name."""
|
||||
mock_storage(hass_storage, {
|
||||
'done': ['hello']
|
||||
'done': []
|
||||
})
|
||||
|
||||
assert await async_setup_component(hass, 'onboarding', {})
|
||||
@ -129,6 +137,7 @@ async def test_onboarding_user_invalid_name(hass, hass_storage,
|
||||
'client_id': CLIENT_ID,
|
||||
'username': 'test-user',
|
||||
'password': 'test-pass',
|
||||
'language': 'en',
|
||||
})
|
||||
|
||||
assert resp.status == 400
|
||||
@ -149,14 +158,69 @@ async def test_onboarding_user_race(hass, hass_storage, aiohttp_client):
|
||||
'name': 'Test 1',
|
||||
'username': '1-user',
|
||||
'password': '1-pass',
|
||||
'language': 'en',
|
||||
})
|
||||
resp2 = client.post('/api/onboarding/users', json={
|
||||
'client_id': CLIENT_ID,
|
||||
'name': 'Test 2',
|
||||
'username': '2-user',
|
||||
'password': '2-pass',
|
||||
'language': 'es',
|
||||
})
|
||||
|
||||
res1, res2 = await asyncio.gather(resp1, resp2)
|
||||
|
||||
assert sorted([res1.status, res2.status]) == [200, 403]
|
||||
|
||||
|
||||
async def test_onboarding_integration(hass, hass_storage, hass_client):
|
||||
"""Test finishing integration step."""
|
||||
mock_storage(hass_storage, {
|
||||
'done': [const.STEP_USER]
|
||||
})
|
||||
|
||||
assert await async_setup_component(hass, 'onboarding', {})
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
resp = await client.post('/api/onboarding/integration', json={
|
||||
'client_id': CLIENT_ID,
|
||||
})
|
||||
|
||||
assert resp.status == 200
|
||||
data = await resp.json()
|
||||
assert 'auth_code' in data
|
||||
|
||||
# Validate refresh token
|
||||
resp = await client.post('/auth/token', data={
|
||||
'client_id': CLIENT_ID,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': data['auth_code']
|
||||
})
|
||||
|
||||
assert resp.status == 200
|
||||
assert const.STEP_INTEGRATION in hass_storage[const.DOMAIN]['data']['done']
|
||||
tokens = await resp.json()
|
||||
|
||||
assert (
|
||||
await hass.auth.async_validate_access_token(tokens['access_token'])
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
async def test_onboarding_integration_requires_auth(hass, hass_storage,
|
||||
aiohttp_client):
|
||||
"""Test finishing integration step."""
|
||||
mock_storage(hass_storage, {
|
||||
'done': [const.STEP_USER]
|
||||
})
|
||||
|
||||
assert await async_setup_component(hass, 'onboarding', {})
|
||||
|
||||
client = await aiohttp_client(hass.http.app)
|
||||
|
||||
resp = await client.post('/api/onboarding/integration', json={
|
||||
'client_id': CLIENT_ID,
|
||||
})
|
||||
|
||||
assert resp.status == 401
|
||||
|
Loading…
x
Reference in New Issue
Block a user