- Refactoring to use a AccountRepository to get an account using the device id

- Created tests for the metrics service
pull/48/head
Matheus Lima 2019-02-25 19:03:08 -03:00
parent a6533fbe32
commit eccb83ed44
14 changed files with 192 additions and 121 deletions

View File

@ -1,30 +0,0 @@
from flask import Flask
from flask_restful import Api
from selene.api import JSON_MIMETYPE, output_json
from public_api.endpoints.device_metrics import DeviceMetricsEndpoint
from .endpoints.device import DeviceEndpoint
from .endpoints.device_setting import DeviceSettingEndpoint
from .endpoints.device_skill import DeviceSkillEndpoint
from .endpoints.device_skills import DeviceSkillsEndpoint
from selene.api.base_config import get_base_config
from .endpoints.device_subscription import DeviceSubscriptionEndpoint
from .endpoints.open_weather_map import OpenWeatherMapEndpoint
from .endpoints.wolfram_alpha import WolframAlphaEndpoint
public = Flask(__name__)
public.config.from_object(get_base_config())
public_api = Api(public)
public_api.representations[JSON_MIMETYPE] = output_json
public_api.representations.update()
public_api.add_resource(DeviceSkillsEndpoint, '/device/<string:device_id>/skill')
public_api.add_resource(DeviceSkillEndpoint, '/device/<string:device_id>/userSkill')
public_api.add_resource(DeviceEndpoint, '/device/<string:device_id>')
public_api.add_resource(DeviceSettingEndpoint, '/device/<string:device_id>/setting')
public_api.add_resource(DeviceSubscriptionEndpoint, '/device/<string:device_id>/subscription')
public_api.add_resource(WolframAlphaEndpoint, '/wa') # TODO: change this path in the API v2
public_api.add_resource(OpenWeatherMapEndpoint, '/owm/<path:path>') # TODO: change this path in the API v2
public_api.add_resource(DeviceMetricsEndpoint, '/device/<string:device_id>/metric/<path:metric>')

View File

@ -3,23 +3,22 @@ import smtplib
from flask import Flask
from selene.api import SeleneResponse, selene_api
from selene.api.base_config import get_base_config
from selene.util.cache import SeleneCache
from selene.api import SeleneResponse, selene_api
from .endpoints.account_device import AccountDeviceEndpoint
from .endpoints.device import DeviceEndpoint
from .endpoints.device_activate import DeviceActivateEndpoint
from .endpoints.device_code import DeviceCodeEndpoint
from .endpoints.device_email import DeviceEmailEndpoint
from .endpoints.device_metrics import DeviceMetricsEndpoint, MetricsService
from .endpoints.device_setting import DeviceSettingEndpoint
from .endpoints.device_skill import DeviceSkillEndpoint
from .endpoints.device_skills import DeviceSkillsEndpoint
from .endpoints.device_subscription import DeviceSubscriptionEndpoint
from .endpoints.google_stt import GoogleSTTEndpoint
from .endpoints.open_weather_map import OpenWeatherMapEndpoint
from .endpoints.wolfram_alpha import WolframAlphaEndpoint
from .endpoints.google_stt import GoogleSTTEndpoint
from .endpoints.device_code import DeviceCodeEndpoint
from .endpoints.device_activate import DeviceActivateEndpoint
from .endpoints.account_device import AccountDeviceEndpoint
from .endpoints.device_email import DeviceEmailEndpoint
from .endpoints.device_metrics import DeviceMetricsEndpoint
public = Flask(__name__)
public.config.from_object(get_base_config())
@ -35,6 +34,8 @@ email_client = smtplib.SMTP(host, port)
email_client.login(user, password)
public.config['EMAIL_CLIENT'] = email_client
public.config['METRICS_SERVICE'] = MetricsService()
public.response_class = SeleneResponse
public.register_blueprint(selene_api)

View File

@ -7,7 +7,7 @@ from schematics import Model
from schematics.types import StringType
from selene.api import SeleneEndpoint
from selene.data.device import DeviceRepository
from selene.data.account import AccountRepository
from selene.util.db import get_db_connection
@ -30,14 +30,14 @@ class DeviceEmailEndpoint(SeleneEndpoint):
send_email.validate()
with get_db_connection(self.config['DB_CONNECTION_POOL']) as db:
email_address = DeviceRepository(db).get_account_email_by_device_id(device_id)
account = AccountRepository(db).get_account_by_device_id(device_id)
if email_address:
if account:
message = EmailMessage()
message['Subject'] = str(send_email.title)
message['From'] = str(send_email.sender)
message.set_content(str(send_email.body))
message['To'] = email_address
message['To'] = account.email_address
self.email_client.send_message(message)
self.email_client.quit()
response = '', HTTPStatus.OK

View File

