Created functions to epire etags from the device entity and the device's settings entity, at device and account level. Fixed the query used to fetch the devices from a user

pull/78/head
Matheus Lima 2019-03-19 12:42:16 -03:00
parent b422de76c2
commit c0ee6224d9
12 changed files with 111 additions and 45 deletions

View File

@ -5,6 +5,7 @@ from schematics import Model
from schematics.types import StringType
from selene.api import PublicEndpoint
from selene.api import device_etag_key
from selene.data.device import DeviceRepository
from selene.util.db import get_db_connection
@ -23,8 +24,7 @@ class DeviceEndpoint(PublicEndpoint):
def get(self, device_id):
self._authenticate(device_id)
etag_key = 'device.etag:{uuid}'.format(uuid=device_id)
self._validate_etag(etag_key)
self._validate_etag(device_etag_key(device_id))
with get_db_connection(self.config['DB_CONNECTION_POOL']) as db:
device = DeviceRepository(db).get_device_by_id(device_id)
if device:
@ -38,7 +38,7 @@ class DeviceEndpoint(PublicEndpoint):
del device['account_id']
response = device, HTTPStatus.OK
self._add_etag(etag_key)
self._add_etag(device_etag_key(device_id))
else:
response = '', HTTPStatus.NO_CONTENT
return response

View File

@ -1,6 +1,6 @@
from http import HTTPStatus
from selene.api import PublicEndpoint
from selene.api import PublicEndpoint, device_setting_etag_key
from selene.data.device import SettingRepository
from selene.util.db import get_db_connection
@ -12,13 +12,12 @@ class DeviceSettingEndpoint(PublicEndpoint):
def get(self, device_id):
self._authenticate(device_id)
etag_key = 'device.setting.etag:{uuid}'.format(uuid=device_id)
self._validate_etag(etag_key)
self._validate_etag(device_setting_etag_key(device_id))
with get_db_connection(self.config['DB_CONNECTION_POOL']) as db:
setting = SettingRepository(db).get_device_settings(device_id)
if setting is not None:
response = (setting, HTTPStatus.OK)
self._add_etag(etag_key)
self._add_etag(device_setting_etag_key(device_id))
else:
response = ('', HTTPStatus.NO_CONTENT)
return response

View File

