Skip to content

Commit

Permalink
[Web] Add new webapi endpoint and interactive webapi docs with Swagge…
Browse files Browse the repository at this point in the history
…r-UI

The new webapi served on /api creates separate endpoints for each
exported python function, e.g. api/web/login

Webapi functions are exported with

webapi = WebapiNamespace("Webapi")

@webapi.post
def exported_post_func(self, arg):
   ...

@webapi.get
def exported_get_func(self):
   ...

The interactive webapi documenation is generated with swagger-ui and served
on http://0.0.0.0:8112/webapidoc

The function doc of the exported functions is parsed with GoogleStyleDocPlugin
that relies on 'docstring_parser' which only supported on python >= 3.6.
With python < 3.6, these doc strings are not included in the api docs.
  • Loading branch information
bendikro committed Sep 15, 2020
1 parent 95c4c7d commit 81ae652
Show file tree
Hide file tree
Showing 19 changed files with 1,565 additions and 218 deletions.
10 changes: 5 additions & 5 deletions deluge/tests/daemon_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,17 @@ def get_pytest_basetemp(self, request):
def common_set_up(self):
common.set_tmp_config_dir()
self.listen_port = 58900
self.core = None
# self.listen_port = 58901
self.process_protocol = None
return component.start()

def terminate_core(self, *args):
if args[0] is not None:
if hasattr(args[0], 'getTraceback'):
print('terminate_core: Errback Exception: %s' % args[0].getTraceback())

if not self.core.killed:
d = self.core.kill()
return d
if not self.process_protocol.killed:
return self.process_protocol.kill()