@ -1,31 +1,41 @@
import json
import os
from http import HTTPStatus
import requests
from selene.api import SeleneEndpoint
from selene.data.device import DeviceRepository
from selene.data.account import AccountRepository
from selene.util.db import get_db_connection
class MetricsService(object):
def __init__(self):
self.metrics_service_host = os.environ['METRICS_SERVICE_HOST']
def send_metric(self, metric: str, user_id: str, device_id: str, data: dict):
body = dict(
userUuid=user_id,
deviceUuid=device_id,
data=data
)
url = '{host}/{metric}'.format(host=self.metrics_service_host, metric=metric)
requests.post(url, body)
class DeviceMetricsEndpoint(SeleneEndpoint):
"""Endpoint to communicate with the metrics service"""
def __init__(self):
super(DeviceMetricsEndpoint, self).__init__()
self.metrics_service_host = os.environ['METRICS_SERVICE_HOST']
self.metrics_service: MetricsService = self.config['METRICS_SERVICE']
def post(self, device_id, metric):
payload = self.request.get_json()
payload = json.loads(self.request.data)
with get_db_connection(self.config['DB_CONNECTION_POOL']) as db:
account_id = DeviceRepository(db).get_account_id_by_device_id(device_id)
if account_id:
body = dict(
userUuid=account_id,
deviceUuid=device_id,
data=payload
)
url = '{host}/{metric}'.format(host=self.metrics_service_host, metric=metric)
requests.post(url, body)
account = AccountRepository(db).get_account_by_device_id(device_id)
if account:
self.metrics_service.send_metric(metric, account.id, device_id, payload)
response = '', HTTPStatus.OK
else:
response = '', HTTPStatus.NO_CONTENT

View File

@ -1,7 +1,7 @@
from http import HTTPStatus
from selene.api import SeleneEndpoint
from selene.data.device import DeviceRepository
from selene.data.account import AccountRepository
from selene.util.db import get_db_connection
@ -11,6 +11,10 @@ class DeviceSubscriptionEndpoint(SeleneEndpoint):
def get(self, device_id):
with get_db_connection(self.config['DB_CONNECTION_POOL']) as db:
subscription = DeviceRepository(db).get_subscription_type_by_device_id(device_id)
response = (subscription, HTTPStatus.OK) if subscription is not None else ('', HTTPStatus.NO_CONTENT)
account = AccountRepository(db).get_account_by_device_id(device_id)
if account:
subscription = account.subscription
response = {'@type': subscription.type if subscription is not None else 'free'}, HTTPStatus.OK
else:
response = '', HTTPStatus.NO_CONTENT
return response

View File

@ -0,0 +1,12 @@
Feature: Send a metric to the metric service
Scenario: a metric is sent to the metric endpoint by a valid device
Given a device pairing code
When a device is added to an account using the pairing code
And device is activated
And the metric is sent
Then 200 status code should be returned
Scenario: a metric is sent by an invalid device
When the metric is sent by an invalid device
Then metrics endpoint should return 204

View File

@ -0,0 +1,56 @@
import json
import uuid
from http import HTTPStatus
from unittest import mock
from unittest.mock import patch, MagicMock, call
from behave import when, then
from hamcrest import assert_that, equal_to
payload = dict(
type='timing',
start='123'
)
@when('the metric is sent')
@patch('public_api.endpoints.device_metrics.MetricsService')
def send_metric(context, metrics_service):
context.client_config['METRICS_SERVICE'] = metrics_service
context.response = context.client.post(
'/device/{uuid}/metric/{metric}'.format(uuid=context.device_id, metric='timing'),
data=json.dumps(payload),
content_type='application_json'
)
@then('200 status code should be returned')
def validate_response(context):
response = context.response
assert_that(response.status_code, equal_to(HTTPStatus.OK))
metrics_service: MagicMock = context.client_config['METRICS_SERVICE']
metrics_service.send_metric.assert_has_calls([
call('timing',
context.account.id,
context.device_id,
mock.ANY)
])
@when('the metric is sent by an invalid device')
@patch('public_api.endpoints.device_metrics.MetricsService')
def send_metrics_invalid(context, metrics_service):
context.client_config['METRICS_SERVICE'] = metrics_service
context.response = context.client.post(
'/device/{uuid}/metric/{metric}'.format(uuid=str(uuid.uuid4()), metric='timing'),
data=json.dumps(payload),
content_type='application_json'
)
@then('metrics endpoint should return 204')
def validate_invalid_device(context):
response = context.response
assert_that(response.status_code, equal_to(HTTPStatus.NO_CONTENT))
metrics_service: MagicMock = context.client_config['METRICS_SERVICE']
metrics_service.send_metric.assert_not_called()

View File

@ -36,7 +36,7 @@ def validate_response_monthly(context):
response = context.subscription_response
assert_that(response.status_code, HTTPStatus.OK)
subscription = json.loads(response.data)
assert_that(subscription, has_entry('@type', 'month'))
assert_that(subscription, has_entry('@type', 'Monthly Supporter'))
@when('try to get the subscription for a nonexistent device')

View File

