diff --git a/.gitignore b/.gitignore index 28e148770b5..e4b7fe4684c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,21 @@ -*.bak +# Python *.pyc *.pyo -*~ + +# Apple .DS_Store -.ropeproject + +.*project +local.properties +*.bak +*~ /*.log /.Tribler /.coverage /.dir-locals.el /.gdb_history -/.idea/ +**.idea/ /.noseids -/.project -/.pydevproject -/Tribler/.project -/Tribler/.pydevproject /TAGS /bootstraptribler.txt /core @@ -33,10 +34,28 @@ memorydump.out test_file test_file.torrent -# pycharm -**.idea/ - # Ignore nosetest coverage output cover/ htmlcov/ .coverage + +# Android +*.apk + +/android/TriblerService/service/typescript +/android/TriblerService/service/ffmpeg +/android/TriblerService/dist + +/android/TriblerApp/app/src/main/jniLibs +/android/TriblerApp/app/src/main/jni/include/python2.7 +/android/TriblerApp/app/src/main/jni/lib +/android/TriblerApp/app/src/main/assets/private.mp3 + +# Android Studio +/android/TriblerApp/app/*.iml +/android/TriblerApp/*.iml +/android/TriblerApp/app/build +/android/TriblerApp/build +/android/TriblerApp/.gradle +/android/TriblerApp/captures + diff --git a/Tribler/Core/Modules/channel/channel.py b/Tribler/Core/Modules/channel/channel.py index ecdabd7615e..4d8a837a849 100644 --- a/Tribler/Core/Modules/channel/channel.py +++ b/Tribler/Core/Modules/channel/channel.py @@ -36,6 +36,14 @@ def channel_id(self): def name(self): return self._channel_community.get_channel_name() + @property + def description(self): + return self._channel_community.get_channel_description() + + @property + def mode(self): + return self._channel_community.get_channel_mode() + def get_rss_feed_url_list(self): return [url for url in self._rss_feed_dict.iterkeys()] diff --git a/Tribler/Core/Modules/channel/channel_manager.py b/Tribler/Core/Modules/channel/channel_manager.py index 2b4f046c915..d8738edb88f 100644 --- a/Tribler/Core/Modules/channel/channel_manager.py +++ b/Tribler/Core/Modules/channel/channel_manager.py @@ -5,6 +5,7 @@ from Tribler.dispersy.util import blocking_call_on_reactor_thread, call_on_reactor_thread from Tribler.community.channel.community import ChannelCommunity +from Tribler.Core.exceptions import DuplicateChannelNameError from Tribler.Core.Modules.channel.channel import ChannelObject @@ -61,6 +62,8 @@ def create_channel(self, name, description, mode, rss_url=None): :param description: Description of the Channel. :param mode: Mode of the Channel ('open', 'semi-open', or 'closed'). :param rss_url: RSS URL for the Channel. + :return: Channel ID + :raises DuplicateChannelNameError if name already exists """ assert isinstance(name, basestring), u"name is not a basestring: %s" % type(name) assert isinstance(description, basestring), u"description is not a basestring: %s" % type(description) @@ -70,8 +73,7 @@ def create_channel(self, name, description, mode, rss_url=None): # if two channels have the same name, this will not work for channel_object in self._channel_list: if name == channel_object.name: - self._logger.error(u"Channel name already exists: %s", name) - return + raise DuplicateChannelNameError(u"Channel name already exists: %s" % name) channel_mode = self._channel_mode_map[mode] community = ChannelCommunity.create_community(self.dispersy, self.session.dispersy_member, @@ -90,6 +92,7 @@ def create_channel(self, name, description, mode, rss_url=None): channel_obj.create_rss_feed(rss_url) self._logger.debug(u"creating channel '%s', %s", channel_obj.name, hexlify(community.cid)) + return channel_obj.channel_id def get_my_channel(self, channel_id): """ diff --git a/Tribler/Core/Modules/restapi/my_channel_endpoint.py b/Tribler/Core/Modules/restapi/my_channel_endpoint.py index c2b1b6f9131..3012f341b36 100644 --- a/Tribler/Core/Modules/restapi/my_channel_endpoint.py +++ b/Tribler/Core/Modules/restapi/my_channel_endpoint.py @@ -3,6 +3,7 @@ from Tribler.Core.CacheDB.sqlitecachedb import str2bin from Tribler.Core.simpledefs import NTFY_CHANNELCAST +from Tribler.Core.exceptions import DuplicateChannelNameError class MyChannelBaseEndpoint(resource.Resource): @@ -36,31 +37,30 @@ class MyChannelEndpoint(MyChannelBaseEndpoint): This endpoint is responsible for handing all requests regarding your channel such as getting and updating torrents, playlists and rss-feeds. """ + def __init__(self, session): MyChannelBaseEndpoint.__init__(self, session) - child_handler_dict = {"overview": MyChannelOverviewEndpoint, "torrents": MyChannelTorrentsEndpoint, - "rssfeeds": MyChannelRssFeedsEndpoint, "playlists": MyChannelPlaylistsEndpoint, + child_handler_dict = {"torrents": MyChannelTorrentsEndpoint, + "rssfeeds": MyChannelRssFeedsEndpoint, + "playlists": MyChannelPlaylistsEndpoint, "recheckfeeds": MyChannelRecheckFeedsEndpoint} for path, child_cls in child_handler_dict.iteritems(): self.putChild(path, child_cls(self.session)) - -class MyChannelOverviewEndpoint(MyChannelBaseEndpoint): - """ - Return the name, description and identifier of your channel. - This endpoint returns a 404 HTTP response if you have not created a channel (yet). - - Example response: - { - "overview": { - "name": "My Tribler channel", - "description": "A great collection of open-source movies", - "identifier": "4a9cfc7ca9d15617765f4151dd9fae94c8f3ba11" - } - } - """ - def render_GET(self, request): + """ + Return the name, description and identifier of your channel. + This endpoint returns a 404 HTTP response if you have not created a channel (yet). + + Example response: + { + "overview": { + "name": "My Tribler channel", + "description": "A great collection of open-source movies", + "identifier": "4a9cfc7ca9d15617765f4151dd9fae94c8f3ba11" + } + } + """ my_channel_id = self.channel_db_handler.getMyChannelId() if my_channel_id is None: return MyChannelBaseEndpoint.return_404(request) @@ -70,6 +70,48 @@ def render_GET(self, request): return json.dumps({'overview': {'identifier': my_channel[1].encode('hex'), 'name': my_channel[2], 'description': my_channel[3]}}) + def render_PUT(self, request): + """ + Create your own new channel. + + Example request: + { + "name": "John Smit's channel", + "description": "Video's of my cat", + "mode": "open" or "semi-open" or "closed" (default) + } + """ + parameters = http.parse_qs(request.content.read(), 1) + + if 'name' not in parameters or len(parameters['name']) == 0: + request.setResponseCode(http.BAD_REQUEST) + return json.dumps({"error": "name parameter missing"}) + + if 'description' not in parameters or len(parameters['description']) == 0: + request.setResponseCode(http.BAD_REQUEST) + return json.dumps({"error": "description parameter missing"}) + + if 'mode' not in parameters or len(parameters['mode']) == 0: + mode = u'closed' + else: + mode = parameters['mode'][0] + + try: + channel_id = self.session.create_channel(parameters['name'][0], parameters['description'][0], mode) + except DuplicateChannelNameError as ex: + request.setResponseCode(http.INTERNAL_SERVER_ERROR) + request.setHeader('Content-Type', 'text/json') + return json.dumps({ + u"error": { + u"handled": True, + u"code": ex.__class__.__name__, + u"message": ex.message + } + }) + + request.setHeader('Content-Type', 'text/json') + return json.dumps({"added": channel_id}) + class MyChannelTorrentsEndpoint(MyChannelBaseEndpoint): """ diff --git a/Tribler/Core/Modules/restapi/rest_manager.py b/Tribler/Core/Modules/restapi/rest_manager.py index 938bc424579..c5056086d9e 100644 --- a/Tribler/Core/Modules/restapi/rest_manager.py +++ b/Tribler/Core/Modules/restapi/rest_manager.py @@ -1,8 +1,11 @@ +import json import logging +from traceback import format_tb from twisted.internet import reactor from twisted.internet.defer import maybeDeferred -from twisted.web import server +from twisted.python.compat import intToBytes +from twisted.web import server, http from Tribler.Core.Modules.restapi.root_endpoint import RootEndpoint from Tribler.dispersy.taskmanager import TaskManager @@ -24,10 +27,42 @@ def start(self): Starts the HTTP API with the listen port as specified in the session configuration. """ self.site = reactor.listenTCP(self.session.get_http_api_port(), - server.Site(RootEndpoint(self.session))) + server.Site(resource=RootEndpoint(self.session), + requestFactory=RESTRequest)) def stop(self): """ Stop the HTTP API and return a deferred that fires when the server has shut down. """ return maybeDeferred(self.site.stopListening) + + +class RESTRequest(server.Request): + """ + This class gracefully takes care of unhandled exceptions raised during the processing of any request. + """ + defaultContentType = b"text/json" + + def __init__(self, *args, **kw): + server.Request.__init__(self, *args, **kw) + self._logger = logging.getLogger(self.__class__.__name__) + + def processingFailed(self, reason): + self._logger.error(reason) + response = { + u"error": { + u"handled": False, + u"code": reason.value.__class__.__name__, + u"message": reason.value.message + } + } + if self.site.displayTracebacks: + response[u"error"][u"trace"] = format_tb(reason.getTracebackObject()) + + body = json.dumps(response) + self.setResponseCode(http.INTERNAL_SERVER_ERROR) + self.setHeader(b'content-type', self.defaultContentType) + self.setHeader(b'content-length', intToBytes(len(body))) + self.write(body) + self.finish() + return reason diff --git a/Tribler/Core/Session.py b/Tribler/Core/Session.py index 835f878f13e..a7fbb8ec8ff 100644 --- a/Tribler/Core/Session.py +++ b/Tribler/Core/Session.py @@ -637,16 +637,16 @@ def search_remote_channels(self, keywords): raise OperationNotEnabledByConfigurationException("channel_search is not enabled") self.lm.search_manager.search_for_channels(keywords) - def create_channel(self, name, description, mode=u'open'): + def create_channel(self, name, description, mode=u'closed'): """ Creates a new Channel. :param name: Name of the Channel. :param description: Description of the Channel. :param mode: Mode of the Channel ('open', 'semi-open', or 'closed'). + :return: Channel ID + :raises DuplicateChannelNameError if name already exists """ - if not self.get_enable_channel_search(): - raise OperationNotEnabledByConfigurationException("channel_search is not enabled") - self.lm.channel_manager.create_channel(name, description, mode) + return self.lm.channel_manager.create_channel(name, description, mode) def check_torrent_health(self, infohash): """ diff --git a/Tribler/Core/exceptions.py b/Tribler/Core/exceptions.py index d0694bf62b8..0972c126da0 100644 --- a/Tribler/Core/exceptions.py +++ b/Tribler/Core/exceptions.py @@ -48,6 +48,14 @@ def __init__(self, msg=None): TriblerException.__init__(self, msg) +class DuplicateChannelNameError(TriblerException): + + """ The Channel name already exists in the ChannelManager channel list, + i.e., one of your own Channels with the same name already exists. """ + + pass + + class DuplicateDownloadException(TriblerException): """ The Download already exists in the Session, i.e., a Download for @@ -65,6 +73,7 @@ class TorrentDefNotFinalizedException(TriblerException): def __init__(self, msg=None): TriblerException.__init__(self, msg) + class TorrentFileException(TriblerException): """ The torrent file that is used is corrupt or cannot be read. """ diff --git a/Tribler/Test/Core/Modules/RestApi/base_api_test.py b/Tribler/Test/Core/Modules/RestApi/base_api_test.py index 4d4a5abef12..d0163e87e58 100644 --- a/Tribler/Test/Core/Modules/RestApi/base_api_test.py +++ b/Tribler/Test/Core/Modules/RestApi/base_api_test.py @@ -50,7 +50,7 @@ def parse_body(self, body): def parse_response(self, response): self.assertEqual(response.code, self.expected_response_code) - if response.code == 200 or response.code == 400: + if response.code in (200, 400, 500): return readBody(response) return succeed(None) diff --git a/Tribler/Test/Core/Modules/RestApi/test_my_channel_endpoints.py b/Tribler/Test/Core/Modules/RestApi/test_my_channel_endpoints.py index f97c229a2cf..22b788976ea 100644 --- a/Tribler/Test/Core/Modules/RestApi/test_my_channel_endpoints.py +++ b/Tribler/Test/Core/Modules/RestApi/test_my_channel_endpoints.py @@ -4,21 +4,30 @@ from Tribler.Core.Modules.channel.channel_manager import ChannelManager from Tribler.Core.Utilities.twisted_thread import deferred from Tribler.Core.simpledefs import NTFY_CHANNELCAST +from Tribler.Core.exceptions import DuplicateChannelNameError from Tribler.Test.Core.Modules.RestApi.base_api_test import AbstractApiTest class ChannelCommunityMock(object): - def __init__(self, channel_id): - self.name = "Channel name" + def __init__(self, channel_id, name, description, mode): self.cid = 'a' * 20 - self.channel_id = channel_id + self._channel_id = channel_id + self._channel_name = name + self._channel_description = description + self._channel_mode = mode + + def get_channel_id(self): + return self._channel_id def get_channel_name(self): - return self.name + return self._channel_name - def get_channel_id(self): - return self.channel_id + def get_channel_description(self): + return self._channel_description + + def get_channel_mode(self): + return self._channel_mode class AbstractTestMyChannelEndpoints(AbstractApiTest): @@ -35,15 +44,18 @@ def create_my_channel(self, name, description): self.channel_db_handler.on_channel_from_dispersy('fakedispersyid', None, name, description) return self.channel_db_handler.getMyChannelId() - def create_fake_channel(self, channel_name, channel_description): + def create_fake_channel(self, name, description, mode=u'closed'): # Use a fake ChannelCommunity object (we don't actually want to create a Dispersy community) - my_channel_id = self.create_my_channel(channel_name, channel_description) + my_channel_id = self.create_my_channel(name, description) self.session.lm.channel_manager = ChannelManager(self.session) - channel_obj = ChannelObject(self.session, ChannelCommunityMock(my_channel_id)) + channel_obj = ChannelObject(self.session, ChannelCommunityMock(my_channel_id, name, description, mode)) self.session.lm.channel_manager._channel_list.append(channel_obj) return my_channel_id + def create_fake_channel_with_existing_name(self, name, description, mode=u'closed'): + raise DuplicateChannelNameError(u"Channel name already exists: %s" % name) + def insert_torrents_into_my_channel(self, torrent_list): self.channel_db_handler.on_torrents_from_dispersy(torrent_list) @@ -58,12 +70,110 @@ def test_my_channel_unknown_endpoint(self): self.should_check_equality = False return self.do_request('mychannel/thisendpointdoesnotexist123', expected_code=404) + @deferred(timeout=10) + def test_my_channel_endpoint_create(self): + """ + Testing whether the API returns the right JSON data if a channel is created + """ + + def verify_channel_created(body): + channel_obj = self.session.lm.channel_manager._channel_list[0] + self.assertEqual(channel_obj.name, post_data["name"]) + self.assertEqual(channel_obj.description, post_data["description"]) + self.assertEqual(channel_obj.mode, post_data["mode"]) + self.assertDictEqual(json.loads(body), {"added": channel_obj.channel_id}) + + post_data = { + "name": "John Smit's channel", + "description": "Video's of my cat", + "mode": "semi-open" + } + self.session.create_channel = self.create_fake_channel + self.should_check_equality = False + return self.do_request('mychannel', expected_code=200, expected_json=None, + request_type='PUT', post_data=post_data).addCallback(verify_channel_created) + + @deferred(timeout=10) + def test_my_channel_endpoint_create_default_mode(self): + """ + Testing whether the API returns the right JSON data if a channel is created + """ + + def verify_channel_created(body): + channel_obj = self.session.lm.channel_manager._channel_list[0] + self.assertEqual(channel_obj.name, post_data["name"]) + self.assertEqual(channel_obj.description, post_data["description"]) + self.assertEqual(channel_obj.mode, u'closed') + self.assertDictEqual(json.loads(body), {"added": channel_obj.channel_id}) + + post_data = { + "name": "John Smit's channel", + "description": "Video's of my cat" + } + self.session.create_channel = self.create_fake_channel + self.should_check_equality = False + return self.do_request('mychannel', expected_code=200, expected_json=None, + request_type='PUT', post_data=post_data).addCallback(verify_channel_created) + + @deferred(timeout=10) + def test_my_channel_endpoint_create_duplicate_name_error(self): + """ + Testing whether the API returns a formatted 500 error if DuplicateChannelNameError is raised + """ + + def verify_error_message(body): + error_response = json.loads(body) + expected_response = { + u"error": { + u"handled": True, + u"code": u"DuplicateChannelNameError", + u"message": u"Channel name already exists: %s" % post_data["name"] + } + } + self.assertDictContainsSubset(expected_response[u"error"], error_response[u"error"]) + + post_data = { + "name": "John Smit's channel", + "description": "Video's of my cat", + "mode": "semi-open" + } + self.session.create_channel = self.create_fake_channel_with_existing_name + self.should_check_equality = False + return self.do_request('mychannel', expected_code=500, expected_json=None, request_type='PUT', + post_data=post_data).addCallback(verify_error_message) + + @deferred(timeout=10) + def test_my_channel_endpoint_create_no_name_param(self): + """ + Testing whether the API returns a 400 and error if the name parameter is not passed + """ + post_data = { + "description": "Video's of my cat", + "mode": "semi-open" + } + expected_json = {"error": "name parameter missing"} + return self.do_request('mychannel', expected_code=400, expected_json=expected_json, request_type='PUT', + post_data=post_data) + + @deferred(timeout=10) + def test_my_channel_endpoint_create_no_description_param(self): + """ + Testing whether the API returns a 400 and error if the name parameter is not passed + """ + post_data = { + "name": "John Smit's channel", + "mode": "semi-open" + } + expected_json = {"error": "description parameter missing"} + return self.do_request('mychannel', expected_code=400, expected_json=expected_json, request_type='PUT', + post_data=post_data) + @deferred(timeout=10) def test_my_channel_overview_endpoint_no_my_channel(self): """ Testing whether the API returns response code 404 if no channel has been created """ - return self.do_request('mychannel/overview', expected_code=404) + return self.do_request('mychannel', expected_code=404) @deferred(timeout=10) def test_my_channel_overview_endpoint_with_channel(self): @@ -74,7 +184,7 @@ def test_my_channel_overview_endpoint_with_channel(self): u'identifier': 'fakedispersyid'.encode('hex')}} self.create_my_channel(channel_json[u'overview'][u'name'], channel_json[u'overview'][u'description']) - return self.do_request('mychannel/overview', expected_code=200, expected_json=channel_json) + return self.do_request('mychannel', expected_code=200, expected_json=channel_json) @deferred(timeout=10) def test_my_channel_torrents_endpoint_no_my_channel(self): @@ -83,22 +193,23 @@ def test_my_channel_torrents_endpoint_no_my_channel(self): """ return self.do_request('mychannel/torrents', expected_code=404) - def verify_torrents_json(self, body): - torrents_dict = json.loads(body) - self.assertTrue(torrents_dict["torrents"]) - self.assertEqual(len(torrents_dict["torrents"]), 1) - @deferred(timeout=10) def test_torrents_endpoint_with_channel(self): """ Testing whether the API returns the right JSON data if a torrents from a channel are fetched """ + + def verify_torrents_json(body): + torrents_dict = json.loads(body) + self.assertTrue(torrents_dict["torrents"]) + self.assertEqual(len(torrents_dict["torrents"]), 1) + self.should_check_equality = False my_channel_id = self.create_my_channel("my channel", "this is a short description") torrent_list = [[my_channel_id, 1, 1, ('a' * 40).decode('hex'), 1460000000, "ubuntu-torrent.iso", [], []]] self.insert_torrents_into_my_channel(torrent_list) - return self.do_request('mychannel/torrents', expected_code=200).addCallback(self.verify_torrents_json) + return self.do_request('mychannel/torrents', expected_code=200).addCallback(verify_torrents_json) @deferred(timeout=10) def test_rss_feeds_endpoint_no_my_channel(self): @@ -117,8 +228,9 @@ def test_rss_feeds_endpoint_with_channel(self): Testing whether the API returns the right JSON data if a rss feeds from a channel are fetched """ expected_json = {u'rssfeeds': [{u'url': u'http://test1.com/feed.xml'}, {u'url': u'http://test2.com/feed.xml'}]} - self.create_fake_channel("my channel", "this is a short description") - channel_obj = self.session.lm.channel_manager.get_channel("Channel name") + channel_name = "my channel" + self.create_fake_channel(channel_name, "this is a short description") + channel_obj = self.session.lm.channel_manager.get_channel(channel_name) for rss_item in expected_json[u'rssfeeds']: channel_obj.create_rss_feed(rss_item[u'url']) @@ -151,6 +263,7 @@ def test_add_rss_feed_with_channel(self): """ Testing whether the API returns a 200 if a channel has been created and when adding a rss feed """ + def verify_rss_added(_): channel_obj = self.session.lm.channel_manager.get_my_channel(my_channel_id) self.assertEqual(channel_obj.get_rss_feed_url_list(), ["http://rssfeed.com/rss.xml"]) diff --git a/Tribler/Test/Core/Modules/RestApi/test_rest_manager.py b/Tribler/Test/Core/Modules/RestApi/test_rest_manager.py new file mode 100644 index 00000000000..0647ba8e13d --- /dev/null +++ b/Tribler/Test/Core/Modules/RestApi/test_rest_manager.py @@ -0,0 +1,37 @@ +import json + +from Tribler.Core.exceptions import TriblerException + +from base_api_test import AbstractApiTest + + +class RestRequestTest(AbstractApiTest): + + def throw_unhandled_exception(self, name, description, mode=u'closed'): + raise TriblerException(u"Oops! Something went wrong. Please restart Tribler") + + def test_unhandled_exception(self): + """ + Testing whether the API returns a formatted 500 error if an unhandled Exception is raised + """ + + def verify_error_message(body): + error_response = json.loads(body) + expected_response = { + u"error": { + u"handled": False, + u"code": u"TriblerException", + u"message": u"Oops! Something went wrong. Please restart Tribler" + } + } + self.assertDictContainsSubset(expected_response[u"error"], error_response[u"error"]) + + post_data = { + "name": "John Smit's channel", + "description": "Video's of my cat", + "mode": "semi-open" + } + self.session.create_channel = self.throw_unhandled_exception + self.should_check_equality = False + return self.do_request('mychannel', expected_code=500, expected_json=None, request_type='PUT', + post_data=post_data).addCallback(verify_error_message) diff --git a/Tribler/Test/Core/Modules/channel/__init__.py b/Tribler/Test/Core/Modules/channel/__init__.py new file mode 100644 index 00000000000..7cd1ded3af7 --- /dev/null +++ b/Tribler/Test/Core/Modules/channel/__init__.py @@ -0,0 +1,3 @@ +""" +This testing package contains tests for the channel module. +""" diff --git a/Tribler/Test/Core/Modules/channel/test_channel_manager.py b/Tribler/Test/Core/Modules/channel/test_channel_manager.py new file mode 100644 index 00000000000..db247711c7b --- /dev/null +++ b/Tribler/Test/Core/Modules/channel/test_channel_manager.py @@ -0,0 +1,33 @@ +from Tribler.Test.Core.base_test import TriblerCoreTest +from Tribler.Core.Session import Session +from Tribler.Core.SessionConfig import SessionStartupConfig +from Tribler.Core.Modules.channel.channel import ChannelObject +from Tribler.Core.exceptions import DuplicateChannelNameError +from Tribler.Core.Modules.channel.channel_manager import ChannelManager +from Tribler.dispersy.util import blocking_call_on_reactor_thread + + +class TestChannelManager(TriblerCoreTest): + + @blocking_call_on_reactor_thread + def test_create_channel_duplicate_name_error(self): + self.config = SessionStartupConfig() + self.session = Session(self.config, ignore_singleton=True) + + class LmMock(object): + channel_manager = ChannelManager(self.session) + + self.session.lm = LmMock() + + class MockCommunity(object): + cid = "" + + def get_channel_name(self): + return "Channel name" + + channel_obj = ChannelObject(self.session, MockCommunity(), is_created=True) + self.session.lm.channel_manager._channel_list = [channel_obj] + + with self.assertRaises(DuplicateChannelNameError) as cm: + self.session.lm.channel_manager.create_channel("Channel name", "description", "open") + self.assertEqual(cm.exception.message, u"Channel name already exists: Channel name") diff --git a/Tribler/Test/Core/test_session.py b/Tribler/Test/Core/test_session.py index 6208fd76ce9..238239605e9 100644 --- a/Tribler/Test/Core/test_session.py +++ b/Tribler/Test/Core/test_session.py @@ -39,3 +39,19 @@ def test_torrent_store_delete(self): session.lm.torrent_store.close() self.assertTrue(raised_key_error) + + def test_create_channel(self): + """ + Test the pass through function of Session.create_channel to the ChannelManager. + """ + + class LmMock(object): + class ChannelManager(object): + def create_channel(self, name, description, mode=u"closed"): + pass + channel_manager = ChannelManager() + + config = SessionStartupConfig() + session = Session(config, ignore_singleton=True) + session.lm = LmMock + session.create_channel("name", "description", "open") diff --git a/doc/Tribler REST API.md b/doc/Tribler REST API.md index cc8381644fd..7ec3d2870b0 100644 --- a/doc/Tribler REST API.md +++ b/doc/Tribler REST API.md @@ -12,6 +12,29 @@ Some requests require one or more parameters. These parameters are passed using curl -X PUT http://localhost:8085/mychannel/rssfeeds/http%3A%2F%2Frssfeed.com%2Frss.xml ``` +## Error handling + +If an unhandled exception occurs the response will have code HTTP 500 and look like this: +```json +{ + "error": { + "handled": False, + "code": "SomeException", + "message": "Human readable error message" + } +} +``` +If a valid request of a client caused a recoverable error the response will have code HTTP 500 and look like this: +```json +{ + "error": { + "handled": True, + "code": "DuplicateChannelNameError", + "message": "Channel name already exists: foo" + } +} +``` + ## Endpoints ### Channels @@ -28,7 +51,8 @@ curl -X PUT http://localhost:8085/mychannel/rssfeeds/http%3A%2F%2Frssfeed.com%2F | Endpoint | Description | | ---- | --------------- | -| GET /mychannel/overview | Get the name, description and identifier of your channel | +| GET /mychannel | Get the name, description and identifier of your channel | +| PUT /mychannel | Create your own new channel | | GET /mychannel/torrents | Get a list of torrents in your channel | | GET /mychannel/rssfeeds | Get a list of rss feeds used by your channel | | PUT /mychannel/rssfeeds/{feedurl} | Add a rss feed to your channel | @@ -149,7 +173,7 @@ Unsubscribe from a specific channel. Returns error 404 if you are not subscribed } ``` -## `GET /mychannel/overview` +## `GET /mychannel` Returns an overview of the channel of the user. This includes the name, description and identifier of the channel. @@ -165,6 +189,20 @@ Returns an overview of the channel of the user. This includes the name, descript } ``` +## `PUT /mychannel` + +Create your own new channel. + +### Example request: + +```json +{ + "name": "John Smit's channel", + "description": "Video's of my cat", + "mode" (optional): "open" or "semi-open" or "closed" (default) +} +``` + ## `GET /mychannel/torrents` Returns a list of torrents in your channel. Each torrent item in the list contains the infohash, name and the timestamp of addition of the torrent.