diff --git a/supysonic/api/__init__.py b/supysonic/api/__init__.py index 21ca9c0..84718da 100644 --- a/supysonic/api/__init__.py +++ b/supysonic/api/__init__.py @@ -193,10 +193,13 @@ class ResponseHelper: for sub, subdict in subelems.iteritems(): subelem = ElementTree.SubElement(elem, sub) ResponseHelper.dict2xml(subelem, subdict) - for seq, dicts in sequences.iteritems(): - for subdict in dicts: + for seq, values in sequences.iteritems(): + for value in values: subelem = ElementTree.SubElement(elem, seq) - ResponseHelper.dict2xml(subelem, subdict) + if isinstance(value, dict): + ResponseHelper.dict2xml(subelem, value) + else: + subelem.text = ResponseHelper.value_tostring(value) @staticmethod def value_tostring(value): diff --git a/tests/__init__.py b/tests/__init__.py index e305a74..3f3bc64 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -11,8 +11,7 @@ import unittest -import base -import managers +import base, managers, api from .test_api import ApiTestCase from .test_frontend import FrontendTestCase @@ -21,6 +20,7 @@ def suite(): suite = unittest.TestSuite() suite.addTest(base.suite()) + suite.addTest(api.suite()) suite.addTest(managers.suite()) suite.addTest(unittest.makeSuite(ApiTestCase)) diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 0000000..6a26dfa --- /dev/null +++ b/tests/api/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# vim:fenc=utf-8 +# +# This file is part of Supysonic. +# Supysonic is a Python implementation of the Subsonic server API. +# +# Copyright (C) 2017 Alban 'spl0k' Féron +# +# Distributed under terms of the GNU AGPLv3 license. + +import unittest + +from .test_response_helper import suite as rh_suite + +def suite(): + suite = unittest.TestSuite() + + suite.addTest(rh_suite()) + + return suite + diff --git a/tests/api/test_response_helper.py b/tests/api/test_response_helper.py new file mode 100644 index 0000000..3aff148 --- /dev/null +++ b/tests/api/test_response_helper.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# vim:fenc=utf-8 +# +# This file is part of Supysonic. +# Supysonic is a Python implementation of the Subsonic server API. +# +# Copyright (C) 2017 Alban 'spl0k' Féron +# +# Distributed under terms of the GNU AGPLv3 license. + +import unittest, sys +from flask import Flask + +import simplejson +from xml.etree import ElementTree + +class AppMock(object): + app = Flask(__name__) + store = None + +class ResponseHelperBaseCase(unittest.TestCase): + def setUp(self): + sys.modules[u'supysonic.web'] = AppMock() + from supysonic.api import ResponseHelper + self.helper = ResponseHelper + +class ResponseHelperJsonTestCase(ResponseHelperBaseCase): + def serialize_and_deserialize(self, d, error = False): + if not isinstance(d, dict): + raise TypeError(u'Invalid tested value, expecting a dict') + + json = self.helper.responsize_json(d, error) + return simplejson.loads(json) + + def process_and_extract(self, d, error = False): + # Basically returns d with additional version and status + return self.serialize_and_deserialize(d, error)[u'subsonic-response'] + + def test_basic(self): + empty = self.serialize_and_deserialize({}) + self.assertEqual(len(empty), 1) + self.assertIn(u'subsonic-response', empty) + self.assertIsInstance(empty[u'subsonic-response'], dict) + + resp = empty[u'subsonic-response'] + self.assertEqual(len(resp), 2) + self.assertIn(u'status', resp) + self.assertIn(u'version', resp) + self.assertEqual(resp[u'status'], u'ok') + + resp = self.process_and_extract({}, True) + self.assertEqual(resp[u'status'], u'failed') + + some_dict = { + u'intValue': 2, + u'someString': u'Hello world!' + } + resp = self.process_and_extract(some_dict) + self.assertIn(u'intValue', resp) + self.assertIn(u'someString', resp) + + def test_lists(self): + resp = self.process_and_extract({ + u'someList': [ 2, 4, 8, 16 ], + u'emptyList': [] + }) + self.assertIn(u'someList', resp) + self.assertNotIn(u'emptyList', resp) + self.assertListEqual(resp[u'someList'], [ 2, 4, 8, 16 ]) + + def test_dicts(self): + resp = self.process_and_extract({ + u'dict': { u's': u'Blah', u'i': 20 }, + u'empty': {} + }) + self.assertIn(u'dict', resp) + self.assertIn(u'empty', resp) + self.assertDictEqual(resp[u'dict'], { u's': u'Blah', u'i': 20 }) + self.assertDictEqual(resp[u'empty'], {}) + + def test_nesting(self): + resp = self.process_and_extract({ + u'dict': { + u'value': u'hey look! a string', + u'list': [ 1, 2, 3 ], + u'emptyList': [], + u'subdict': { u'a': u'A' } + }, + u'list': [ + { u'b': u'B' }, + { u'c': u'C' }, + [ 4, 5, 6 ], + u'final string' + ] + }) + + self.assertEqual(len(resp), 4) # dict, list, status and version + self.assertIn(u'dict', resp) + self.assertIn(u'list', resp) + + d = resp[u'dict'] + l = resp[u'list'] + + self.assertIn(u'value', d) + self.assertIn(u'list', d) + self.assertNotIn('emptyList', d) + self.assertIn(u'subdict', d) + self.assertIsInstance(d[u'value'], basestring) + self.assertIsInstance(d[u'list'], list) + self.assertIsInstance(d[u'subdict'], dict) + + self.assertEqual(l, [ + { u'b': u'B' }, + { u'c': u'C' }, + [ 4, 5, 6 ], + u'final string' + ]) + +class ResponseHelperJsonpTestCase(ResponseHelperBaseCase): + def test_basic(self): + result = self.helper.responsize_jsonp({}, u'callback') + self.assertTrue(result.startswith(u'callback({')) + self.assertTrue(result.endswith(u'})')) + + json = simplejson.loads(result[9:-1]) + self.assertIn(u'subsonic-response', json) + +class ResponseHelperXMLTestCase(ResponseHelperBaseCase): + def serialize_and_deserialize(self, d, error = False): + xml = self.helper.responsize_xml(d, error) + xml = xml.replace(u'xmlns="http://subsonic.org/restapi"', u'') + root = ElementTree.fromstring(xml) + return root + + def assertAttributesMatchDict(self, elem, d): + d = { k: str(v) for k, v in d.iteritems() } + self.assertDictEqual(elem.attrib, d) + + def test_root(self): + xml = self.helper.responsize_xml({ 'tag': {}}) + self.assertIn(u'')) + + def test_basic(self): + empty = self.serialize_and_deserialize({}) + self.assertIsNotNone(empty.find(u'.[@version]')) + self.assertIsNotNone(empty.find(u".[@status='ok']")) + + resp = self.serialize_and_deserialize({}, True) + self.assertIsNotNone(resp.find(u".[@status='failed']")) + + some_dict = { + u'intValue': 2, + u'someString': u'Hello world!' + } + resp = self.serialize_and_deserialize(some_dict) + self.assertIsNotNone(resp.find(u'.[@intValue]')) + self.assertIsNotNone(resp.find(u'.[@someString]')) + + def test_lists(self): + resp = self.serialize_and_deserialize({ + u'someList': [ 2, 4, 8, 16 ], + u'emptyList': [] + }) + + elems = resp.findall(u'./someList') + self.assertEqual(len(elems), 4) + self.assertIsNone(resp.find(u'./emptyList')) + + for e, i in zip(elems, [ 2, 4, 8, 16 ]): + self.assertEqual(int(e.text), i) + + def test_dicts(self): + resp = self.serialize_and_deserialize({ + u'dict': { u's': u'Blah', u'i': 20 }, + u'empty': {} + }) + + d = resp.find(u'./dict') + self.assertIsNotNone(d) + self.assertIsNotNone(resp.find(u'./empty')) + self.assertAttributesMatchDict(d, { u's': u'Blah', u'i': 20 }) + + def test_nesting(self): + resp = self.serialize_and_deserialize({ + u'dict': { + u'value': u'hey look! a string', + u'list': [ 1, 2, 3 ], + u'emptyList': [], + u'subdict': { u'a': u'A' } + }, + u'list': [ + { u'b': u'B' }, + { u'c': u'C' }, + u'final string' + ] + }) + + self.assertEqual(len(resp), 4) # 'dict' and 3 'list's + + d = resp.find(u'./dict') + lists = resp.findall(u'./list') + + self.assertIsNotNone(d) + self.assertAttributesMatchDict(d, { u'value': u'hey look! a string' }) + self.assertEqual(len(d.findall(u'./list')), 3) + self.assertEqual(len(d.findall(u'./emptyList')), 0) + self.assertIsNotNone(d.find(u'./subdict')) + + self.assertEqual(len(lists), 3) + self.assertAttributesMatchDict(lists[0], { u'b': u'B' }) + self.assertAttributesMatchDict(lists[1], { u'c': u'C' }) + self.assertEqual(lists[2].text, u'final string') + +def suite(): + suite = unittest.TestSuite() + + suite.addTest(unittest.makeSuite(ResponseHelperJsonTestCase)) + suite.addTest(unittest.makeSuite(ResponseHelperJsonpTestCase)) + suite.addTest(unittest.makeSuite(ResponseHelperXMLTestCase)) + + return suite + +if __name__ == '__main__': + unittest.main() +