You've already forked homehub
Release 1.4.0
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__version__ = '1.3.0'
|
||||
__version__ = '1.4.0'
|
||||
|
||||
@@ -11,8 +11,8 @@ from aiojobs.aiohttp import setup as setup_aiojobs
|
||||
|
||||
from homehub_backend import handlers
|
||||
from homehub_backend.defs import FRONTEND_DIR
|
||||
from homehub_backend.lib.application import HomeHubApplication
|
||||
from homehub_backend.lib.state_store import setup as setup_state_store
|
||||
from homehub_backend.lib.rpc import setup as setup_rpc
|
||||
from homehub_backend.lib.services import setup as setup_services
|
||||
from homehub_backend.lib.websocket import setup as setup_websocket
|
||||
|
||||
@@ -49,13 +49,14 @@ async def app_on_startup(app):
|
||||
|
||||
|
||||
def create_app(loop=None):
|
||||
app = web.Application(logger=app_logger, loop=loop)
|
||||
app['SETTINGS'] = settings
|
||||
application_class = getattr(
|
||||
settings, 'APPLICATION_CLASS', HomeHubApplication,
|
||||
)
|
||||
app = application_class(settings, logger=app_logger, loop=loop)
|
||||
|
||||
app.on_startup.append(app_on_startup)
|
||||
|
||||
setup_aiojobs(app)
|
||||
setup_rpc(app)
|
||||
setup_state_store(app)
|
||||
setup_services(app)
|
||||
setup_websocket(app)
|
||||
@@ -67,13 +68,15 @@ def create_app(loop=None):
|
||||
app['INDEX_HTML'] = index_html_f.read()
|
||||
|
||||
app.add_routes([
|
||||
web.post('/backend/rpc', app.get_jsonrpc_view()),
|
||||
web.get('/', handlers.get_index),
|
||||
web.get('/index.html', handlers.get_index),
|
||||
])
|
||||
|
||||
if os.path.isdir(frontend_dir):
|
||||
static_path = getattr(settings, 'STATIC_PATH', 'frontend')
|
||||
app.add_routes([
|
||||
web.static('/frontend', frontend_dir),
|
||||
web.static(f'/{static_path}', frontend_dir),
|
||||
])
|
||||
|
||||
return app
|
||||
|
||||
41
packages/homehub_backend/homehub_backend/lib/application.py
Normal file
41
packages/homehub_backend/homehub_backend/lib/application.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
from bthlabs_jsonrpc_aiohttp import JSONRPCView
|
||||
|
||||
LOGGER = logging.getLogger('zagony_bot.lib.application')
|
||||
|
||||
|
||||
class HomeHubJSONRPCView(JSONRPCView):
|
||||
PUBLIC_METHODS = (
|
||||
'services.get_states', 'services.start', 'state.get_frontend',
|
||||
'system.list_methods',
|
||||
)
|
||||
|
||||
async def can_call(self, request, method, args, kwargs):
|
||||
result = method in self.PUBLIC_METHODS
|
||||
|
||||
if request.app['SETTINGS'].ADMIN_HOSTS == '*':
|
||||
result = True
|
||||
|
||||
if request.host in request.app['SETTINGS'].ADMIN_HOSTS:
|
||||
result = True
|
||||
|
||||
if result is False:
|
||||
LOGGER.warning(
|
||||
'HomeHubJSONRPCView.can_call(): Access denied! host=%s method=%s',
|
||||
request.host,
|
||||
method,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class HomeHubApplication(web.Application):
|
||||
def __init__(self, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self['SETTINGS'] = settings
|
||||
|
||||
def get_jsonrpc_view(self):
|
||||
return HomeHubJSONRPCView()
|
||||
@@ -1,175 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from types import MethodType
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
LOGGER = logging.getLogger('homehub.lib.rpc')
|
||||
|
||||
|
||||
class BaseJSONRPCError(Exception):
|
||||
def __init__(self, data=None):
|
||||
self.data = data
|
||||
|
||||
def to_json(self):
|
||||
result = {
|
||||
'code': self.ERROR_CODE,
|
||||
'message': self.ERROR_MESSAGE,
|
||||
}
|
||||
|
||||
if self.data:
|
||||
result['data'] = self.data
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class JSONRPCParseError(BaseJSONRPCError):
|
||||
ERROR_CODE = -32700
|
||||
ERROR_MESSAGE = 'Parse error'
|
||||
|
||||
|
||||
class JSONRPCInvalidRequestError(BaseJSONRPCError):
|
||||
ERROR_CODE = -32600
|
||||
ERROR_MESSAGE = 'Invalid Request'
|
||||
|
||||
|
||||
class JSONRPCMethodNotFoundError(BaseJSONRPCError):
|
||||
ERROR_CODE = -32601
|
||||
ERROR_MESSAGE = 'Method not found'
|
||||
|
||||
|
||||
class JSONRPCInvalidParamsError(BaseJSONRPCError):
|
||||
ERROR_CODE = -32602
|
||||
ERROR_MESSAGE = 'Invalid params'
|
||||
|
||||
|
||||
class JSONRPCInternalErrorError(BaseJSONRPCError):
|
||||
ERROR_CODE = -32603
|
||||
ERROR_MESSAGE = 'Internal error'
|
||||
|
||||
|
||||
async def post_rpc(request):
|
||||
responses = []
|
||||
results = []
|
||||
calls = []
|
||||
try:
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception as exc:
|
||||
LOGGER.error('Error deserializing RPC call!', exc_info=exc)
|
||||
raise JSONRPCParseError()
|
||||
|
||||
if isinstance(data, list):
|
||||
calls = data
|
||||
else:
|
||||
calls.append(data)
|
||||
|
||||
if len(calls) == 0:
|
||||
raise JSONRPCInvalidRequestError()
|
||||
|
||||
for call in calls:
|
||||
call_method = None
|
||||
method_handler = None
|
||||
result = None
|
||||
|
||||
try:
|
||||
try:
|
||||
assert isinstance(call, dict), JSONRPCInvalidRequestError
|
||||
assert call.get('jsonrpc') == '2.0', JSONRPCInvalidRequestError
|
||||
|
||||
call_method = call.get('method', None)
|
||||
assert call_method is not None, JSONRPCInvalidRequestError
|
||||
|
||||
method_handler = request.config_dict['RPC_METHOD_REGISTRY'].get(call_method, None)
|
||||
assert method_handler is not None, JSONRPCMethodNotFoundError
|
||||
except AssertionError as exc:
|
||||
klass = exc.args[0]
|
||||
raise klass()
|
||||
|
||||
args = []
|
||||
kwargs = {}
|
||||
|
||||
call_params = call.get('params', None)
|
||||
if call_params is not None:
|
||||
if isinstance(call_params, list):
|
||||
args = call_params
|
||||
elif isinstance(call_params, dict):
|
||||
kwargs = call_params
|
||||
else:
|
||||
raise JSONRPCInvalidParamsError()
|
||||
|
||||
args.insert(0, request)
|
||||
|
||||
try:
|
||||
result = await method_handler(*args, **kwargs)
|
||||
except TypeError as exc:
|
||||
LOGGER.error(
|
||||
f'Error handling RPC method: {call_method}!',
|
||||
exc_info=exc,
|
||||
)
|
||||
raise JSONRPCInvalidParamsError(str(exc))
|
||||
except Exception as exc:
|
||||
if isinstance(exc, BaseJSONRPCError):
|
||||
result = exc
|
||||
else:
|
||||
LOGGER.error(
|
||||
f'Error handling RPC method: {call_method}!',
|
||||
exc_info=exc,
|
||||
)
|
||||
result = JSONRPCInternalErrorError(str(exc))
|
||||
finally:
|
||||
results.append((call, result))
|
||||
except Exception as exc:
|
||||
if isinstance(exc, BaseJSONRPCError):
|
||||
results = [(None, exc)]
|
||||
else:
|
||||
raise
|
||||
|
||||
for result in results:
|
||||
call, call_result = result
|
||||
|
||||
response = {
|
||||
'jsonrpc': '2.0',
|
||||
'id': None,
|
||||
}
|
||||
|
||||
if isinstance(call, dict) and call.get('id', None) is not None:
|
||||
response['id'] = call['id']
|
||||
|
||||
if isinstance(call_result, BaseJSONRPCError):
|
||||
response['error'] = call_result.to_json()
|
||||
else:
|
||||
response.update({
|
||||
'method': call['method'],
|
||||
'result': call_result,
|
||||
})
|
||||
|
||||
if 'error' in response or response['id'] is not None:
|
||||
responses.append(response)
|
||||
|
||||
if len(responses) == 1:
|
||||
return web.json_response(responses[0])
|
||||
|
||||
return web.json_response(responses)
|
||||
|
||||
|
||||
async def list_methods(request):
|
||||
return sorted(request.config_dict['RPC_METHOD_REGISTRY'].keys())
|
||||
|
||||
|
||||
def add_rpc_methods(app, methods):
|
||||
for name, method in methods:
|
||||
app['RPC_METHOD_REGISTRY'][name] = method
|
||||
|
||||
|
||||
def setup(app):
|
||||
app['RPC_METHOD_REGISTRY'] = {}
|
||||
app.add_rpc_methods = MethodType(add_rpc_methods, app)
|
||||
|
||||
app.add_routes([
|
||||
web.post('/backend/rpc', post_rpc),
|
||||
])
|
||||
|
||||
app.add_rpc_methods([
|
||||
('system.list_methods', list_methods),
|
||||
])
|
||||
@@ -1,10 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
from bthlabs_jsonrpc_core import register_method
|
||||
|
||||
LOGGER = logging.getLogger('homehub.lib.services')
|
||||
|
||||
CapabilitySpec = namedtuple('CapabilitySpec', [
|
||||
'handler', 'public', 'public_content_type',
|
||||
])
|
||||
|
||||
|
||||
class BaseServiceError(Exception):
|
||||
pass
|
||||
@@ -42,6 +50,7 @@ class BaseService:
|
||||
self.instance = instance
|
||||
self.characteristics = characteristics
|
||||
|
||||
self.capabilities = {}
|
||||
self.state = self.app['STATE'].get()['backend'].get(
|
||||
self.state_key(), None,
|
||||
)
|
||||
@@ -104,6 +113,15 @@ class BaseService:
|
||||
|
||||
self.app['STATE'].save(new_state)
|
||||
|
||||
def register_capability(self,
|
||||
name,
|
||||
handler,
|
||||
public=False,
|
||||
public_content_type=None):
|
||||
self.capabilities[name] = CapabilitySpec(
|
||||
handler, public, public_content_type,
|
||||
)
|
||||
|
||||
|
||||
def _get_service_key(request, kind, instance):
|
||||
klass = request.config_dict['SETTINGS'].SERVICES.get(kind, None)
|
||||
@@ -121,6 +139,7 @@ def _lookup_service(request, kind, instance):
|
||||
return request.config_dict['SERVICES'].get(service_key, None)
|
||||
|
||||
|
||||
@register_method('services.start')
|
||||
async def start_service(request, kind, instance, characteristics):
|
||||
data = None
|
||||
|
||||
@@ -145,6 +164,7 @@ async def start_service(request, kind, instance, characteristics):
|
||||
return data.data
|
||||
|
||||
|
||||
@register_method('services.stop')
|
||||
async def stop_service(request, kind, instance):
|
||||
result = 'ok'
|
||||
|
||||
@@ -158,29 +178,83 @@ async def stop_service(request, kind, instance):
|
||||
return result
|
||||
|
||||
|
||||
async def use_service(request, kind, instance, capability, params):
|
||||
def get_service_capability(request, kind, instance, capability):
|
||||
result = None
|
||||
|
||||
service = _lookup_service(request, kind, instance)
|
||||
if not service:
|
||||
raise BaseServiceError(f'Unknown service: {instance} of {kind}')
|
||||
else:
|
||||
capability_handler = getattr(service, capability, None)
|
||||
if not capability_handler:
|
||||
capability_spec = service.capabilities.get(capability, None)
|
||||
if not capability_spec:
|
||||
raise BaseServiceError(f'Unknown capability: {kind}.{capability}')
|
||||
else:
|
||||
result = await capability_handler(*params)
|
||||
result = capability_spec
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@register_method('services.use')
|
||||
async def use_service(request, kind, instance, capability, params):
|
||||
capability_spec = get_service_capability(
|
||||
request, kind, instance, capability,
|
||||
)
|
||||
|
||||
result = await capability_spec.handler(*params)
|
||||
return result
|
||||
|
||||
|
||||
async def web_use_service(request):
|
||||
try:
|
||||
kind = request.query.get('kind', None)
|
||||
capability = request.query.get('capability', None)
|
||||
|
||||
capability_spec = get_service_capability(
|
||||
request,
|
||||
kind,
|
||||
request.query.get('instance', None),
|
||||
capability,
|
||||
)
|
||||
|
||||
if capability_spec.public is False:
|
||||
raise BaseServiceError(f'Unknown capability: {kind}.{capability}')
|
||||
except BaseServiceError as exception:
|
||||
LOGGER.error(
|
||||
'web_use_service(): Capability lookup error.', exc_info=exception,
|
||||
)
|
||||
|
||||
response_body = 'Bad Request'
|
||||
is_debug_or_testing = any((
|
||||
request.config_dict['SETTINGS'].DEBUG,
|
||||
request.config_dict['SETTINGS'].TESTING,
|
||||
))
|
||||
if is_debug_or_testing is True:
|
||||
response_body = exception.args[0]
|
||||
|
||||
return web.Response(
|
||||
body=response_body,
|
||||
content_type='text/plain',
|
||||
charset='utf-8',
|
||||
status=400,
|
||||
)
|
||||
|
||||
content_type = capability_spec.public_content_type
|
||||
|
||||
params = request.query.getall('params', [])
|
||||
result = await capability_spec.handler(*params)
|
||||
if isinstance(result, str):
|
||||
result = result.encode('utf-8')
|
||||
|
||||
return web.Response(
|
||||
body=result, content_type=content_type, charset='utf-8',
|
||||
)
|
||||
|
||||
|
||||
def setup(app):
|
||||
app['SERVICES'] = {}
|
||||
|
||||
app.add_rpc_methods([
|
||||
('services.start', start_service),
|
||||
('services.stop', stop_service),
|
||||
('services.use', use_service),
|
||||
app.add_routes([
|
||||
web.get('/services/use', web_use_service),
|
||||
])
|
||||
|
||||
async def on_cleanup(app):
|
||||
|
||||
@@ -6,6 +6,8 @@ import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from bthlabs_jsonrpc_core import register_method
|
||||
|
||||
LOGGER = logging.getLogger('homehub.lib.state_store')
|
||||
|
||||
|
||||
@@ -42,10 +44,12 @@ class StateStore:
|
||||
self.threadpool.submit(self.do_save, state)
|
||||
|
||||
|
||||
@register_method('state.get_frontend')
|
||||
async def get_frontend_state(request):
|
||||
return request.config_dict['STATE'].get()['frontend']
|
||||
|
||||
|
||||
@register_method('state.save_frontend')
|
||||
async def save_frontend_state(request, new_state):
|
||||
state = copy.deepcopy(request.config_dict['STATE'].get())
|
||||
state['frontend'] = new_state
|
||||
@@ -66,9 +70,4 @@ def setup(app):
|
||||
app.on_startup.append(store.on_app_startup)
|
||||
app.on_cleanup.append(store.on_app_cleanup)
|
||||
|
||||
app.add_rpc_methods([
|
||||
('state.get_frontend', get_frontend_state),
|
||||
('state.save_frontend', save_frontend_state),
|
||||
])
|
||||
|
||||
app['STATE'] = store
|
||||
|
||||
@@ -6,5 +6,6 @@ from homehub_backend.services import SERVICES # noqa: F401
|
||||
DEBUG = False
|
||||
TESTING = False
|
||||
STATE_PATH = os.path.join(os.getcwd(), 'state.json')
|
||||
ADMIN_HOSTS = '*'
|
||||
|
||||
WEATHER_SERVICE_API_KEY = None
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import importlib
|
||||
|
||||
from homehub_backend.app import create_app
|
||||
|
||||
|
||||
@@ -13,9 +15,6 @@ async def fial(request):
|
||||
def create_testing_app(loop=None):
|
||||
app = create_app(loop=None)
|
||||
|
||||
app.add_rpc_methods([
|
||||
('testing.greet', greet),
|
||||
('testing.fial', fial),
|
||||
])
|
||||
importlib.reload(app['SETTINGS'])
|
||||
|
||||
return app
|
||||
|
||||
@@ -6,6 +6,7 @@ from homehub_backend.testing import services
|
||||
DEBUG = False
|
||||
TESTING = True
|
||||
STATE_PATH = os.path.join(os.path.dirname(__file__), 'state.json')
|
||||
ADMIN_HOSTS = '*'
|
||||
|
||||
WEATHER_SERVICE_API_KEY = 'thisisntright'
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from homehub_backend.app import settings
|
||||
from homehub_backend.lib import application
|
||||
|
||||
|
||||
class Test_HomeHubApplication:
|
||||
def test_init(self):
|
||||
# When
|
||||
result = application.HomeHubApplication(settings)
|
||||
|
||||
# Then
|
||||
assert result['SETTINGS'] == settings
|
||||
|
||||
def test_get_jsonrpc_view(self):
|
||||
# Given
|
||||
homehub_application = application.HomeHubApplication(settings)
|
||||
|
||||
# When
|
||||
result = homehub_application.get_jsonrpc_view()
|
||||
|
||||
# Then
|
||||
assert isinstance(result, application.HomeHubJSONRPCView)
|
||||
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from unittest import mock
|
||||
|
||||
from aiohttp.web import Request
|
||||
import pytest
|
||||
|
||||
from homehub_backend.lib import application
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_request(homehub_app):
|
||||
result = mock.Mock(spec=Request)
|
||||
result.app = homehub_app
|
||||
result.host = 'homehub.test'
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class Test_HomeHubJSONRPCView:
|
||||
async def test_can_call_public_method(self, fake_request):
|
||||
# Given
|
||||
jsonrpc_view = application.HomeHubJSONRPCView()
|
||||
|
||||
# When
|
||||
result = await jsonrpc_view.can_call(
|
||||
fake_request, 'system.list_methods', [], {},
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result is True
|
||||
|
||||
async def test_can_call_admin_hosts_wildcard(self, fake_request):
|
||||
# Given
|
||||
fake_request.app['SETTINGS'].ADMIN_HOSTS = '*'
|
||||
|
||||
jsonrpc_view = application.HomeHubJSONRPCView()
|
||||
|
||||
# When
|
||||
result = await jsonrpc_view.can_call(
|
||||
fake_request, 'services.stop', [], {},
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result is True
|
||||
|
||||
async def test_can_call_host_in_admin_hosts(self, fake_request):
|
||||
# Given
|
||||
fake_request.app['SETTINGS'].ADMIN_HOSTS = ['homehub.test']
|
||||
|
||||
jsonrpc_view = application.HomeHubJSONRPCView()
|
||||
|
||||
# When
|
||||
result = await jsonrpc_view.can_call(
|
||||
fake_request, 'services.stop', [], {},
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result is True
|
||||
|
||||
async def test_can_call_False(self, fake_request):
|
||||
# Given
|
||||
fake_request.app['SETTINGS'].ADMIN_HOSTS = ['admin.homehub.test']
|
||||
|
||||
jsonrpc_view = application.HomeHubJSONRPCView()
|
||||
|
||||
# When
|
||||
result = await jsonrpc_view.can_call(
|
||||
fake_request, 'services.stop', [], {},
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result is False
|
||||
@@ -1,24 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from homehub_backend.lib import rpc
|
||||
|
||||
|
||||
class Test_BaseJSONRPCError:
|
||||
def test_to_json(self):
|
||||
exception = rpc.BaseJSONRPCError(data='spam')
|
||||
exception.ERROR_CODE = -32604
|
||||
exception.ERROR_MESSAGE = 'Test error'
|
||||
|
||||
result = exception.to_json()
|
||||
assert result['code'] == exception.ERROR_CODE
|
||||
assert result['message'] == exception.ERROR_MESSAGE
|
||||
assert result['data'] == exception.data
|
||||
|
||||
def test_to_json_without_data(self):
|
||||
exception = rpc.BaseJSONRPCError()
|
||||
exception.ERROR_CODE = -32604
|
||||
exception.ERROR_MESSAGE = 'Test error'
|
||||
|
||||
result = exception.to_json()
|
||||
assert result['code'] == exception.ERROR_CODE
|
||||
assert result['message'] == exception.ERROR_MESSAGE
|
||||
assert 'data' not in result
|
||||
@@ -1,182 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from homehub_backend.lib import rpc
|
||||
|
||||
|
||||
class Test_PostRPC:
|
||||
def _valid_call(self, **kwargs):
|
||||
result = {
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'testing.greet',
|
||||
}
|
||||
result.update(kwargs)
|
||||
|
||||
return result
|
||||
|
||||
async def test_parse_error(self, homehub_client):
|
||||
response = await homehub_client.post('/backend/rpc', data=b'spam')
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCParseError.ERROR_CODE
|
||||
|
||||
async def test_empty_call(self, homehub_client):
|
||||
response = await homehub_client.post('/backend/rpc', json={})
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCInvalidRequestError.ERROR_CODE
|
||||
|
||||
async def test_empty_batch(self, homehub_client):
|
||||
response = await homehub_client.post('/backend/rpc', json=[])
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCInvalidRequestError.ERROR_CODE
|
||||
|
||||
async def test_call_not_dict(self, homehub_client):
|
||||
response = await homehub_client.post('/backend/rpc', json='"spam"')
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCInvalidRequestError.ERROR_CODE
|
||||
|
||||
async def test_call_without_jsonrpc(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
call.pop('jsonrpc')
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCInvalidRequestError.ERROR_CODE
|
||||
|
||||
async def test_call_without_method(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
call.pop('method')
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCInvalidRequestError.ERROR_CODE
|
||||
|
||||
async def test_method_not_found(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
call['method'] = 'testing.idontexist'
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCMethodNotFoundError.ERROR_CODE
|
||||
|
||||
async def test_invalid_params(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
call['params'] = 'spam'
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCInvalidParamsError.ERROR_CODE
|
||||
assert 'data' not in data['error']
|
||||
|
||||
async def test_call_with_method_params_error(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCInvalidParamsError.ERROR_CODE
|
||||
assert data['error']['data'] == (
|
||||
"greet() missing 1 required positional argument: 'who'"
|
||||
)
|
||||
|
||||
async def test_method_exception(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
call['method'] = 'testing.fial'
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCInternalErrorError.ERROR_CODE
|
||||
assert data['error']['data'] == 'FIAL'
|
||||
|
||||
async def test_call_with_args(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
call['params'] = ['aiohttp-jsonrpc']
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'error' not in data
|
||||
assert data['result'] == 'Hello, aiohttp-jsonrpc!'
|
||||
|
||||
async def test_call_with_kwargs(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
call['params'] = {
|
||||
'who': 'aiohttp-jsonrpc',
|
||||
'how': 'Hi,',
|
||||
}
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'error' not in data
|
||||
assert data['result'] == 'Hi, aiohttp-jsonrpc!'
|
||||
|
||||
async def test_result(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
call['params'] = ['aiohttp-jsonrpc']
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'error' not in data
|
||||
assert data['jsonrpc'] == call['jsonrpc']
|
||||
assert data['id'] == call['id']
|
||||
assert data['method'] == call['method']
|
||||
|
||||
async def test_batch(self, homehub_client):
|
||||
call_without_id = self._valid_call(params=['World'])
|
||||
call_without_id.pop('id')
|
||||
|
||||
batch = [
|
||||
self._valid_call(params=['aiohttp-jsonrpc']),
|
||||
self._valid_call(id='testing2', method='testing.fial', params=[]),
|
||||
call_without_id,
|
||||
]
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=batch)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert isinstance(data, list) is True
|
||||
assert len(data) == 2
|
||||
|
||||
first_result, second_result = data
|
||||
|
||||
assert first_result['id'] == batch[0]['id']
|
||||
assert 'error' not in first_result
|
||||
assert first_result['result'] == 'Hello, aiohttp-jsonrpc!'
|
||||
|
||||
assert second_result['id'] == batch[1]['id']
|
||||
assert 'result' not in second_result
|
||||
assert second_result['error']['code'] == rpc.JSONRPCInternalErrorError.ERROR_CODE
|
||||
assert second_result['error']['data'] == 'FIAL'
|
||||
@@ -24,6 +24,7 @@ class Test_BaseService:
|
||||
assert service.app == app
|
||||
assert service.instance == instance
|
||||
assert service.characteristics == characteristics
|
||||
assert service.capabilities == {}
|
||||
assert service.state == 'fake_state'
|
||||
|
||||
@mock.patch.object(app['STATE'], 'get')
|
||||
@@ -202,3 +203,34 @@ class Test_BaseService:
|
||||
'testing.FakeService.fake_instance': 'spam',
|
||||
},
|
||||
})
|
||||
|
||||
def test_register_capability(self):
|
||||
service = FakeService(app, 'fake_instance', {})
|
||||
|
||||
service.register_capability('capability', service.capability)
|
||||
|
||||
expected_capabilities = {
|
||||
'capability': services.CapabilitySpec(
|
||||
service.capability, False, None,
|
||||
),
|
||||
}
|
||||
assert service.capabilities == expected_capabilities
|
||||
|
||||
def test_register_capability_with_public(self):
|
||||
service = FakeService(app, 'fake_instance', {})
|
||||
|
||||
service.register_capability(
|
||||
'capability',
|
||||
service.capability,
|
||||
public=True,
|
||||
public_content_type='text/plain',
|
||||
)
|
||||
|
||||
assert len(service.capabilities) == 1
|
||||
|
||||
expected_capabilities = {
|
||||
'capability': services.CapabilitySpec(
|
||||
service.capability, True, 'text/plain',
|
||||
),
|
||||
}
|
||||
assert service.capabilities == expected_capabilities
|
||||
|
||||
@@ -66,6 +66,8 @@ class Test_UseService:
|
||||
service = testing_services.FakeService(
|
||||
homehub_client.server.app, 'fake_instance', {},
|
||||
)
|
||||
service.register_capability('capability', service.capability)
|
||||
|
||||
homehub_client.server.app['SERVICES'].update({
|
||||
'testing.FakeService.fake_instance': service,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from homehub_backend.testing import services as testing_services
|
||||
|
||||
|
||||
class Test_WebUseService:
|
||||
async def test_unknown_service_instance(self, homehub_client):
|
||||
response = await homehub_client.get('/services/use', params={
|
||||
'kind': 'testing.FakeService',
|
||||
'instance': 'idontexist',
|
||||
'capability': 'capability',
|
||||
'params': 'It works!',
|
||||
})
|
||||
assert response.status == 400
|
||||
|
||||
data = await response.text()
|
||||
assert data == 'Unknown service: idontexist of testing.FakeService'
|
||||
|
||||
async def test_unknown_service_kind(self, homehub_client):
|
||||
response = await homehub_client.get('/services/use', params={
|
||||
'kind': 'IDontExist',
|
||||
'instance': 'fake_instance',
|
||||
'capability': 'capability',
|
||||
'params': 'It works!',
|
||||
})
|
||||
assert response.status == 400
|
||||
|
||||
data = await response.text()
|
||||
assert data == 'Unknown service: IDontExist'
|
||||
|
||||
async def test_unknown_capability(self, homehub_client):
|
||||
service = testing_services.FakeService(
|
||||
homehub_client.server.app, 'fake_instance', {},
|
||||
)
|
||||
homehub_client.server.app['SERVICES'].update({
|
||||
'testing.FakeService.fake_instance': service,
|
||||
})
|
||||
|
||||
response = await homehub_client.get('/services/use', params={
|
||||
'kind': 'testing.FakeService',
|
||||
'instance': 'fake_instance',
|
||||
'capability': 'idontexist',
|
||||
'params': 'It works!',
|
||||
})
|
||||
assert response.status == 400
|
||||
|
||||
data = await response.text()
|
||||
assert data == 'Unknown capability: testing.FakeService.idontexist'
|
||||
|
||||
async def test_capability_not_public(self, homehub_client):
|
||||
service = testing_services.FakeService(
|
||||
homehub_client.server.app, 'fake_instance', {},
|
||||
)
|
||||
service.register_capability('capability', service.capability)
|
||||
|
||||
homehub_client.server.app['SERVICES'].update({
|
||||
'testing.FakeService.fake_instance': service,
|
||||
})
|
||||
|
||||
response = await homehub_client.get('/services/use', params={
|
||||
'kind': 'testing.FakeService',
|
||||
'instance': 'fake_instance',
|
||||
'capability': 'capability',
|
||||
'params': 'It works!',
|
||||
})
|
||||
assert response.status == 400
|
||||
|
||||
data = await response.text()
|
||||
assert data == 'Unknown capability: testing.FakeService.capability'
|
||||
|
||||
async def test_ok(self, homehub_client):
|
||||
service = testing_services.FakeService(
|
||||
homehub_client.server.app, 'fake_instance', {},
|
||||
)
|
||||
service.register_capability(
|
||||
'capability',
|
||||
service.capability,
|
||||
public=True,
|
||||
public_content_type='text/plain',
|
||||
)
|
||||
|
||||
homehub_client.server.app['SERVICES'].update({
|
||||
'testing.FakeService.fake_instance': service,
|
||||
})
|
||||
|
||||
response = await homehub_client.get('/services/use', params={
|
||||
'kind': 'testing.FakeService',
|
||||
'instance': 'fake_instance',
|
||||
'capability': 'capability',
|
||||
'params': 'It works!',
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.text()
|
||||
assert data == 'It works!'
|
||||
@@ -1,10 +1,10 @@
|
||||
-r requirements.txt
|
||||
aiohttp-devtools==0.13.1
|
||||
aiohttp-devtools==1.0.post0
|
||||
flake8==3.8.3
|
||||
flake8-commas==2.0.0
|
||||
pycodestyle==2.6.0
|
||||
pytest==6.0.1
|
||||
pytest-aiohttp==0.3.0
|
||||
pytest-asyncio==0.14.0
|
||||
pytest==7.1.2
|
||||
pytest-aiohttp==1.0.4
|
||||
pytest-asyncio==0.18.3
|
||||
pytest-env==0.6.2
|
||||
twine==2.0.0
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
aiohttp==3.6.2
|
||||
aiojobs==0.2.2
|
||||
aiohttp==3.8.1
|
||||
aiojobs==1.0.0
|
||||
bthlabs-jsonrpc-aiohttp==1.0.0
|
||||
|
||||
@@ -52,6 +52,6 @@ setup(
|
||||
],
|
||||
packages=packages,
|
||||
include_package_data=True,
|
||||
python_requires='>=3.7',
|
||||
python_requires='>=3.10',
|
||||
install_requires=requirements,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user