@ -1,8 +1,9 @@
from logging import getLogger
from passlib.hash import sha512_crypt
from os import environ, path
from typing import List
from passlib.hash import sha512_crypt
from selene.util.db import (
DatabaseRequest,
Cursor,
@ -164,3 +165,11 @@ class AccountRepository(object):
account = Account(**result['account'])
return account
def get_account_by_device_id(self, device_id) -> Account:
"""Return an account using the id of the device associated to the account"""
request = DatabaseRequest(
sql=get_sql_from_file(path.join(SQL_DIR, 'get_account_by_device_id.sql')),
args=dict(device_id=device_id)
)
return self._get_account(request)

View File

@ -0,0 +1,69 @@
WITH
refresh_tokens AS (
SELECT
array_agg(refresh_token)
FROM
account.refresh_token acc_ref
INNER JOIN
account.account acc ON acc_ref.account_id = acc.id
INNER JOIN
device.device dev ON acc.id = dev.account_id
WHERE
dev.id = %(device_id)s
),
agreements AS (
SELECT
array_agg(
json_build_object(
'id', aa.id,
'type', ag.agreement,
'accept_date', aa.accept_date
)
)
FROM
account.account_agreement aa
INNER JOIN
account.agreement ag ON ag.id = aa.agreement_id
INNER JOIN
account.account acc ON aa.account_id = acc.id
INNER JOIN
device.device dev ON acc.id = dev.account_id
WHERE
dev.id = %(device_id)s
),
subscription AS (
SELECT
json_build_object(
'id', asub.id,
'type', s.subscription,
'start_date', lower(asub.subscription_ts_range)::DATE,
'stripe_customer_id', asub.stripe_customer_id
)
FROM
account.account_subscription asub
INNER JOIN
account.subscription s ON asub.subscription_id = s.id
INNER JOIN
account.account acc ON asub.account_id = acc.id
INNER JOIN
device.device dev ON acc.id = dev.account_id
WHERE
dev.id = %(device_id)s
AND upper(asub.subscription_ts_range) IS NULL
)
SELECT
json_build_object(
'id', acc.id,
'email_address', acc.email_address,
'username', acc.username,
'subscription', (SELECT * FROM subscription),
'refresh_tokens', (SELECT * FROM refresh_tokens),
'agreements', (SELECT * FROM agreements)
) as account
FROM
account.account acc
INNER JOIN
device.device dev ON acc.id = dev.account_id
WHERE
dev.id = %(device_id)s

View File

@ -1,10 +1,10 @@
from os import path
from typing import List
from selene.util.db import DatabaseRequest, get_sql_from_file, Cursor
from ..entity.device import Device
from ..entity.text_to_speech import TextToSpeech
from ..entity.wake_word import WakeWord
from ..entity.device import Device
from selene.util.db import DatabaseRequest, get_sql_from_file, Cursor
SQL_DIR = path.join(path.dirname(__file__), 'sql')
@ -42,20 +42,6 @@ class DeviceRepository(object):
sql_results = self.cursor.select_all(query)
return [Device(**result) for result in sql_results]
def get_subscription_type_by_device_id(self, device_id):
"""Return the type of subscription of device's owner
:param device_id: device uuid
"""
query = DatabaseRequest(
sql=get_sql_from_file(path.join(SQL_DIR, 'get_subscription_type_by_device_id.sql')),
args=dict(device_id=device_id)
)
sql_result = self.cursor.select_one(query)
if sql_result:
rate_period = sql_result['rate_period']
# TODO: Remove the @ in the API v2
return {'@type': rate_period} if rate_period is not None else {'@type': 'free'}
def add_device(self, account_id: str, name: str, wake_word_id: str, text_to_speech_id: str):
""" Creates a new device with a given name and associate it to an account"""
# TODO: validate foreign keys
@ -130,21 +116,3 @@ class DeviceRepository(object):
args=dict(text_to_speech_id=text_to_speech_id)
)
self.cursor.delete(query)
def get_account_email_by_device_id(self, device_id):
query = DatabaseRequest(
sql=get_sql_from_file(path.join(SQL_DIR, 'get_account_email_by_device_id.sql')),
args=dict(device_id=device_id)
)
sql_result = self.cursor.select_one(query)
if sql_result:
return sql_result['email_address']
def get_account_id_by_device_id(self, device_id):
query = DatabaseRequest(
sql=get_sql_from_file(path.join(SQL_DIR, 'get_account_id_by_device_id.sql')),
args=dict(device_id=device_id)
)
sql_result = self.cursor.select_one(query)
if sql_result:
return sql_result['id']

View File

@ -1,8 +0,0 @@
SELECT
acc.email_address
FROM
account.account acc
INNER JOIN
device.device dev ON acc.id = dev.account_id
WHERE
dev.id = %(device_id)s

View File

@ -1,8 +0,0 @@
SELECT
acc.id
FROM
account.account acc
INNER JOIN
device.device dev ON acc.id = dev.account_id
WHERE
dev.id = %(device_id)s

View File

@ -1,12 +0,0 @@
SELECT
sub.rate_period
FROM
device.device dev
INNER JOIN
account.account acc ON dev.account_id = acc.id
LEFT JOIN
account.account_subscription acc_sub ON acc.id = acc_sub.account_id
LEFT JOIN
account.subscription sub ON acc_sub.subscription_id = sub.id
WHERE
dev.id = %(device_id)s