@defer.inlineCallbacks
def start_core(
Expand All @@ -69,7 +69,7 @@ def start_core(

for dummy in range(port_range):
try:
d, self.core = common.start_core(
d, self.process_protocol = common.start_core(
listen_port=self.listen_port,
logfile=logfile,
timeout=timeout,
Expand Down
1 change: 1 addition & 0 deletions deluge/tests/test_httpdownloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from __future__ import unicode_literals

import os
import tempfile
from email.utils import formatdate

Expand Down
40 changes: 28 additions & 12 deletions deluge/tests/test_json_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from deluge.error import DelugeError
from deluge.ui.client import client
from deluge.ui.web.auth import Auth
from deluge.ui.web.json_api import JSON, JSONException
from deluge.ui.web.json_api import ERROR_RESPONSE_CODE, JSON, JsonAPIException

from . import common
from .basetest import BaseTestCase
Expand Down Expand Up @@ -85,9 +85,9 @@ def write(response_str):
self.assertEqual(response['result'], None)
self.assertEqual(response['id'], None)
self.assertEqual(
response['error']['message'], 'JSONException: JSON not decodable'
response['error']['message'], 'JsonAPIException: JSON not decodable'
)
self.assertEqual(response['error']['code'], 5)
self.assertEqual(response['error']['code'], ERROR_RESPONSE_CODE.RPC_ERROR)

request.write = write
request.write_was_called = False
Expand All @@ -102,20 +102,29 @@ def test_handle_request_invalid_method(self):
json_data = {'method': 'no-existing-module.test', 'id': 0, 'params': []}
request.json = json_lib.dumps(json_data).encode()
request_id, result, error = json._handle_request(request)
self.assertEqual(error, {'message': 'Unknown method', 'code': 2})
self.assertEqual(
error,
{
'message': 'Unknown method',
'code': ERROR_RESPONSE_CODE.RPC_UNKNOWN_METHOD,
},
)

def test_handle_request_invalid_json_request(self):
json = JSON()
request = MagicMock()
# Missing method
json_data = {'id': 0, 'params': []}
request.json = json_lib.dumps(json_data).encode()
self.assertRaises(JSONException, json._handle_request, request)
json_data = {'method': 'some.method', 'params': []}
request.json = json_lib.dumps(json_data).encode()
self.assertRaises(JSONException, json._handle_request, request)
self.assertRaises(JsonAPIException, json._handle_request, request)
# Missing param
json_data = {'method': 'some.method', 'id': 0}
request.json = json_lib.dumps(json_data).encode()
self.assertRaises(JSONException, json._handle_request, request)
self.assertRaises(JsonAPIException, json._handle_request, request)
# No id is valid
json_data = {'method': 'system.listMethods', 'params': []}
request.json = json_lib.dumps(json_data).encode()
json._handle_request(request)

def test_on_json_request_invalid_content_type(self):
"""Test for exception with content type not application/json"""
Expand All @@ -124,7 +133,7 @@ def test_on_json_request_invalid_content_type(self):
request.getHeader.return_value = b'text/plain'
json_data = {'method': 'some.method', 'id': 0, 'params': []}
request.json = json_lib.dumps(json_data).encode()
self.assertRaises(JSONException, json._on_json_request, request)
self.assertRaises(JsonAPIException, json._on_json_request, request)


class JSONCustomUserTestCase(JSONBase):
Expand All @@ -148,7 +157,13 @@ def test_handle_request_auth_error(self):
json_data = {'method': 'core.get_libtorrent_version', 'id': 0, 'params': []}
request.json = json_lib.dumps(json_data).encode()
request_id, result, error = json._handle_request(request)
self.assertEqual(error, {'message': 'Not authenticated', 'code': 1})
self.assertEqual(
error,
{
'message': 'Not authenticated',
'code': ERROR_RESPONSE_CODE.RPC_NOT_AUTHENTICATED,
},
)


class RPCRaiseDelugeErrorJSONTestCase(JSONBase):
Expand Down Expand Up @@ -181,6 +196,7 @@ def get_session_id(s_id):
auth = Auth(auth_conf)
request = Request(MagicMock(), False)
request.base = b''
request.path = b'json'
auth._create_session(request)
methods = yield json.get_remote_methods()
# Verify the function has been registered
Expand Down Expand Up @@ -273,7 +289,7 @@ def write(response_str):
'Failure: [Failure instance: Traceback (failure with no frames):'
" <class 'deluge.error.DelugeError'>: DelugeERROR\n]",
)
self.assertEqual(response['error']['code'], 4)
self.assertEqual(response['error']['code'], ERROR_RESPONSE_CODE.RPC_ERROR)

request.write = write
request.write_was_called = False
Expand Down
29 changes: 25 additions & 4 deletions deluge/tests/test_ui_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,23 +370,41 @@ def test_console_unrecognized_arguments(self):


class ConsoleUIWithDaemonBaseTestCase(UIWithDaemonBaseTestCase):
"""Implement Console tests that require a running daemon"""
"""
Implement Console tests that require a running daemon
NOTE: The tests in this class must not be run directly. They must be run
through a subclass that also inherits from unittest.TestCase
"""

def set_up(self):
# Avoid calling reactor.shutdown after commands are executed by main.exec_args()
deluge.ui.console.main.reactor = common.ReactorOverride()
# Avoid calling reactor.stop (in ConsoleUI.quit) after commands are executed by main.exec_args()
mock_reactor = common.ReactorOverride()
self.patcher = mock.patch('deluge.ui.console.main.reactor', mock_reactor)
self.patcher.start()
return UIWithDaemonBaseTestCase.set_up(self)

def tear_down(self):
d = UIWithDaemonBaseTestCase.tear_down(self)

def stop_patch(*args):
# Must stop the patcher as late as possible (after the ConsoleUI.quit method is called)
self.patcher.stop()

d.addCallback(stop_patch)
return d

def patch_arg_command(self, command):
if type(command) == str:
command = [command]
username, password = get_localhost_auth()

self.patch(
sys,
'argv',
self.var['sys_arg_cmd']
+ ['--port']
+ ['58900']
+ [str(self.listen_port)]
+ ['--username']
+ [username]
+ ['--password']
Expand Down Expand Up @@ -427,6 +445,9 @@ def test_console_command_add_move_completed(self):
yield self.exec_command()

std_output = fd.out.getvalue()
print('std_output: %s' % std_output, file=sys.stderr)
raise Exception()

self.assertTrue(
std_output.endswith('move_completed: True\nmove_completed_path: /tmp\n')
or std_output.endswith('move_completed_path: /tmp\nmove_completed: True\n')
Expand Down
10 changes: 8 additions & 2 deletions deluge/tests/test_web_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,14 @@ def test_add_host(self):

# Add invalid port
conn[2] = 'bad port'
ret = self.deluge_web.web_api.add_host(conn[1], conn[2], conn[3], conn[4])
self.assertEqual(ret, (False, 'Invalid port. Must be an integer'))

from deluge.ui.hostlist import InvalidHostPort

with self.assertRaises(InvalidHostPort) as context:
self.deluge_web.web_api.add_host(conn[1], conn[2], conn[3], conn[4])
self.assertEqual(
'Invalid port: "bad port" Must be an integer', str(context.exception)
)

def test_remove_host(self):
conn = ['connection_id', '', 0, '', '']
Expand Down
Loading

0 comments on commit 81ae652

Please sign in to comment.