diff --git a/beetsplug/subsonicupdate.py b/beetsplug/subsonicupdate.py index 29b01986ed..45fc3a8cb0 100644 --- a/beetsplug/subsonicupdate.py +++ b/beetsplug/subsonicupdate.py @@ -35,47 +35,6 @@ __author__ = 'https://github.com/maffo999' -def create_token(): - """Create salt and token from given password. - - :return: The generated salt and hashed token - """ - password = config['subsonic']['pass'].as_str() - - # Pick the random sequence and salt the password - r = string.ascii_letters + string.digits - salt = "".join([random.choice(r) for _ in range(6)]) - salted_password = password + salt - token = hashlib.md5().update(salted_password.encode('utf-8')).hexdigest() - - # Put together the payload of the request to the server and the URL - return salt, token - - -def format_url(): - """Get the Subsonic URL to trigger a scan. Uses either the url - config option or the deprecated host, port, and context_path config - options together. - - :return: Endpoint for updating Subsonic - """ - - url = config['subsonic']['url'].as_str() - if url and url.endsWith('/'): - url = url[:-1] - - # @deprecated("Use url config option instead") - if not url: - host = config['subsonic']['host'].as_str() - port = config['subsonic']['port'].get(int) - context_path = config['subsonic']['contextpath'].as_str() - if context_path == '/': - context_path = '' - url = "http://{}:{}{}".format(host, port, context_path) - - return url + '/rest/startScan' - - class SubsonicUpdate(BeetsPlugin): def __init__(self): super(SubsonicUpdate, self).__init__() @@ -90,10 +49,51 @@ def __init__(self): config['subsonic']['pass'].redact = True self.register_listener('import', self.start_scan) + @staticmethod + def __create_token(): + """Create salt and token from given password. + + :return: The generated salt and hashed token + """ + password = config['subsonic']['pass'].as_str() + + # Pick the random sequence and salt the password + r = string.ascii_letters + string.digits + salt = "".join([random.choice(r) for _ in range(6)]) + salted_password = password + salt + token = hashlib.md5(salted_password.encode('utf-8')).hexdigest() + + # Put together the payload of the request to the server and the URL + return salt, token + + @staticmethod + def __format_url(): + """Get the Subsonic URL to trigger a scan. Uses either the url + config option or the deprecated host, port, and context_path config + options together. + + :return: Endpoint for updating Subsonic + """ + + url = config['subsonic']['url'].as_str() + if url and url.endswith('/'): + url = url[:-1] + + # @deprecated("Use url config option instead") + if not url: + host = config['subsonic']['host'].as_str() + port = config['subsonic']['port'].get(int) + context_path = config['subsonic']['contextpath'].as_str() + if context_path == '/': + context_path = '' + url = "http://{}:{}{}".format(host, port, context_path) + + return url + '/rest/startScan' + def start_scan(self): user = config['subsonic']['user'].as_str() - url = format_url() - salt, token = create_token() + url = self.__format_url() + salt, token = self.__create_token() payload = { 'u': user, diff --git a/test/test_subsonic.py b/test/test_subsonic.py new file mode 100644 index 0000000000..6d37cdf4f4 --- /dev/null +++ b/test/test_subsonic.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- + +"""Tests for the 'subsonic' plugin""" + +from __future__ import division, absolute_import, print_function + +import requests +import responses +import unittest + +from test import _common +from beets import config +from beetsplug import subsonicupdate +from test.helper import TestHelper +from six.moves.urllib.parse import parse_qs, urlparse + + +class ArgumentsMock(object): + def __init__(self, mode, show_failures): + self.mode = mode + self.show_failures = show_failures + self.verbose = 1 + + +def _params(url): + """Get the query parameters from a URL.""" + return parse_qs(urlparse(url).query) + + +class SubsonicPluginTest(_common.TestCase, TestHelper): + @responses.activate + def setUp(self): + config.clear() + self.setup_beets() + + config["subsonic"]["user"] = "admin" + config["subsonic"]["pass"] = "admin" + config["subsonic"]["url"] = "http://localhost:4040" + + self.subsonicupdate = subsonicupdate.SubsonicUpdate() + + def tearDown(self): + self.teardown_beets() + + @responses.activate + def test_start_scan(self): + responses.add( + responses.POST, + 'http://localhost:4040/rest/startScan', + status=200 + ) + + self.subsonicupdate.start_scan() + + @responses.activate + def test_url_with_extra_forward_slash_url(self): + config["subsonic"]["url"] = "http://localhost:4040/contextPath" + + responses.add( + responses.POST, + 'http://localhost:4040/contextPath/rest/startScan', + status=200 + ) + + self.subsonicupdate.start_scan() + + @responses.activate + def test_url_with_context_path(self): + config["subsonic"]["url"] = "http://localhost:4040/" + + responses.add( + responses.POST, + 'http://localhost:4040/rest/startScan', + status=200 + ) + + self.subsonicupdate.start_scan() + + @responses.activate + def test_url_with_missing_port(self): + config["subsonic"]["url"] = "http://localhost/airsonic" + + responses.add( + responses.POST, + 'http://localhost:4040/rest/startScan', + status=200 + ) + + with self.assertRaises(requests.exceptions.ConnectionError): + self.subsonicupdate.start_scan() + + @responses.activate + def test_url_with_missing_schema(self): + config["subsonic"]["url"] = "localhost:4040/airsonic" + + responses.add( + responses.POST, + 'http://localhost:4040/rest/startScan', + status=200 + ) + + with self.assertRaises(requests.exceptions.InvalidSchema): + self.subsonicupdate.start_scan() + + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + + +if __name__ == '__main__': + unittest.main(defaultTest='suite')