@ -5,6 +5,7 @@ from behave import fixture, use_fixture
from public_api.api import public
from selene.api import generate_device_login
from selene.api.etag import ETagManager
from selene.data.account import (
Account,
AccountRepository,
@ -55,6 +56,8 @@ def before_feature(context, _):
def before_scenario(context, _):
cache = context.client_config['SELENE_CACHE']
context.etag_manager = ETagManager(cache, context.client_config)
with get_db_connection(context.client_config['DB_CONNECTION_POOL']) as db:
_add_agreements(context, db)
_add_account(context, db)

View File

@ -24,7 +24,7 @@ Feature: Get device's information
Then 304 status code should be returned by the device endpoint
Scenario: Get a device using an expired etag
Given an etag expired by selene ui
Given a device's etag expired by the web ui
When try to fetch a device using an expired etag
Then should return status 200
And a new etag

View File

@ -15,6 +15,11 @@ Feature: Retrieve device's settings
Then 304 status code should be returned by the device's settings endpoint
Scenario: Try to get a device's settings using a expired etag
Given a device's setting with a valid etag
Given a device's setting etag expired by the web ui at device level
When try to fetch the device's settings using an expired etag
Then 200 status code should be returned by the device's setting endpoint and a new etag
Scenario: Try to get a device's settings using a expired etag
Given a device's setting etag expired by the web ui at account level
When try to fetch the device's settings using an expired etag
Then 200 status code should be returned by the device's setting endpoint and a new etag

View File

@ -5,7 +5,7 @@ from http import HTTPStatus
from behave import when, then, given
from hamcrest import assert_that, equal_to, has_key, not_none, is_not
from selene.util.cache import SeleneCache
from selene.api.etag import ETagManager, device_etag_key
new_fields = dict(
platform='mycroft_mark_1',
@ -93,14 +93,9 @@ def validate_update(context):
@given('a device with a valid etag')
def get_device_etag(context):
access_token = context.device_login['accessToken']
headers = dict(Authorization='Bearer {token}'.format(token=access_token))
etag_manager: ETagManager = context.etag_manager
device_id = context.device_login['uuid']
context.get_device_response = context.client.get(
'/v1/device/{uuid}'.format(uuid=device_id),
headers=headers
)
context.device_etag = context.get_device_response.headers.get('ETag')
context.device_etag = etag_manager.get(device_etag_key(device_id))
@when('try to fetch a device using a valid etag')
@ -125,13 +120,12 @@ def validate_etag(context):
assert_that(response.status_code, equal_to(HTTPStatus.NOT_MODIFIED))
@given('an etag expired by selene ui')
@given('a device\'s etag expired by the web ui')
def expire_etag(context):
context.device_etag = '123'
new_etag = '456'
etag_manager: ETagManager = context.etag_manager
device_id = context.device_login['uuid']
cache: SeleneCache = context.client_config['SELENE_CACHE']
cache.set('device.etag:{uuid}'.format(uuid=device_id), new_etag)
context.device_etag = etag_manager.get(device_etag_key(device_id))
etag_manager.expire_device_etag_by_device_id(device_id)
@when('try to fetch a device using an expired etag')

View File

@ -5,6 +5,8 @@ from http import HTTPStatus
from behave import when, then, given
from hamcrest import assert_that, equal_to, has_key, is_not
from selene.api.etag import ETagManager, device_setting_etag_key
@when('try to fetch device\'s setting')
def get_device_settings(context):
@ -49,14 +51,9 @@ def validate_response(context):
@given('a device\'s setting with a valid etag')
def get_device_setting_etag(context):
access_token = context.device_login['accessToken']
headers = dict(Authorization='Bearer {token}'.format(token=access_token))
device_id = context.device_login['uuid']
context.get_device_response = context.client.get(
'/v1/device/{uuid}/setting'.format(uuid=device_id),
headers=headers
)
context.device_etag = context.get_device_response.headers.get('ETag')
etag_manager: ETagManager = context.etag_manager
context.device_etag = etag_manager.get(device_setting_etag_key(device_id))
@when('try to fetch the device\'s settings using a valid etag')
@ -80,10 +77,26 @@ def validate_etag_response(context):
assert_that(response.status_code, equal_to(HTTPStatus.NOT_MODIFIED))
@given('a device\'s setting etag expired by the web ui at device level')
def expire_etag_device_level(context):
device_id = context.device_login['uuid']
etag_manager: ETagManager = context.etag_manager
context.device_etag = etag_manager.get(device_setting_etag_key(device_id))
etag_manager.expire_device_setting_etag_by_device_id(device_id)
@given('a device\'s setting etag expired by the web ui at account level')
def expire_etag_account_level(context):
account_id = context.account.id
device_id = context.device_login['uuid']
etag_manager: ETagManager = context.etag_manager
context.device_etag = etag_manager.get(device_setting_etag_key(device_id))
etag_manager.expire_device_setting_etag_by_account_id(account_id)
@when('try to fetch the device\'s settings using an expired etag')
def get_device_settings_using_etag(context):
etag = '123'
context.device_etag = etag
etag = context.device_etag
access_token = context.device_login['accessToken']
device_id = context.device_login['uuid']
headers = {

View File

@ -1,6 +1,7 @@
from .base_config import get_base_config
from .base_endpoint import APIError, SeleneEndpoint
from .blueprint import selene_api
from .etag import device_etag_key, device_setting_etag_key
from .public_endpoint import PublicEndpoint
from .public_endpoint import generate_device_login
from .response import SeleneResponse, snake_to_camel

58
shared/selene/api/etag.py Normal file
View File

@ -0,0 +1,58 @@
import random
import string
from selene.data.device import DeviceRepository
from selene.util.cache import SeleneCache
from selene.util.db import get_db_connection
def device_etag_key(device_id: str):
return 'device.etag:{uuid}'.format(uuid=device_id)
def device_setting_etag_key(device_id: str):
return 'device.setting.etag:{uuid}'.format(uuid=device_id)
class ETagManager(object):
"""Class responsible for generate and expire etags"""
etag_chars = string.ascii_letters + string.digits
def __init__(self, cache: SeleneCache, config: dict):
self.cache: SeleneCache = cache
self.db_connection_pool = config['DB_CONNECTION_POOL']
def get(self, key: str) -> str:
"""Generate a etag with 32 random chars and store it into a given key
:param key: key where the etag will be stored
:return etag"""
etag = self.cache.get(key)
if etag is None:
etag = ''.join(random.choice(self.etag_chars) for _ in range(32))
self.cache.set(key, etag)
return etag
def _expire(self, key):
"""Expires an existent etag
:param key: key where the etag is stored"""
etag = ''.join(random.choice(self.etag_chars) for _ in range(32))
self.cache.set(key, etag)
def expire_device_etag_by_device_id(self, device_id: str):
"""Expire the etag associated with a device entity
:param device_id: device uuid"""
self._expire(device_etag_key(device_id))
def expire_device_setting_etag_by_device_id(self, device_id: str):
"""Expire the etag associated with a device's settings entity
:param device_id: device uuid"""
self._expire(device_setting_etag_key(device_id))
def expire_device_setting_etag_by_account_id(self, account_id):
"""Expire the settings' etags for all devices from a given account. Used when the settings are updated
at account level"""
with get_db_connection(self.db_connection_pool) as db:
devices = DeviceRepository(db).get_devices_by_account_id(account_id)
for device in devices:
self.expire_device_setting_etag_by_device_id(device.id)

View File

@ -1,12 +1,11 @@
import hashlib
import json
import random
import string
import uuid
from flask import current_app, request, Response, after_this_request
from flask.views import MethodView
from selene.api.etag import ETagManager
from selene.util.auth import AuthenticationError
from selene.util.not_modified import NotModifiedError
from ..util.cache import SeleneCache
@ -61,12 +60,11 @@ def generate_device_login(device_id: str, cache: SeleneCache) -> dict:
class PublicEndpoint(MethodView):
"""Abstract class for all endpoints used by Mycroft devices"""
etag_chars = string.ascii_letters + string.digits
def __init__(self):
self.config: dict = current_app.config
self.request = request
self.cache: SeleneCache = self.config['SELENE_CACHE']
self.etag_manager: ETagManager = ETagManager(self.cache, self.config)
def _authenticate(self, device_id: str = None):
headers = self.request.headers
@ -90,10 +88,7 @@ class PublicEndpoint(MethodView):
def _add_etag(self, key):
"""Add a etag header to the response. We try to get the etag from the cache using the given key.
If the cache has the etag, we use it, otherwise we generate a etag, store it and add it to the response"""
etag = self.cache.get(key)
if etag is None:
etag = ''.join(random.choice(self.etag_chars) for _ in range(32))
self.cache.set(key, etag)
etag = self.etag_manager.get(key)
@after_this_request
def set_etag_header(response: Response):

View File

@ -4,6 +4,5 @@ from dataclasses import dataclass
@dataclass
class Geography(object):
country: str
postal_code: str
time_zone: str
id: str = None

View File

@ -18,15 +18,14 @@ SELECT
'id', tts.id
) AS text_to_speech,
json_build_object(
'id', l.id,
'country', l.country,
'postal_code', l.postal_code,
'time_zone', l.time_zone
'id', g.id,
'country', g.country,
'time_zone', g.time_zone
) AS geography
FROM
device.device d
INNER JOIN device.wake_word ww ON d.wake_word_id = ww.id
INNER JOIN device.text_to_speech tts ON d.text_to_speech_id = tts.id
LEFT JOIN device.location l ON d.location_id = l.id
LEFT JOIN device.geography g ON d.geography_id = g.id
WHERE
d.account_id = %(account_id)s